为什么是 Redis
库存扣减的执行位置决定系统上限:数据库行锁让 10 万个请求排队,实际有效吞吐只有几百;Redis 单线程内存操作,简单命令十万级 QPS——同样的扣减语义,换一个执行位置,吞吐差三个数量级。库存作为预扣资格放进 Redis,数据库只在最后承接真实订单量(几百笔),压力断崖式下降。
Lua:把判断和扣减焊死在一步
直觉写法是先 GET 判断、再 DECR 扣减——两步之间就是并发窗口:一万个请求同时 GET 到"剩 1 个",然后一万个 DECR,库存变成负数,超卖。Lua 脚本在 Redis 里原子执行:整个脚本跑完才会执行下一条命令,判断与扣减之间不存在并发窗口,超卖从机制上被消灭。
-- seckill_deduct.lua
local stock = redis.call("GET", KEYS[1])
if not stock then
return -1 -- 库存 key 不存在:未预热,拒绝
end
if tonumber(stock) < tonumber(ARGV[1]) then
return 0 -- 库存不足:售罄
end
return redis.call("DECRBY", KEYS[1], ARGV[1]) -- 扣减并返回剩余// 服务端调用:三种返回值对应三种处理
Long r = redis.eval(DEDUCT_LUA, "stock:sku:1001", "1");
if (r == null || r == -1) return fail(ServiceState.NOT_READY); // 未就绪
if (r == 0) return fail(Result.SOLD_OUT); // 售罄
mq.send(new OrderMsg(orderId, userId, skuId)); // 扣成功才发消息
return queuing(orderId); // 返回"排队中"预热:开抢前库存必须就位
三个预热动作缺一不可:库存装载——活动创建时把库存数 SET 进 Redis,key 带 TTL 覆盖活动全周期;资格数据——限购名单、风控黑名单提前就位;连接与脚本——Lua 脚本用 SCRIPT LOAD 预加载(EVALSHA 比 EVAL 少传脚本体),连接池预热到目标并发。开抢前一分钟做一次自检脚本:库存 key 存在且值正确、Lua 可执行、MQ 通路健康——预热不是加载完就完了,要验证。
一致性:Redis 扣了,订单没成
预扣成功只代表"资格到手",后面消息可能丢、落库可能失败——Redis 与数据库是两个存储,一致性要靠闭环设计兜底:消息可靠投递(生产确认 + 持久化 + 手动 ack);落库失败回补——消费端写库失败,发回补消息把 Redis 库存加回去;对账兜底——活动后 Redis 剩余 + 已成订单 + 回补记录 = 总库存,对不上的进人工处理。一致性不是靠某个机制一劳永逸,是靠"每个失败路径都有出口"。
Redis 挂了怎么办
Redis 成为关键路径后,它的高可用就是活动的高可用:哨兵或集群部署是底线;预扣失败(连接不上)时的降级不是退回数据库硬扛——那等于回到事故现场——而是直接返回排队页并告警,宁可活动暂停一分钟,也不冒超卖的风险;恢复后先校验库存值(有对账记录才有底气恢复)。下一篇解决预扣的下一个瓶颈:单个热 Key 扛不住一万个人怎么办。
评论 (0)