秒杀的特点是:
-
高并发 同一时间有几十万、上百万请求同时抢一个库存。
-
库存极少 比如 1000 件。
-
不能超卖 超卖导致重大售后问题。
-
业务链路必须极短 扣库存必须快(毫秒级)。
-
数据库承压不能太大 秒杀流量不能把主站压垮。
所以目标是:
- 扣减要 原子性
- 避免数据库被打爆
- 不允许超卖
- 能回溯成功/失败
- 撤回和放库存也要安全
update product set stock = stock - 1 where id = 1 and stock > 0;
优点:
- 简单
- 强一致性
缺点:
- QPS 极低(几百)
- 秒杀瞬间直接把库打挂
- 不适合真实秒杀场景
→ 不能用于真正秒杀,只能用于普通下单。
秒杀场景一般采用 “削峰 + 前置判断 + 内存扣减 + 异步确认” 的架构。
下面给最佳方案。
Redis 的特性:
- 单线程 → 天然原子性
- 内存 → 高吞吐
- 支持 Lua → 业务逻辑原子执行
local stock = tonumber(redis.call("GET", KEYS[1]))
if stock <= 0 then
return -1 -- 库存不足
end
redis.call("DECR", KEYS[1])
return 1 -- 扣减成功Go/Java 直接执行 Lua,每秒可承受 10~20 万 QPS。
- 扣减是原子的,不会超卖
- Redis 是内存级别,性能爆炸强
- 所有请求不打数据库
- Redis 异常会影响库存一致性,需要持久化机制(后面讲)
流程图:
用户请求 → 接入层限流
→ Redis 预扣库存(Lua 原子扣减)
→ 写入秒杀订单消息队列
→ 消费 MQ,在数据库最终落地订单、扣库存
如果 Redis 扣库存成功,用户不会立即下订单,而是将消息写入 Kafka / RocketMQ:
- 订单写数据库是慢操作
- 秒杀并发巨大
- 直接写数据库必挂
- MQ 能承受百万级写入做削峰
最终由订单服务异步消费消息:
- 检查用户资格
- 建订单
- 持久化最终库存(这里使用 MySQL 扣减)
- 削峰
- 数据库只处理异步订单,压力小
- 确保订单按序落地,可回查可补偿
当 Redis 扣库存成功,但数据库订单没写成功怎么办?
解决方案:
扣库存时记录:
stock_reserved = +1
订单失败时,补偿库存:
stock += 1
MQ 有死信队列,订单失败会触发“归还库存消息”。
每天使用定时任务扫描数据库与 Redis 差异,修复异常。
根据商品库存量,比如 1000 件,整个系统最多放行 10 倍请求(1 万),剩下都在网关拒绝。
不让所有请求打到库存服务。
服务收到库存为 0,即刻标记 sold_out = true,直接拒绝后续请求
秒杀场景下库存扣减不能直接打数据库,需要使用 “Redis + Lua 扣库存 + MQ 下单 + MySQL 最终落库” 的异步架构。
具体流程: 1)流量通过网关限流、削峰 2)Redis Lua 原子扣减库存,保证不超卖 3)扣减成功的请求写入消息队列 4)订单服务异步消费 MQ,写 MySQL,完成真正的扣库存 5)通过补偿消息、定时任务确保 Redis 和数据库一致性
这样可以轻松支撑几十万到上百万 QPS,同时保证库存严格正确。