秒杀系统如何防超卖与重复下单
秒杀系统如何防超卖与重复下单
秒杀的核心难点是短时间内大量请求争抢有限库存。把“查询库存、检查用户是否买过、扣库存、创建订单”拆成多个普通请求,在并发下会发生竞态:多个请求都读到库存大于零,然后都成功扣减。
1. 先明确必须保证什么
一个基本版本至少要满足:
- 库存不能被扣成负数。
- 同一用户不能重复购买同一活动商品(若业务规则如此)。
- 订单失败或服务重启时,库存与订单最终能恢复到一致状态。
- 重试同一请求不能重复创建订单。
先写下这些不变量,才能判断 Redis、数据库和消息队列分别负责什么。
2. 数据库原子条件更新
数据库可用条件更新避免库存减为负数:
1 | |
检查受影响行数:更新 1 行表示预扣成功,0 行表示没有库存或记录不匹配。若还要一人一单,应在订单表建立符合业务键的唯一约束,并在冲突时按重复请求处理。
3. Redis Lua 预检和原子扣减
为了减少数据库承受的无效请求,可以把库存与用户购买标记放在 Redis 中,并用一个 Lua 脚本完成“检查库存、检查用户、扣减库存、记录用户”的原子判断。多个 Redis 命令放在同一个脚本中执行,可以避免脚本内部步骤被其他客户端插入。
但 Redis 预扣不是数据库订单的最终提交。应用仍要处理 Redis 扣减成功后数据库写入失败的情况,例如补偿库存、记录失败任务或设计可重放流程。不能把“脚本返回成功”当作订单已经持久化。
4. 异步下单与消息处理
将订单创建放入队列能把高峰请求和数据库写入解耦,但也增加了消息可靠性、重复消费和积压处理的问题。一个可恢复的流程通常包含:
1 | |
“先响应、后落库”只有在请求已可靠记录、客户端有查询订单状态的办法、失败有补偿机制时才站得住。仅把消息放进进程内 BlockingQueue,进程崩溃时消息可能丢失。
5. Redis Stream 和确认
Redis Stream 消费者组可跟踪已投递但尚未确认的消息。消费者处理完成后应 XACK;消费者崩溃后,未确认消息可以通过 pending 信息检查并重新认领。重试可能让同一事件被再次处理,因此消费者仍要幂等。还要设置保留上限、监控积压,并设计无法处理消息的隔离方式。
Pub/Sub 更适合在线广播,不保留离线消息;不能把它当作可靠订单队列。选择队列时要根据持久性、投递语义、运维复杂度和业务规模决定。
6. 幂等与补偿
为每次下单生成稳定的业务请求 ID,数据库用唯一约束防止重复订单。消费者重复收到同一消息时,能查询已处理结果并安全返回。补偿操作也应幂等,否则重试补库存可能把库存加多。
7. 小结
可靠秒杀不是“Redis 抢锁”单点解决的问题。Redis 可做快速预检和排队,数据库条件更新与唯一约束维护持久化不变量,消息系统负责异步解耦,幂等和补偿处理失败与重试。每个边界都要明确谁是最终事实来源。
参考:Redis Streams 文档。





