「回顾2022,展望2023,我正在参与2022年终总结征文大赛活动」
不写文章还有点不自在,完了(有点懒了,同步文章~)
今天来分享下这段时刻学习的一个秒杀项目,黑马点评。
好久没跟着学这些,感觉十分十分好玩,又想捣鼓点项目玩玩了 哈哈。
我也简略建立了一个,用到了这些技能。
Springboot2 + Redis7 + Lua + Redisson + MySQL8 + RabbitMQ3.9 + MybatisPlus + Hutool
其中 Redis 和 MySQL 都是之前建立在云端的 K8S 上的 主从 结构,用 Traefik 做总网关。
RabbitMQ 则是之前在本地虚拟机上用 docker 建立的 ,还有 Prometheus + Grafana 监控。
思路
躲藏秒杀地址
这个就是实现一个用户一个地址,给脚本东西加点难度。
根据需要生成这个 path,比方用 md5 混淆下 。
然后放到 Redis 中 key :秒杀活动ID+’path‘ + 秒杀产品ID+用户ID , value :path
真实的秒杀地址如下
lua 脚本预扣库存
用 lua 脚本来确保这个操作的原子性,判别 库存key 存不存在,数量够不够,够的话履行扣减操作
我这样写的脚本是有问题的,没有进行 重复订单校验 , 以及 set 这个 订单信息 到 redis 中。
这 3 步操作应该是原子性的,校验,扣减,设置
所以即使 lua 脚本能确保操作的原子性,可是并发状况下会呈现 少卖 的状况。
改正后也就正常了,之前我是老想着 订单ID 的生成要从 分布式ID 中获取,想尽量较少这个 网络恳求 的,一不小心就忽略了。(今后得先把 中心思路 写下,再思考优化,不能边写边想优化了)
分布式ID ,我之前研讨这个 美团Leaf 也是想简略建立一个,奈何总喜欢偷懒,这儿我是用 Hutool 的 雪花算法 简略生成的。
保存订单信息到 Redis
出 大bug 之前,我以为这儿只是做 重复订单校验 的,没想到,还有这种状况
MQ 挂了,音讯还没发送出去,乃至一开端就没连接上的状况。
比方 我这个本机和虚拟机 休眠后得重启下 虚拟网络vm8,不然连不上去。
所以,这儿得写个小脚本,将 订单信息 发送到 MQ 中,在紧迫状况下能快速补救。
分布式锁
现在用 jvm 级别的锁其实就足够了,但后边上集群还得改代码,干脆趁热打铁。
锁的粒度,不能太大,首要避免用户重复下单。
比方
- 第一版 错误的 lua 脚本中,就会呈现 重复下单 的状况
- 集群形式下,多个顾客的状况,此刻谁先拿到分布式锁,谁就能够消费这个订单,避免重复下单
经过分布式锁,确保这个订单只有一个顾客消费,即使在多个顾客形式下,也不会呈现 重复下单 的状况。
同时,也能够避免运用 Redis 呈现意外,就像上面 错误运用 lua 脚本的事例,以及 可能存在的 key 过期等问题导致的重复下单问题。
当然,这还不是 兜底方案 ,如果这个 分布式锁 也呈现意外了呢,所以稳妥起见,还需要给 订单表 树立 仅有索引(用户id+产品id),靠数据库自身确保了。
这儿如果不用分布式锁,那就得从数据库层面去确保了,得用 select …… for update 敞开 失望锁,那效率会进一步下降的。
注意,这儿也是 缓存击穿 的常见处理思路,分布式锁,两层查看锁形式。
业务
我这儿是简易版的,没有涉及到 分库分表,所以也谈不上这个 分布式业务。
这儿我用的 编程式业务 ,毕竟 扣减库存和保存订单 要在一个业务里,用注解的话还得考虑这个失效的场景,获取这个代理对象去履行,没有这个 编程式业务 来得方便。
假定 订单在订单库中,产品在产品库中,那这种状况下,是不是还得考虑这个 分布式业务 呢?
我可能仍是不会选择这个 分布式业务 ,我会直接往 产品库 中 树立一个 秒杀订单表 或许在 订单库 中树立这个 秒杀产品库存表,乃至专门弄一个 秒杀库 , 冗余 一下,事后如果需要同步到相应的 库表 中,再进行相关的操作。
那假如还有个积分体系呢 ?
比方 付出回调后,更新订单状况的同时,还要更新这个用户积分。
这我仍是会选择 MQ ,经过 MQ 的可靠性 来到达这个 最终一致性
先发送音讯到积分体系,更新订单信息单独在业务中。
这是分布式业务中常见的一种处理方案 基于MQ可靠性音讯的最终一致性方案。
有时刻能够学习下 Seata
重试机制
上图将 MySQL 和 MQ 的操作放一同,还得小心这个 MQ 的反常,导致这个 业务回滚,可是 ACK 仍是正常发出去的状况。
这儿我最后还将反常抛出去,是为了触发这个 重试机制 ,配置文件中 敞开 RabbitMQ 顾客重试机制即可。
ACK 前产生反常,业务回滚,触发重试机制。
ACK 中产生反常,捕获,丢弃反常,提交业务。再次消费时,发现是重复订单。
ACK 后还有反常,未捕获,业务回滚,但音讯现已被 ACK,触发了重试机制,在重试期间没有反常,则正常处理。如果重试后还有反常,则会呈现 音讯丢掉 的状况,这又得 紧迫处理 了。
避免超卖
有两个扣减动作
Redis 预扣库存,这儿得在 lua 脚本中操作。
MySQL 扣减库存,这儿中心就是 达观锁的方法 a=a-1 where a > 0;
缓存
这儿再简短啰嗦下
缓存穿透
针对不存在的 key ,能够用 布隆过滤器
缓存击穿
key 刚好过期,或许 产品成了爆款
用 分布式锁 , 两层查看锁形式 能处理上面这两种状况,锁的粒度也是这个 产品。
针对 key 刚好过期 的状况,我了解到一种新的处理思路:逻辑过期
不在 Redis 中判别是否过期,在 代码 中进行判别,过期的话获取锁,开线程去更新,但实现起来比较复杂。
缓存雪崩
大量 key 同时过期,能够 给不同的Key的TTL增加随机值 ,给业务增加多级缓存 ,降级限流战略安排上
总结
到这儿,这个简易秒杀体系就介绍完了,至于 限流,用户鉴权,符号 ,订单付出,超时处理,音讯的顺序性 …… 再到大一点的 集群,缓存一致性 等等东西,得抽暇再完善下了。
建立过程中,最有意思的是,一向防着 超卖,成果还呈现了 少卖 的场景
所以这 Redis 预扣库存 也得谨慎呀,lua脚本 三合一疗程:查,扣,存
MySQL 也相同,分布式锁,业务 ,查,扣,存
希望届时能把笔记中的技能都过一遍。
下面是我用 JMeter 测验的一些数据状况
JMeter
这儿两个 http 恳求别离模仿,获取秒杀地址,开端秒杀。
陈述一
这个 均匀响应 是 326 ms , 50 % 的恳求是 245 ms,99% 是 1342 ms ,最小是 21 ms,最大是 1359 ms ,吞吐量是 605/s 。
这个成果。。一言难尽,这仍是用了 MQ 异步下单 ,还有 内存符号,Redis 预扣库存 的成果,并且是 预热了 JVM 的状况
这最大的开销应该是网络问题,要拜访 云服务器 K8S 中的 Redis 以及 本地虚拟机上的 MQ。
或许是我的老伙计功能问题,又得跑项目,还得测验,这 CPU ,内存,网卡 估量也忙坏了。
简略剖析下
获取秒杀地址 , 这儿就拜访一次 Redis ,履行 Set 命令。
开端秒杀 中,涉及的网路操作有
- 校验地址
- 是否重复下单
- 预扣库存 lua 脚本
- 发送订单信息到 MQ(虚拟机上)
后边把项目建立到云服务器上再来测下。
陈述二
这儿看到 第一个恳求 的 RT 都比第二个恳求的 小。
Redis
能够看到,内存多了 0.1M 左右,这是多了 601 个 key
至于怎样多了 32 条 client connection , 只能做个简略的估测先了
项目中运用了这个 redisson 做分布式锁,占用了 25 条
简略看下源码
拿到服务器上的一切连接,排掉之前的 5 条,刚好剩下 32 条。
这儿看到运用 resp3 的有 7 条,刚好契合,应该是 RedisTemplate 相关创立的。
这儿简略看下源码, Redis 6 开端默许运用 RESP3 的协议的
RabbitMQ
下面是从 Prometheus + Grafana 监控截取的
这儿 发送端和消费端 在一个应用上,共用一条 connection, 发送端创立了 24 个 channel , 消费端 2 个。
从这个监控图能够看到,消费端开端消费的时刻点大约是 16:47:00
而生产者发送第一条音讯和被confirm 的时刻大约是 16:46:30 ; 这个有差错是因为这个监控主动改写的频率是 15s ,现在是最小的了(可能是我挑的模板问题,或许是这并发太小)
K8S
minikube 节点,上面运行了 Redis 主从 , MySQL 主从。
根本没变化。
后边再把 MQ 和 镜像仓库建立一下,然后再把项目丢上去跑跑看看 ,届时再看看这个测验陈述。
over!