一个做食品的客户,活动当晚卖了 800 单,第二天发现其中 60 单没货。技术查了半天,结论是“并发扣减没加锁”。但真正的问题在更前面:他们把“仓库里有 1000 件”直接当成了“线上可卖 1000 件”,没有给已下单未发货、线下门店、预售留货留出位置。
库存不是一张表的一个数字,而是一组规则。规则没定清楚,加多少锁都会超卖。
先拆 SKU,再谈库存
SKU 的拆分标准是“用户是否需要单独选择”。颜色、规格、包装单位,只要会影响购买决策,就应该拆成独立 SKU;只在内部区分批次、供应商的,不要拆到前台,否则商品列表会变得无法维护。
拆完 SKU 后,每个 SKU 至少要有四个字段:可售库存、锁定库存、已售未发、安全库存。只用一个“库存”字段,后面所有场景都会打架。
三个数之间的关系
| 字段 | 含义 | 什么时候变 |
|---|---|---|
| 可售库存 | 用户现在能下单的数量 | 入库、锁定、释放、发货 |
| 锁定库存 | 已下单未支付/未发货占用的数量 | 下单锁定、支付确认、取消释放 |
| 已售未发 | 已支付但仓库还没出的数量 | 支付成功、发货完成 |
前台只展示可售库存。可售 = 实物库存 − 锁定 − 已售未发 − 安全库存。这个公式要写进系统,不要让运营手工改数字。
下单锁库的时序
推荐顺序:创建订单 → 校验可售 → 锁定库存 → 发起支付 → 支付成功转为已售未发 → 发货扣减实物库存。每一步都要有超时释放,未支付订单 15 到 30 分钟自动取消并释放锁定。
不要在支付成功后才锁库。那时用户已经付了钱,没货只能退款,体验最差。也不要在加入购物车时锁库,购物车不是承诺,会把库存占死。
四种超卖场景
- 并发下单:两个请求同时读到 1 件。用数据库行锁或原子扣减,不要“先查再改”。
- 多端同步:小程序、门店、平台共用库存,某端没同步。设一个库存中心,所有端走同一接口。
- 退款回滚:退款成功但库存没加回来,或者加回到错误的 SKU。退款单要记录原 SKU 和数量。
- 预售与现货混卖:预售占用了现货库存。预售单独建虚拟仓,不要和现货共用可售数。
退款与回滚的顺序
仅退款:确认未发货,库存从“已售未发”退回“可售”。退货退款:先收货质检,合格后回库,不合格走残次品仓,不能直接加回可售。部分退款:按明细回滚,不要按订单总额比例估算。
每一步都要留流水:谁、什么时候、从哪个仓、回滚多少。没有流水,月底对账一定对不上。
对账怎么做
每天跑一次:实物库存 = 可售 + 锁定 + 已售未发 + 残次 + 在途。差异超过阈值就告警。刚开始会有很多小差异,逐条查,通常能在两周内把规则跑顺。
不要把对账做成月底一次。月底发现差异,已经找不到是哪一单造成的了。
预售、组合装与赠品
预售商品单独建虚拟库存,不要占用现货。组合装要拆成子 SKU 扣减,任何一个子件不足,整单不可售。赠品也要占库存,否则活动结束才发现赠品发完了。
组合装的库存计算有两种方式:固定搭配按最小可组合数算,自由搭配按子件分别校验。不要用一个总数掩盖子件差异。
库存精度与盘点
线上库存不可能和仓库永远一致。设定一个可接受的差异率,比如 2%,超过就停售盘点。每天抽盘十个高频 SKU,比月底大盘更容易发现问题。
盘点期间要把可售库存临时置零,避免边盘边卖。盘点结束后按实际数调整,并记录原因:破损、错发、退货未入库。
库存准确性首先是运营问题,不是技术问题。先把入库、出库、退换货流程固定,再谈系统和锁。
相关阅读:CMS 数据库优化。
常见问题
库存放在数据库还是缓存?
以数据库为准,缓存只做展示。扣减必须落库,缓存可以异步刷新。用缓存做扣减,迟早会出现数据不一致。
秒杀要不要单独一套库存?
要。活动库存独立锁定,活动结束再回补。和日常库存混在一起,平时销售会被活动拖垮。
多门店怎么共享库存?
设总仓和门店仓,线上优先总仓,门店自提走门店仓。跨仓调拨要有在途状态,不要直接改两个仓的数字。
库存和支付、订单状态强相关,接入支付时建议一起看支付接入流程,把回调与对账一起设计。