如何设计高并发下的库存扣减防超卖

域名注册
域名注册 正式会员超兽战士 👑年卡会员
发布于 2026-10-07 18:37 ·1 浏览 ·0 回复

防超卖的核心只有一句话:把"判断库存是否足够"和"扣减库存"合并成一次原子操作,绝不允许先 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 扛住峰值,唯一索引保证不重复扣减,回补逻辑保证不少卖,离线对账兜住一切意外。五件事各管一段,缺哪一段,问题就会在对应的流量量级上冒出来。

版权声明:本文来自 GJ站长论坛《如何设计高并发下的库存扣减防超卖》
原文链接:https://www.gj0.com/thread-473.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。

全部回复 0

还没有回复,来抢沙发~