架构
秒杀系统需要单独部署 ,如果说放在订单服务里面,秒杀的系统压力太大了就会影响正常的用户下单。
常用架构:
Redis 数据倾斜问题
第一步扣减库存时
假设现在有 10 个商品需要秒杀,正常情况下,这 10 个商品应该均匀的分布在 Redis 集群的每个主节点上。商品是通过下图的算法通过商品 id 计算出自己应当在哪个分片并定位到 Redis 的,但是可能由于分片算法不太均匀,导致这 10 个商品都落到了某一个节点上去。
解决 :因此我们需要给 Redis 的 key 加一个 hashtag:{redis1},这样 Redis 计算分片时就会用 {} 里的数据来计算。
依旧会有问题(热点 key 问题):假设商品 1 非常火爆,10w 个请求都去秒杀商品 1 ,就会导致其对应的 Redis 扛不住压力。
解决:像这种热门商品应当有预判(如:原价 2000 的商品现在只要 500),提前将该商品的库存均匀的分片在多个 Redis 主节点上。当第一个请求进入时,先判断该商品是不是热点商品,如果是,请求第一个分片。当第二个请求进入时,先判断该商品是不是热点商品,如果是,请求第二个分片。
MQ 消息丢失问题
第二步,扣减库存后,将订单消息发送到 MQ
不同的 MQ 解决方案不同,一个简单通用的方案是:加一张消息发送表,先在消息发送表中记录"待处理"然后再给 MQ 发消息,消费者(下单服务)收到消息生成完订单后,回调发送者(抢购服务)将记录改为"已处理"。
设置一个定时任务,隔一段时间去扫描消息发送表,如果发现有消息一直没有被处理,消息很有可能丢失了,那就重发该消息。但是这种方案可能导致消息的重复发送,消费者需要做幂等处理。
分布式事务方案 -- 最终一致性,seata 比较重,很多中小型公司采用这种方案
分布式事务方案 -- 最终一致性,seata 比较重,很多中小型公司采用这种方案
消费者如何做幂等处理?
- 如果消费者收到消息时,订单号已经生成了,那只需要判断一下该订单号是否存在即可
- 如果消费者收到消息时没有订单号,订单号是消费者处理消息时通过一些算法(雪花)生成的,就不能直接判断订单号是否存在。
- 可以生成一个标识唯一消息的业务 id
- 可以在下单逻辑处理的第一行代码加上一把分布式锁。直接用 Redis 实现,执行 setnx 命令,关键在于 key 的设计。setnx(userId + productId, value),同一个用户秒杀同一件商品 key 是一样的。
MQ 消息积压怎么解决?
生产者的生产速度远远大于消费者的消费速度,就会导致 MQ 消息积压。
- 增加消费者数量
- 增加消息队列的容量
依旧有可能消息积压,因为 Redis 扣减库存的速度比数据库高几个量级。
解决:假设消费者拿到一条消息,先判断消息的发送时间,如果这条消息的发送时间和当前时间已经超过了一个阈值(1 分钟),那么就认为出现了消息积压,则将这些消息直接放入 Redis。用户查询订单信息会先查 Redis,再查数据库,在 Redis 查到就可以直接返回了。
该消息最终还是得同步到数据库中生成订单,同步完后再从 Redis 里面删除。
如果说超时了太久依旧没有被处理,就直接丢弃掉该消息,提示用户下单失败。
Redis 集群崩溃了如何保证高可用?
- 操作 Redis 时网络不稳定出现瞬断 -- 降级
在减库存代码上套一层 try-catch,在 catch 里面重试 1、2 次
- 如果网络瞬断比较长或者 Redis 集群真的崩溃了 -- 本地缓存
解决:在抢购服务设置一个本地高速缓存 rocksDB,将下单请求临时存到 rocksDB 里面。然后设置一个定时任务定时去扫描这个缓存,将下单请求重新发送到 Redis 里面。(前提是 Redis 集群能够快速恢复)
库存超卖
主节点扣减库存成功了,但是在同步到从节点前主节点宕机了,从节点被选为了新的主节点,Redis 与数据库数据也不一致了。
- 将 Redis 主从异步同步改为同步同步
- 不使用 Redis 主从,只用 Redis 集群
- 数据库的数据已经为 0 了生成订单不成功
回答:
- 使用并发控制来确保扣减的原子性 --- 影响性能
- 在高并发场景下,可以将库存提前预热到 Redis 中,利用 Redis 的原子操作指令扣减库存,后续对于库存的扣减只操作 Redis
- 如果某个商品的访问量比较大,我们可以对这个商品的库存进行拆分,把不同的库存放进不同的库里面,后续对单个数据加锁