因为专注所以专业
助力成长与创新,汇集前沿程序开发观点

库存扣减先查再改老是超卖,先加锁还是先把校验和写入并成一条语句?

2026年9月18日 阅读:77

库存扣减先查再改老是超卖,多数情况不是锁选错了,而是校验和写入没有放进同一个原子操作里。按 2026 年项目交付经验,判断顺序是:先分清超卖来自并发写、请求重放还是回补错位,再看同一行的写冲突强度;单库、中小流量的日常下单,用一条带条件的更新语句配合唯一约束通常就够用,只有同一行每秒被抢几百次以上,才值得考虑行锁、分段库存或缓存预扣。锁是实现手段,不是结论。

超卖从哪里来:三类根因别混在一起

很多人一看到库存出现负数就去加锁,其实超卖常见来自三类根因,处理方式并不相同,锁只对其中一类有明显作用。先分清哪一类,能省掉大量白做的工作。

第一类是「先查后改」:查询时库存还有 10,写入时已经被别的请求改成 0,两个动作之间的空隙被插队。第二类是请求重放:支付回调或消息被重复消费,同一笔扣减执行了两次,典型表现是扣减次数与订单数对不上。第三类是回补错位:超时未支付释放库存时,把已经被其他订单占用的数量又加了回去,表现是库存忽多忽少且难复现。

更省事的判断是先看两件事:能不能用一条带条件的 UPDATE 完成扣减;重复请求能不能在入口拦掉。如果库存流水里同一个订单号出现两条扣减记录,那大概率不是锁的问题,而是幂等没做好。

  • 先查后改型:并发请求在查询与写入的间隙插入,表现为库存出现负数,订单量却基本正常。
  • 重放型:同一请求号被处理多次,表现为扣减笔数与订单数对不上。
  • 回补型:超时释放与人工调整同时发生,表现为库存忽多忽少,且难以复现。

条件更新和行锁,差在谁先排队

乐观思路是不提前占坑,写入时再确认数据没被别人改过,常见做法是 UPDATE 带上库存大于等于待扣数量的条件,或用版本号做条件;悲观思路是先把这一行锁住,事务结束后别人才读写,常见做法是 SELECT 配合 FOR UPDATE。差别不只在性能,更在冲突概率:冲突低时条件更新省掉排队开销,冲突高时行锁能减少大量无效重试和回滚。

2026 年比较常见的交付习惯是,普通下单先用数据库条件更新兜底,等压测确认单行冲突明显,再叠加缓存预扣或分段库存,而不是一上来就引入分布式锁。下面这组对比里的工作量与并发区间来自项目上的常见区间,只能当量级参考,具体仍要以自己的压测结果为准。

  • 数据库条件更新:适合读多写少、冲突概率低;改造量常见区间在 1~3 人天;失败重试常见区间 2~3 次;日常下单的默认选择。
  • 数据库行锁:适合同一行高并发写且事务足够短;锁等待上限常见区间 1~3 秒,超过就快速失败而不是继续排队。
  • 缓存原子扣减:适合秒杀类极热点,先扣缓存再异步落库;代价是短暂不一致,需要补偿链路,改造常见区间 5~15 人天。
  • 队列串行扣减:适合同商品可接受最终一致的场景,按商品分区顺序处理;代价是链路变长、排障变难。

库存扣减四步核对,顺序别颠倒

与其争论用哪种锁,不如按固定顺序核对四件事。这个顺序的价值在于:前一步没确认,后一步的锁方案很可能白做,甚至把问题从超卖变成锁等待。

  1. 原子性:把校验和写入合成一条带条件的写操作,影响行数为 0 即视为扣减失败,不依赖先查后改。
  2. 幂等:每笔扣减带唯一请求号或订单号,数据库加唯一约束,重复到达时按已处理直接返回。
  3. 回补路径:扣减成功但下单失败、以及超时未支付释放,都要有明确触发条件、状态校验和审计记录。
  4. 可观测:库存流水要能按商品和时间还原,出现负数时能落到具体请求,而不是只看到一个错误结果。

四步都成立之后再评估要不要加锁。判断做得好不好的口径也很直接:任取一笔异常库存,能通过流水还原完整的扣减与释放序列,并能统计出超卖笔数,而不是只凭人工核对。

极热点商品:分段与预扣的取舍

单个商品每秒被抢几千次时,无论条件更新还是行锁,压力都会集中在一行上,锁等待会迅速堆积。常见做法是拆分:把可售库存拆成若干段,请求按哈希或随机落到不同段扣减,失败时按固定顺序跨段处理;或者用缓存预扣加异步落库,把写压力从数据库挪走。代价是库存口径变复杂,对账、回补和人工调整都要遵循同一套分段规则,否则差异会越滚越大。

