秒杀场景的核心矛盾是:极短时间内大量请求抢同一份有限库存,稍有不慎就会超卖——卖出的数量超过真实库存,后续要么砍单要么赔付。单纯靠数据库"查一下再减"在并发下必然出错,单纯靠 Redis 又会面临缓存与数据库不一致。可靠的方案是分层的:Redis 做高速预扣挡住大部分流量,消息队列做异步削峰,数据库做最终一致的落库。本文拆解这套链路和每一层的关键细节。
最常见的错误写法是先 SELECT 出库存、在应用层判断够不够、再 UPDATE 扣减。两个并发请求可能同时查到库存为 1,都判断"够",于是各扣一次,超卖就此发生。问题的根源是"判断"和"扣减"不是一个原子操作。
正确的数据库扣减必须把条件写进同一条语句,让数据库在行锁内完成判断:
UPDATE sku_stock
SET available = available - :qty
WHERE sku_id = :skuId AND available >= :qty;影响行数为 1 才算扣减成功,为 0 说明库存不足。这是防超卖的最后一道底线,但数据库直接扛秒杀的全部并发并不现实,所以需要在它前面加缓存层。
活动开始前把可售库存预热到 Redis,下单时先在 Redis 原子扣减,扣不到就直接返回"已售罄",根本不打到数据库。原子性靠 Lua 脚本保证——把"判断余量 + 扣减 + 记录"放进一个脚本里,Redis 单线程执行,天然互斥:
-- KEYS[1]=库存key KEYS[2]=已购用户集合 ARGV[1]=用户 ARGV[2]=数量
local stock = tonumber(redis.call('GET', KEYS[1]))
local buyQty = tonumber(ARGV[2])
if stock == nil then return -1 end -- 库存未初始化
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then return -2 end -- 限购
if stock < buyQty then return 0 end -- 库存不足
redis.call('DECRBY', KEYS[1], buyQty)
redis.call('SADD', KEYS[2], ARGV[1])
return 1 -- 预扣成功返回值区分了"未初始化、超出限购、库存不足、成功"几种结果,前端可以给出准确提示。限购判断必须和扣减放在同一个脚本里,否则一个用户并发发起多个请求仍可能突破限购。
Redis 预扣成功后,并不立即写订单库,而是发一条消息到队列,由后端消费者按数据库能承受的速率平稳落库:
async function onSeckill(req) {
const code = await redis.eval(DEDUCT_SCRIPT, keys, [req.userId, 1]);
if (code !== 1) return failHint(code);
await mq.send("order.create", { userId: req.userId, skuId: req.skuId });
return { accepted: true, tip: "排队中,结果稍后通知" };
}这样秒杀入口的响应只取决于 Redis 和 MQ,数据库不会被瞬时洪峰打穿。用户侧用"排队/处理中"状态承接,落库成功后再通过轮询或推送告知最终结果。
引入缓存预扣和异步落库后,最大的问题变成"Redis 说扣了,数据库订单却没建成",这时库存就对不上。需要处理好三个一致性环节:
1. 落库失败要回补 Redis。 消费者创建订单时若因校验失败(地址异常、风控拦截等)没建成,必须把预扣的库存加回去,并移除已购标记:
-- 回补脚本,与扣减对应
redis.call('INCRBY', KEYS[1], ARGV[2])
redis.call('SREM', KEYS[2], ARGV[1])
return 12. 订单状态机要幂等。 MQ 可能重复投递,创建订单要以业务唯一键(用户+商品+活动批次)做幂等,避免同一预扣生成两笔订单。
3. 定期对账兜底。 定时任务比对"Redis 已扣量、已建订单量、数据库实际扣减量",差异超过阈值告警并触发校准。任何分布式方案都不能只靠运行时自觉,对账是最后安全网。
库存为 0 之后仍会有大量请求涌来,不能让它们反复执行 Lua 甚至回源。常见做法:
一套完整的秒杀链路应当是"网关限流 → Redis Lua 原子预扣与限购 → MQ 异步削峰 → 消费者幂等落库 → 失败回补 → 定时对账",层层过滤、层层兜底。Redis 库存务必在活动前预热并设置合理的过期与兜底值,防止缓存被穿透;消费者并发数要按数据库承载能力反推,宁可排队也不要压垮落库。压测不能只测"全部成功"的理想路径,要专门构造库存临界并发、重复投递、消费者宕机、缓存与库不一致这几类故障,确认不会超卖、不会少卖、最终能自动对齐。技术选型上,Lua 脚本、可靠消息、本地售罄标记都是通用手段,重点是把状态流转和补偿路径设计闭环。
available>=qty 条件的单条原子语句;秒杀防超卖的本质,是用"原子操作"消灭并发判断的时间差,用"分层缓存加异步队列"把洪峰削平,再用"回补加对账"保证缓存与数据库最终对齐。没有哪一层能单独解决全部问题,真正可靠的系统是每一层都假设上一层可能出错、并为它准备好兜底。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。