库存系统出错,直接就是货损。系统显示有库存实际没有,卖出去发不出货,要么赔用户要么发空包,这是超卖。反过来,实际有货系统显示没有,明明能卖的卖不了,这是少卖,损失的是销售。库存的账实不符不像支付资损那样有明确金额,它更隐蔽,超卖变成客诉和赔偿,少卖变成库存积压和周转下降,两边都在悄悄漏钱。
库存一致性治理之所以难,是因为库存数据分散在多个地方。Redis 里一份可售库存用来扛流量,DB 里一份主数据,仓库里还有真实的物理实物。任何两份对不上都是不一致。治理的目标是让这三者在任何时刻都保持账实一致,这要求从扣减的原子性到多级同步再到物理盘点,全链路每个环节都不能漏。
防超卖的底线,扣减必须原子
图:防超卖对比——先查后改两步操作必然超卖,带条件 UPDATE / Lua 脚本原子扣减是唯一底线
超卖的根因几乎千篇一律,把检查库存和扣减库存拆成了两步。先查一下库存够不够,够再扣。在并发下,请求 A 查到库存为一,请求 B 同时也查到为一,两个都以为够,都执行扣减,库存变成负一,超卖发生了。
防超卖只有一条不可妥协的底线,扣减必须原子。MySQL 里靠带条件的 UPDATE 把检查和扣减合并到一条 SQL 内完成。
UPDATE stock SET stock = stock - 1 WHERE sku_id = ? AND stock >= 1数据库的行锁保证两个并发请求里只有一个能 affected 为 1,另一个因为库存不足 affected 为 0。检查和扣减之间没有间隙,不存在被插队的窗口。如果库存落在 Redis 里,就用 Lua 脚本,把判断和扣减放在同一个脚本里原子执行。任何先查后改的实现,无论中间隔了多短的代码,在高并发下都必然超卖。这条线没有妥协余地。
库存扣减必须原子,MySQL 用带条件 UPDATE,Redis 用 Lua 脚本。先查后改的两步操作在并发下必然超卖,A 查到一 B 也查到一,都扣,变负一。这是一切库存系统的地基,地基歪了上面盖什么都会塌。
三级库存与一个恒等式
图:三级库存与恒等式——每种操作必须同时更新三个数,保持可售=实物−锁定始终成立
库存分三级,Redis 里的可售库存负责扛流量做快速扣减,DB 里的库存主数据是账面真相,仓库里的物理实物是根本。三级之间要保持一致,靠的是一个恒等式,可售等于实物减锁定。
这个等式任何时刻必须成立。用户下单但还没付款,库存从可售里锁定一部分,可售减少锁定增加,实物不变。付款后锁定转为实物扣减,实物减少锁定归零,可售同步减少。超时未付,锁定释放回可售。退款则把已扣的实物回补,可售恢复。
每一种库存操作,下单锁定、支付扣实物、超时释放、退款回补,都必须同时正确更新这三个数,让等式始终成立。任何只动其中一两个数的实现,都会让等式失衡,账实就分叉了。
Redis 和 DB 的同步与对账
三级里最难维护的是 Redis 和 DB 之间的一致。Redis 扛流量意味着扣减发生在 Redis,但 DB 是账面真相,两边得同步。常见做法是 Redis 原子扣减成功后,通过 MQ 异步把扣减事件同步到 DB。
异步同步引入了一个取舍。它让扣减路径快,Redis 不用等 DB,但代价是 Redis 和 DB 之间存在短暂的不一致窗口,并且异步同步有失败概率。MQ 消息丢了、DB 更新失败,Redis 扣了 DB 没同步,账实就永久分叉了。
所以 Redis 加 DB 的方案必须配对账兜底。定时任务比对 Redis 和 DB 的库存差值,差异超阈值告警并修复。这个对账是命脉,因为异步同步总会失败那么几次,没有对账这些失败就会累积成持续的账实偏差。慢 SQL 治理里常用的手段,索引优化、反范式、批量处理,在对账任务上同样适用,对账跑得快才能跑得勤,跑得勤差异才不会堆积。
逆向流程,最容易被漏的回补
正向流程大家都很重视,下单扣减的逻辑反复打磨。真正的高发区是逆向流程,退款时库存的回补。
一次退款如果忘了回补库存,系统就少了一件可售商品,而实物还在仓库里。这种不一致是单向的少卖,库房有货系统却说没货,白白损失销售机会。更隐蔽的是,它不会触发任何报错,库存数字只是悄悄变小,直到盘点或对账才发现亏空。
退款时的库存处理要看订单处在什么阶段。已扣实物库存的,退款要回补库存。仅锁定还没扣的,退款要释放锁定。两种情况的处理不同,但都必须有完整的回补流水可查。逆向流程的严格程度要和正向一致,不能因为它是退款不是下单就放松。每一笔回补都要落流水,能追溯能审计。
盘点,账实核对的终极手段
前面这些都是系统层面的对账,核对的是系统内部各份数据的一致。但系统再自洽,也可能和物理实物对不上,仓库里的货被挪动、损耗、被盗,这些都不经过系统。物理盘点是核对系统库存和实物的唯一手段。
盘点要定期做。仓库实物清点一遍,和系统库存比对。盘盈,实物比系统多,要分析是入库漏登还是别的。盘亏,实物比系统少,更要追根因,丢损、被盗、系统 bug 都可能。盘亏超阈值必须查到底,严重的要追责。账实严重不符必有系统性问题,盘点数据本身就是系统健康度的一个信号。
预售和现货要分开管库存。预售的货还没到仓,如果混进现货库存一起算,就会把预售的量当现货卖出去,超卖。预售有独立的状态机,定金期、尾款期、发货期,尾款超时未付要释放预售库存,不能和现货混算。
监控关键信号
图:库存一致性四大监控信号——负库存告警是最强异常信号,出现必立即介入
最后用几个监控指标兜住异常。可售库存告警,热销品快售罄时提前预警补货。负库存告警,库存小于零是绝对异常,必有 bug,要立刻查。账实差异告警,系统对账发现 Redis 和 DB 或系统和实物不一致。扣减失败率,失败率上升说明扣减链路有问题。
其中负库存是最强的异常信号。一个设计正确的库存系统,靠带条件扣减,理论上不可能出现负数。一旦出现,说明要么原子扣减被破坏了,要么有旁路写入绕过了校验,两种都是要立即介入的问题。
库存一致性治理是一条全链路的工程,从原子扣减到三级联动到对账兜底到物理盘点。最关键的两条铁律,扣减必须原子,逆向不能漏回补。Redis 加 DB 的方案必配对账兜底,因为异步同步总会失败。一致性永远优先于性能,宁可卖慢一点也要卖对,卖错的代价比卖慢高得多。








