如何设计高并发下的库存扣减防超卖
防超卖的核心只有一句话:把"判断库存是否足够"和"扣减库存"合并成一次原子操作,绝不允许先 SELECT 再 UPDATE 两步走。工程上成熟的方案是三层——Redis + Lua 预扣扛峰值流量、数据库条件更新做最终账本、订单号唯一索引保证幂等,单靠任何一层都会在某个量级上翻车。
为什么"先查库存再扣减"一定会超卖?
结论:因为查和扣之间存在时间窗口,并发请求会读到同一个旧库存值。
假设库存剩 1 件,两个线程同时 SELECT 到 stock=1,都判断"够扣",随后各自 UPDATE 成 0,结果卖出 2 件。这个窗口在单机多线程下是微秒级,在分布式多节点下是毫秒级,1000 QPS 时必然触发。
另一个常见误区是"应用层加锁":synchronized、本地 ReentrantLock 只在单实例内有效,服务一旦部署 2 个以上实例就直接失效。防超卖必须在数据存储层解决。
库存扣减怎么做才能原子?
结论:数据库条件 UPDATE、悲观锁、Redis Lua 脚本三种方式,按流量规模从低到高选择。
最简单的是条件更新(CAS,Compare And Swap,即"值还是旧值才更新"):
UPDATE sku_stock SET stock = stock - 1
WHERE sku_id = 1001 AND stock >= 1;
判断 affected_rows == 1 才算扣减成功,返回 0 就是库存不足。这条 SQL 把判断和扣减压成一行,靠 InnoDB 行锁保证原子性,不会超卖。瓶颈在热点行——同一个 sku_id 的更新被串行化,单行热点写落在 1000~3000 TPS 量级。
第二种 SELECT ... FOR UPDATE 悲观锁,本质和第一种一样,但把行锁显式拿到整个事务里,持锁时间更长,只适合扣减逻辑极短的场景。
第三种是 Redis + Lua。Redis 单线程执行脚本,脚本内的多条命令不会被其他请求打断:
-- KEYS[1]=stock:sku:1001 ARGV[1]=扣减数量
local s = redis.call('GET', KEYS[1])
if not s then return -1 end -- 库存未预热
if tonumber(s) < tonumber(ARGV[1]) then return 0 end -- 不足
return redis.call('DECRBY', KEYS[1], ARGV[1])
返回 -1 表示 key 不存在,0 表示库存不足,正数是扣减后的余量。单机 Redis 执行这个脚本能到 10 万 QPS 量级,是秒杀流量的主力通道。
Redis 扣完为什么还要落库?一致性怎么保证?
结论:Redis 是"准入闸门",数据库才是"最终账本",两者通过消息队列(MQ)异步对齐。
流程:Lua 预扣成功 → 投递扣减消息到 MQ → 消费者执行条件 UPDATE → 落库成功才创建订单。这里有两个绕不开的点:
一是幂等(同一个请求执行多次,结果和执行一次相同)。MQ 会重复投递,必须建唯一索引:
CREATE TABLE stock_deduct_log (
order_no VARCHAR(64) PRIMARY KEY,
sku_id BIGINT NOT NULL,
num INT NOT NULL
);
插入主键冲突就说明这条消息处理过,直接 ACK 跳过。没有这张幂等表的异步扣减,重试一次就多扣一次。
二是少卖。Lua 扣成功但落库失败(DB 超时、DB 库存真的不足),必须把预扣量回补(INCRBY)并关单。漏掉回补,Redis 与 DB 的差额会永久累积,现象是"Redis 显示有货,下单全失败"。
热点 SKU 扛不住怎么办?
结论:把单个 SKU 的库存拆成 N 份,落在不同 Redis key 和不同数据库行上,把串行争抢变成并行。
做法:sku_id=1001 库存 1000 件,拆成 stock:sku:1001:0 到 stock:sku:1001:9 共 10 个分片,每片 100 件。请求按 user_id % 10 或随机选分片扣,某片扣空就换下一片。Redis 侧不再有单 key 热点,DB 侧变成 10 行并行更新,吞吐提升接近 10 倍。代价是库存碎片化,需要后台任务在余量低于阈值时做分片合并。
最后一道防线:离线对账
结论:再严谨的在线链路也要配离线对账,因为线上总有未预料的失败路径。
每天跑一次校对任务:以 stock_deduct_log 的 SUM(num) 为准,与数据库 sku_stock 的变化量、Redis 余量三方比对,差额超过设定阈值就告警并人工介入。这一步不提升性能,但能在事故变成资损之前拦下来。
整条链路可以这样收束:条件 UPDATE 保证不超卖,Redis Lua 扛住峰值,唯一索引保证不重复扣减,回补逻辑保证不少卖,离线对账兜住一切意外。五件事各管一段,缺哪一段,问题就会在对应的流量量级上冒出来。
原文链接:https://www.gj0.com/thread-473.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。