交付现场比较常见的一种情形是:排期只留出一层缓存预扣的工期,压测数据只有单线程脚本,历史流水也没有归档。我们当时的做法是先补一段带并发和重复请求的压测,再把同商品请求号唯一约束、库存流水表、对账脚本三样一起交付,缓存预扣放到第二期。结果是上线首月没有出现口径对不上的差异,代价是首版多花了大约 3~5 人天做流水与对账,比直接上分段方案省,但确实比只改一条 SQL 慢。这类改造按常见区间估,日常下单场景大约 1~3 人天,引入分段或预扣通常在 5~15 人天,还取决于对账要求有多细。

  • 分段库存:适合单品极热点、可接受短暂分布不均,需要较强的对账能力支撑。
  • 缓存预扣:适合秒杀入口削峰,需要配异步落库与失败补偿。
  • 队列串行:适合同一商品需要强顺序扣减,读延迟通常会上升。
  • 数据库条件更新:适合日常下单,改造成本低,是不少系统的默认选择。

什么情况适用,什么情况不适用

适合先上条件更新加幂等约束的情况:实例数量少、单商品并发在几十到几百、允许失败后有限重试、有对账手段。适合考虑行锁、分段库存或缓存预扣的情况:压测中已经出现明显锁等待或版本冲突重试,同一行的写冲突成为瓶颈。需要谨慎的情况:内部盘点、后台批量导入这类低并发场景,加锁通常只增加故障点;涉及财务口径的库存数据,不宜只靠缓存预扣,仍要保留以数据库为准的核对链路。

边界句可以直接摘出来用:锁的选择取决于同一行的并发写冲突强度,而不取决于系统规模;没有幂等和对账,任何锁都挡不住重放带来的库存差异。这条边界同样适用于 2026 年之后新增的库存模块,换了框架也不改变。

几个容易踩的坑

一类坑是事务范围过大,把扣减之外的操作也包进事务,连接池很快被占满。一类坑是重试不设上限,版本冲突后一直循环,把数据库 CPU 拉高。还有一类坑是释放库存时不校验状态,已支付订单占用的数量又被回补,库存虚增后只能到盘库时才发现。

  • 事务里包含远程调用或消息发送:锁等待时间容易被放大。
  • 重试次数不设上限:冲突时形成自激,表现为接口整体变慢。
  • 释放库存不做状态校验:已支付订单也被回补,库存虚增。
  • 只做单线程测试:压测要带并发和重复请求,否则超卖在测试环境看不出来。

常见问题

库存扣减一定要上分布式锁吗?

不一定。单库能保证单行原子更新时,一条带条件的更新加唯一约束通常就够;分布式锁更适合跨资源协调,改造成本和排障难度都更高。

条件更新失败后重试几次算正常?

常见区间是 2~3 次。超过这个次数仍失败,通常说明同一行的写冲突已经集中,此时该考虑入口限流、分段库存或缓存预扣,而不是继续加重试。

悲观锁会不会把数据库拖慢?

事务足够短、锁等待有上限时影响可控,常见做法是把等待上限设在 1~3 秒;一旦锁范围里包含远程调用,等待会被放大,连接池压力明显上升。

缓存预扣和数据库对不上,怎么收尾?

以数据库为准做对账,把差异落进库存流水表,再按固定脚本补正。不建议人工直接改库存数字收尾,否则同一类差异会反复出现,也失去可回溯的凭据。

秒杀场景只靠数据库条件更新行不行?

单品每秒几百次以内,经验上多数能扛住;再高就容易在一行上堆积等待,需要入口限流、分段库存或缓存预扣配合,单靠数据库通常不是好选择。


手上如果有具体的库存扣减问题,可以先按四步核对法确认原子性、幂等、回补和可观测,再决定是否引入锁、分段或预扣;多数日常订单不必上复杂方案,而极热点场景要提前准备对账与补偿。以上判断基于项目交付经验与常见验收口径,具体阈值仍要按自己的压测结果和数据库文档核对。

有类似的项目需求?
联系我们,获取一对一项目参考方案
获取方案
准备好开始了吗,
那就与我们取得联系吧!
13370032918
了解更多服务,随时联系我们
请填写您的需求
您希望我们为您提供什么服务呢
您的预算

微信二维码
扫码添加客服微信
专业对接各类技术问题
联系电话
13370032918 (金经理)
电话若占线或未接到、就加下微信
联系邮箱
349077570@qq.com
提交成功
感谢您的信任,我们会尽快与您联系!
为您推荐以下案例