ARTICLE / 3 MIN READ

秒杀系统如何防止超卖与重复下单

从预热、限流、库存扣减到异步下单,拆解高峰抢购链路的关键状态和一致性边界。

秒杀的核心矛盾是请求峰值远大于数据库能够承受的写入速度。设计目标不是让所有请求都同步成功,而是让有限库存被准确分配,失败请求快速得到明确结果。

先在入口挡住无效流量

活动开始前把商品信息和库存摘要预热到缓存。网关按活动、用户和 IP 限流,未登录、活动未开始和重复点击的请求在最前面返回。

不要把“校验库存、创建订单、扣减库存”全部放进一个长事务里。长事务会持有连接和锁,最终把数据库拖垮。

两种扣库存路径

数据库原子扣减适合库存量不大、并发可控的活动:

UPDATE sku
SET stock = stock - 1
WHERE sku_id = ? AND stock > 0;

根据 affected rows 判断是否抢到库存。Redis 预扣适合把峰值挡在数据库之前,但必须记录预扣流水,并由异步消费者完成最终订单和失败回补。

排队而不是无限等待

抢购接口可以返回排队编号,消费者按固定速率创建订单。用户通过轮询、SSE 或 WebSocket 查询状态。排队状态要有过期时间,未支付订单要能释放库存。

一个简化状态机如下:

待排队 -> 已预扣 -> 订单创建中 -> 待支付 -> 已支付
                  |             |
                  v             v
                失败回补      超时关闭并回补

所有状态转换必须有合法的前置状态和幂等条件。例如已支付订单不能被超时任务再次关闭。

防刷与公平性

设备指纹、账号限购、验证码和行为风控可以减少脚本流量,但不能把风控逻辑全部放在前端。后端应限制单用户的成功次数,并对同一设备的异常频率做动态封禁。

验收数据

上线前至少准备库存对账表:初始库存、预扣数量、成功订单、取消订单、回补数量和最终库存。压测要验证库存为零时的错误率、重复请求、消费者重启、Redis 故障和数据库主从切换。

秒杀做得好不是页面按钮转得快,而是库存、订单和支付状态在高峰后仍然能够对上账。

GITHUB DISCUSSION

评论与回复

评论保存在 GitHub Discussions,登录 GitHub 后即可参与,发布和回复都在本页完成。

评论区进入视口后自动加载。