秒杀项目,很多人都有做过大大小小,有单体秒杀,微服务秒杀。
从古至今,秒杀项目在面试中还是会被问到,
作为后端工程师,我们不仅要让系统“跑得快”,更要让系统在极限压力下“跑得稳”、“算得准”。
秒杀业务,本质上是一个“读多写少”的极端场景。其核心挑战可以归结为三点:
传统的单体架构或简单的微服务架构,在如此压力下,数据库会迅速成为瓶颈,导致连接池耗尽、CPU飙高,最终整个系统雪崩。
先看看秒杀一般都架构设计

微服务秒杀架构设计一般是:
作为后端工程师,我们不仅要让系统“跑得快”,更要让系统在极限压力下“跑得稳”、“算得准”。
秒杀业务,本质上是一个“读多写少”的极端场景。其核心挑战可以归结为三点:
“从逆向流程思考:用户秒杀失败阶段(没进秒杀页面,打开秒杀页面结束,支付失败,超时取消)
从用请求秒杀到成功到流程,可以概括为几个大类问题:
“因为后端是异步的,所以前端不能直接同步等待结果。”
参加的秒杀的人虽然多,但是秒杀成功的不多
因为需要轮询的并发压力,与秒杀开始的瞬时并发,完全不在一个数量级上。
参加的秒杀的人虽然多,但是秒杀成功的不多
“核心答案: 支付失败(包括用户主动取消、余额不足、支付渠道问题等)后,核心操作是 “释放锁定的库存”,并将订单状态置为“已关闭”,最终结果对用户而言就是 “秒杀失败”。
处理流程:
INCR 命令,将预扣的库存加回去。locked_stock - 1),恢复可用库存(available_stock + 1)。“关键点:支付失败后,商品必须重新放回库存池,让其他等待的用户有机会购买。这是一个标准的库存回补流程。
“超时未支付更加麻烦一点,因为超时可能是 10分钟,而你的秒杀活动 3分钟就结束了你还不还 库存都没意义了
对于超时未支付:“这是一个更复杂的问题,它揭示了秒杀场景的一个固有矛盾。我们的解决方案是分层的:
seckill_user:{activity_id}:{user_id}seckill_user:888:12345。用户防重(set NX) + 扣库存(DECR)。如果你源源不断一直有人参加秒杀,那就可以做到,不然就做不到
在技术层面,我们无法100%保证商品刚好全部卖完。我们的核心架构目标是 第一,绝不超卖;第二,尽可能减少少卖。
潜台词:redis撑不住
直接转Kafka,MySQL能撑住吗?能! MySQL压力只与消息消费速率有关,与秒杀瞬时并发无关
命中率降低,库存不均的问题,有些先抢完了,有些没抢完不需要解决的,因为后面只要有人参加,最终都会抢完
key0...key9
随机一个起点,keyN,如果 keyN 没有,就到 keyN+1.最多轮询 3个
stealing,keyN 没了,从 keyX 里面拿走一半
1. 客户端轮询方案
key0, key1, ..., key9。keyN 作为起点。key 进行扣减。keyN 已售罄,则顺序查询 keyN+1, keyN+2, ...,最多轮询 3 个。2. 服务端代理与动态路由方案
key**。key。3. 库存窃取方案
keyN。keyN 已售罄,不是简单地轮询下一个,而是从一个事先指定的、库存可能较多的“备用Key”(如 keyX)中,“窃取”一部分库存(例如一半)到 keyN**。keyN。1.最简单的做法,redis的decr命令,decr之后返回的库存数量>=0就是秒杀成功
使用decr扣库存有什么问题
不做幂等谁没有必要,非幂等操作,在超时情况下也无法重试,只能返回失败
2.用lua脚本(GET,检查,扣库存)+内嵌一个幂等操作
3.代码get出来,用lua脚本cas操作
(1)从mysql读出数据写redis也会失败
(2)Redis-kafka-mysql(能够缓解问题,尽可能保证我东西都卖出去)
(3)当我redis扣减库存0的时候,再次尝试同步mysql数据
“确保不同的key,落到不同的redis cluster集群上 手动抓几个key,然后计算crc16得到槽,再根据映射关系得到目标节点 如果目标节点重复,就还一个key
消费失败,重试也失败,返回秒杀失败
当发送到Kafka成功,但消费者处理失败时,标准的处理模式是:重试机制 -> 死信队列 -> 最终处理。
UPDATE stock SET stock = stock - 1 WHERE stock > 0)是防止超卖的最终且最可靠的一道防线。“面试点睛:强调这里是保证“不超卖”的终极手段,任何到达这里并发现库存不足的请求,都是正常业务结果,而非系统错误。
参考文献:
https://www.bilibili.com/video/BV1N4b8zEEm3/?spm_id_from=333.1387.upload.video_card.click