库存扣减的并发难题超卖·悲观锁·乐观锁·Redis 预扣减 4 种方案实战演示地址http://ruoyioffice.com | 源码1·GitHubruoyi-office | 源码2·GitCoderuoyi-office | 源码3·Giteeruoyi-office | 微信17156169080备注「RuoYi Office」库存扣减是几乎每个有库存的系统都绕不开的并发难题。它的本质只有一句话判断库存够不够和扣减库存这两步不是原子的并发下就会超卖。库存只剩 1 件两个请求同时来都查到还有 1 件于是都扣结果库存变成 -1超卖了。本文系统讲透 4 种主流解法——数据库悲观锁、条件 UPDATE乐观锁/CAS、Redis 预扣减、分布式锁每种都配可落地代码与适用场景并结合RuoYi Office 里 ERP 进销存用条件 UPDATE、WMS 仓储用SELECT FOR UPDATE 悲观锁的两套真实源码对照帮你想清楚什么并发量该用什么方案。▲ 全景图超卖根因查询与扣减非原子→ 4 种解法悲观锁 / 条件 UPDATE / Redis 预扣减 / 分布式锁→ 适用并发量与一致性强弱对照引言超卖是怎么发生的先把问题讲清楚。最朴素的库存扣减代码长这样——几乎所有超卖都源于它// ❌ 反例查询与扣减非原子并发必超卖ErpStockDOstockstockMapper.selectById(id);// 线程A、B 都查到 count 1if(stock.getCount()buyCount){// 都判断够stock.setCount(stock.getCount()-buyCount);// 都算出 0stockMapper.updateById(stock);// 都写 0实际卖出了 2 件}问题在于读—判断—写三步之间没有任何保护多个线程交错执行判断都基于同一个旧值。要解决它核心思路只有两类让这三步串行化加锁悲观同一时刻只让一个线程进入临界区。让判断扣减变成一个原子操作CAS乐观在写的瞬间再校验一次条件。下面 4 种方案本质都是这两类思路的不同实现与权衡。概念定义超卖Oversell指实际扣减的库存超过了真实可用库存导致库存为负或卖出不存在的货。它是典型的竞态条件Race Condition问题。一、方案一数据库悲观锁SELECT … FOR UPDATE1.1 原理悲观锁的思路是先占坑再干活扣减前先用SELECT ... FOR UPDATE把库存行锁住事务提交前别的事务改不了这行从而把读—判断—写串行化。Transactional(rollbackForException.class)publicvoiddeduct(Longid,BigDecimalbuyCount){// 1. 加行锁其它事务在本事务提交前无法修改/锁定该行StockDOstockstockMapper.selectByIdForUpdate(id);// SELECT ... FOR UPDATE// 2. 锁内判断 扣减此刻独占安全if(stock.getQuantity().compareTo(buyCount)0){throwexception(STOCK_NOT_ENOUGH);}stockMapper.updateQuantity(id,stock.getQuantity().subtract(buyCount));}对应的 MapperselectidselectByIdForUpdateresultTypeStockDOSELECT * FROM stock WHERE id #{id} FOR UPDATE/select1.2 RuoYi Office 的真实落地WMS 仓储用悲观锁RuoYi Office 的 WMS 仓储模块就是悲观锁路线。因为一张出库/移库单常涉及多个 SKU它先把本次涉及的所有库存行批量SELECT ... FOR UPDATE锁住再在内存里整体计算校验、批量更新——保证整单一致privateMapItem,TuplechangeInventoryList(ListItemitems){// 1. 批量加锁把本次涉及的库存行全部 SELECT ... FOR UPDATE 锁住ListWmsInventoryDOinventoriesgetOrCreateInventoryList(items);inventoriesinventoryMapper.selectListByIdsForUpdate(convertSet(inventories,WmsInventoryDO::getId));// 2. 锁内逐条计算 校验充足for(Itemitem:items){WmsInventoryDOinvfindInventory(inventories,item);BigDecimalafterinv.getQuantity().add(item.getQuantity());// 出库为负if(after.compareTo(BigDecimal.ZERO)0){throwbuildInventoryQuantityNotEnoughException(item,inv.getQuantity());}inv.setQuantity(after);}// 3. 校验全通过后批量更新已加锁安全inventoryMapper.updateBatch(/* ... */);}1.3 优缺点维度评价一致性⭐⭐⭐⭐⭐ 强一致最稳妥适合场景一单多明细、移库改两仓等需要整单原子的复杂场景缺点行锁持有期间其它事务阻塞并发吞吐受限务必缩短事务、按固定顺序加锁防死锁二、方案二条件 UPDATE乐观锁 / CAS2.1 原理乐观锁的思路是先干活写时再校验不提前加锁而是把判断库存够不够塞进 UPDATE 的 WHERE 条件里靠数据库行级锁在更新瞬间完成判断扣减。扣得动返回 1库存不足返回 0UPDATEstockSETcountcount-#{buyCount}WHEREid#{id} AND count #{buyCount}应用层用影响行数判断成败0 行就说明并发下库存不足回滚即可。2.2 RuoYi Office 的真实落地ERP 进销存用条件 UPDATERuoYi Office 的 ERP 进销存就是条件 UPDATE 路线。库存扣减压成一条原子 SQL不加显式锁defaultintupdateCountIncrement(Longid,BigDecimalcount,booleannegativeEnable){LambdaUpdateWrapperErpStockDOwrappernewLambdaUpdateWrapperErpStockDO().eq(ErpStockDO::getId,id);if(count.compareTo(BigDecimal.ZERO)0){// 入库直接加wrapper.setSql(count count count);}elseif(count.compareTo(BigDecimal.ZERO)0){// 出库扣减if(!negativeEnable){wrapper.ge(ErpStockDO::getCount,count.abs());// 关键count 扣减量}wrapper.setSql(count count - count.abs());}returnupdate(null,wrapper);// 返回影响行数}Service 层按影响行数兜底——0 行说明被别人扣走了抛异常回滚intupdateCountstockMapper.updateCountIncrement(stock.getId(),count,NEGATIVE_STOCK_COUNT_ENABLE);if(updateCount0){throwexception(STOCK_COUNT_NEGATIVE2,productName,warehouseName);}2.3 版本号乐观锁另一种写法如果不是扣减而是覆盖式更新可以加version字段做乐观锁更新时带版本条件版本不匹配则失败重试UPDATEstockSETcount#{newCount}, version version 1WHEREid#{id} AND version #{oldVersion}2.4 优缺点维度评价一致性⭐⭐⭐⭐ 强一致单行原子性能不加显式锁吞吐高于悲观锁适合场景单行库存扣减、并发中等大多数进销存/后台场景缺点高冲突时大量更新失败需重试多明细整单一致不如悲观锁直观三、方案三Redis 预扣减高并发秒杀3.1 原理当并发量飙升到秒杀级别每秒上万请求数据库扛不住了。这时把库存预热到 Redis用 Redis 的原子操作先扣减再异步同步到数据库。Redis 单线程 原子命令天然防超卖// 库存预热活动开始前把可售库存放进 RedisstringRedisTemplate.opsForValue().set(stock:skuId,1000);// 扣减DECRBY 原子返回扣后值小于 0 说明超卖立即补回publicbooleandeduct(LongskuId,intbuyCount){LongremainstringRedisTemplate.opsForValue().increment(stock:skuId,-buyCount);if(remain!nullremain0){returntrue;// 预扣成功发 MQ 异步落库 创建订单}stringRedisTemplate.opsForValue().increment(stock:skuId,buyCount);// 回补returnfalse;// 库存不足}3.2 用 Lua 脚本保证判断扣减原子上面DECRBY再回补有细微时间窗更严谨的做法是用 Lua 脚本把判断扣减在 Redis 端原子执行-- deduct.lua库存足才扣返回剩余不足返回 -1localstocktonumber(redis.call(GET,KEYS[1]))ifstocknilthenreturn-1endifstocktonumber(ARGV[1])thenreturn-1endreturnredis.call(DECRBY,KEYS[1],ARGV[1])Redis 预扣成功后通过消息队列如 RocketMQ异步创建订单、扣减数据库库存把数据库从抗并发中解放出来只做最终落库。3.3 优缺点维度评价性能⭐⭐⭐⭐⭐ 抗万级 QPS秒杀首选适合场景秒杀、抢购、大促等超高并发瞬时扣减缺点架构复杂需处理 Redis 与 DB 最终一致、超卖回补、缓存预热与重建、宕机数据恢复四、方案四分布式锁Redisson4.1 原理在分布式/微服务环境下悲观锁锁的是单库的行跨服务的复杂业务如扣库存 加积分 记日志要整体串行可以用分布式锁。RuoYi Office 框架内置 Redisson加锁即一行ResourceprivateRedissonClientredissonClient;publicvoiddeduct(LongskuId,intbuyCount){RLocklockredissonClient.getLock(lock:stock:skuId);lock.lock();// 也可 tryLock(等待, 持有, 单位)try{// 临界区查库存、判断、扣减、写其它表整体串行}finally{lock.unlock();}}更优雅的是用框架的Lock注解若提供或 AOP把锁逻辑与业务解耦。4.2 优缺点维度评价适用跨服务/跨库的复杂临界区或需要锁业务而非某一行缺点锁粒度大则并发低需设合理超时与看门狗续期强依赖 Redis 可用性注意分布式锁与数据库锁不是二选一。常见组合是分布式锁锁业务边界 数据库条件 UPDATE 兜底防超卖双保险。五、选型决策什么并发量用什么方案结论先行没有最好的方案只有最匹配并发量与一致性要求的方案。一张对照表说清楚方案并发量级一致性实现复杂度典型场景RuoYi Office 对应条件 UPDATE乐观/CAS中百~千 QPS强⭐ 低进销存、后台扣减、单行ERP 进销存悲观锁 FOR UPDATE中百~千 QPS强⭐⭐ 中一单多明细、移库、复杂事务WMS 仓储Redis 预扣减高万级 QPS最终一致⭐⭐⭐⭐ 高秒杀、抢购、大促可扩展分布式锁 Redisson中高强⭐⭐⭐ 中高跨服务复杂临界区框架内置实战建议绝大多数企业后台系统进销存、ERP、WMS、CRM用条件 UPDATE 或悲观锁就够了——它们是强一致、易理解、零外部依赖的方案。不要一上来就上 Redis 预扣减过度设计。单行扣减优先条件 UPDATE性能好多明细整单一致优先悲观锁逻辑清晰——这正是 RuoYi Office 里 ERP 与 WMS 的分工。只有真到秒杀级并发才需要 Redis 预扣减 MQ 异步落库并接受最终一致的复杂度成本。跨服务复杂业务再叠加分布式锁但仍要用数据库条件更新兜底防超卖。六、避坑清单坑表现对策先查后改非原子偶发超卖用条件 UPDATE 或锁内判断绝不读—判断—写裸奔事务范围过大悲观锁阻塞严重缩短事务、只锁必要行、尽早提交加锁顺序不一致死锁多行加锁按固定顺序如 ID 升序Redis 与 DB 不一致预扣成功但落库失败MQ 重试 对账补偿 超卖回补乐观锁高冲突大量更新失败评估冲突率必要时改悲观锁或排队库存行不存在首次扣减报错不存在则创建唯一索引冲突回查WMS 的做法反向操作不对称反审核退库存出错审核/反审核用对称的加/退流水七、RuoYi Office 的双方案实践RuoYi Office 是少有的在同一套代码里同时演示两种库存并发方案的开源项目非常适合学习对照模块方案核心代码设计动机ERP 进销存条件 UPDATE乐观count count - x WHERE count x单行扣减为主追求吞吐WMS 仓储悲观锁FOR UPDATEselectListByIdsForUpdate批量锁定一单多 SKU、移库改两仓要整单一致两者都做到了强一致防超卖区别只在单行高吞吐还是多明细整单一致的取舍——这恰恰说明方案选型要跟着业务场景走而不是跟着听起来高级走。八、快速体验在线演示http://ruoyioffice.com/web/账号admin/ 密码admin123验证超卖防护进入 ERP 进销存对某产品库存调到很小如 1开两张大额销售出库单先后审核观察第二张被库存不足拦截条件 UPDATE 生效。进入 WMS 仓储对某 SKU 开出库单超量出库观察库存不足拦截悲观锁生效。阅读源码ErpStockMapper.updateCountIncrement与WmsInventoryServiceImpl.changeInventoryList对照两种实现。源码仓库平台地址GitHubhttps://github.com/yuqing2026/ruoyi-officeGitCodehttps://gitcode.com/zhouzhongyan/ruoyi-officeGiteehttps://gitee.com/yqzy1688/ruoyi-office结语库存扣减的并发难题核心就一句话让判断和扣减原子化。实现路径无非两条——加锁串行悲观或写时校验乐观/CAS再叠加 Redis 与分布式锁应对更高并发与更复杂边界。最重要的不是记住四种方案而是建立选型直觉先问并发量级和一致性要求再选方案。企业后台用条件 UPDATE 或悲观锁就稳了秒杀才上 Redis 预扣减——RuoYi Office 的 ERP 与 WMS 双实现就是最好的教材。如果你正在被库存超卖困扰欢迎参考 RuoYi Office 的源码实现也欢迎在评论区聊聊你们线上的库存扣减用的是哪种方案踩过哪些坑常见问题FAQ库存扣减用乐观锁还是悲观锁好看场景。单行库存扣减、并发中等优先条件 UPDATE乐观锁性能好、零依赖一单多明细或移库等需要整单一致的复杂事务优先悲观锁SELECT ... FOR UPDATE。RuoYi Office 里 ERP 用前者、WMS 用后者可对照学习。条件 UPDATE 真能防超卖吗能。UPDATE stock SET count count - x WHERE id ? AND count x利用数据库行级锁在更新瞬间完成判断扣减库存不足时影响 0 行应用层据此回滚并发再高也不会扣成负数。什么时候才需要 Redis 预扣减只有真正的秒杀/抢购级超高并发万级 QPS 瞬时才需要。普通企业进销存、后台系统用数据库锁或条件 UPDATE 完全够用盲目上 Redis 预扣减属于过度设计还要额外处理一致性与回补。RuoYi Office 的库存并发是怎么做的双方案ERP 进销存用条件 UPDATEcount count - x WHERE count x防超卖WMS 仓储用悲观锁selectListByIdsForUpdate批量锁定库存行后内存整体计算校验。两者都强一致防超卖且都开源可参考。分布式锁能替代数据库锁吗不建议完全替代。分布式锁适合锁跨服务的业务边界但仍应用数据库条件 UPDATE 兜底防超卖形成双保险。单纯依赖分布式锁一旦 Redis 抖动就可能失效。想要体验 RuoYi Office 的强大功能在线演示http://ruoyioffice.com/web/账号 admin / admin123源码仓库GitHub | GitCode | Gitee技术咨询添加微信17156169080备注「RuoYi Office」⭐如果觉得不错请给个 Star 支持一下
库存扣减的并发难题:超卖·悲观锁·乐观锁·Redis 预扣减 4 种方案实战
库存扣减的并发难题超卖·悲观锁·乐观锁·Redis 预扣减 4 种方案实战演示地址http://ruoyioffice.com | 源码1·GitHubruoyi-office | 源码2·GitCoderuoyi-office | 源码3·Giteeruoyi-office | 微信17156169080备注「RuoYi Office」库存扣减是几乎每个有库存的系统都绕不开的并发难题。它的本质只有一句话判断库存够不够和扣减库存这两步不是原子的并发下就会超卖。库存只剩 1 件两个请求同时来都查到还有 1 件于是都扣结果库存变成 -1超卖了。本文系统讲透 4 种主流解法——数据库悲观锁、条件 UPDATE乐观锁/CAS、Redis 预扣减、分布式锁每种都配可落地代码与适用场景并结合RuoYi Office 里 ERP 进销存用条件 UPDATE、WMS 仓储用SELECT FOR UPDATE 悲观锁的两套真实源码对照帮你想清楚什么并发量该用什么方案。▲ 全景图超卖根因查询与扣减非原子→ 4 种解法悲观锁 / 条件 UPDATE / Redis 预扣减 / 分布式锁→ 适用并发量与一致性强弱对照引言超卖是怎么发生的先把问题讲清楚。最朴素的库存扣减代码长这样——几乎所有超卖都源于它// ❌ 反例查询与扣减非原子并发必超卖ErpStockDOstockstockMapper.selectById(id);// 线程A、B 都查到 count 1if(stock.getCount()buyCount){// 都判断够stock.setCount(stock.getCount()-buyCount);// 都算出 0stockMapper.updateById(stock);// 都写 0实际卖出了 2 件}问题在于读—判断—写三步之间没有任何保护多个线程交错执行判断都基于同一个旧值。要解决它核心思路只有两类让这三步串行化加锁悲观同一时刻只让一个线程进入临界区。让判断扣减变成一个原子操作CAS乐观在写的瞬间再校验一次条件。下面 4 种方案本质都是这两类思路的不同实现与权衡。概念定义超卖Oversell指实际扣减的库存超过了真实可用库存导致库存为负或卖出不存在的货。它是典型的竞态条件Race Condition问题。一、方案一数据库悲观锁SELECT … FOR UPDATE1.1 原理悲观锁的思路是先占坑再干活扣减前先用SELECT ... FOR UPDATE把库存行锁住事务提交前别的事务改不了这行从而把读—判断—写串行化。Transactional(rollbackForException.class)publicvoiddeduct(Longid,BigDecimalbuyCount){// 1. 加行锁其它事务在本事务提交前无法修改/锁定该行StockDOstockstockMapper.selectByIdForUpdate(id);// SELECT ... FOR UPDATE// 2. 锁内判断 扣减此刻独占安全if(stock.getQuantity().compareTo(buyCount)0){throwexception(STOCK_NOT_ENOUGH);}stockMapper.updateQuantity(id,stock.getQuantity().subtract(buyCount));}对应的 MapperselectidselectByIdForUpdateresultTypeStockDOSELECT * FROM stock WHERE id #{id} FOR UPDATE/select1.2 RuoYi Office 的真实落地WMS 仓储用悲观锁RuoYi Office 的 WMS 仓储模块就是悲观锁路线。因为一张出库/移库单常涉及多个 SKU它先把本次涉及的所有库存行批量SELECT ... FOR UPDATE锁住再在内存里整体计算校验、批量更新——保证整单一致privateMapItem,TuplechangeInventoryList(ListItemitems){// 1. 批量加锁把本次涉及的库存行全部 SELECT ... FOR UPDATE 锁住ListWmsInventoryDOinventoriesgetOrCreateInventoryList(items);inventoriesinventoryMapper.selectListByIdsForUpdate(convertSet(inventories,WmsInventoryDO::getId));// 2. 锁内逐条计算 校验充足for(Itemitem:items){WmsInventoryDOinvfindInventory(inventories,item);BigDecimalafterinv.getQuantity().add(item.getQuantity());// 出库为负if(after.compareTo(BigDecimal.ZERO)0){throwbuildInventoryQuantityNotEnoughException(item,inv.getQuantity());}inv.setQuantity(after);}// 3. 校验全通过后批量更新已加锁安全inventoryMapper.updateBatch(/* ... */);}1.3 优缺点维度评价一致性⭐⭐⭐⭐⭐ 强一致最稳妥适合场景一单多明细、移库改两仓等需要整单原子的复杂场景缺点行锁持有期间其它事务阻塞并发吞吐受限务必缩短事务、按固定顺序加锁防死锁二、方案二条件 UPDATE乐观锁 / CAS2.1 原理乐观锁的思路是先干活写时再校验不提前加锁而是把判断库存够不够塞进 UPDATE 的 WHERE 条件里靠数据库行级锁在更新瞬间完成判断扣减。扣得动返回 1库存不足返回 0UPDATEstockSETcountcount-#{buyCount}WHEREid#{id} AND count #{buyCount}应用层用影响行数判断成败0 行就说明并发下库存不足回滚即可。2.2 RuoYi Office 的真实落地ERP 进销存用条件 UPDATERuoYi Office 的 ERP 进销存就是条件 UPDATE 路线。库存扣减压成一条原子 SQL不加显式锁defaultintupdateCountIncrement(Longid,BigDecimalcount,booleannegativeEnable){LambdaUpdateWrapperErpStockDOwrappernewLambdaUpdateWrapperErpStockDO().eq(ErpStockDO::getId,id);if(count.compareTo(BigDecimal.ZERO)0){// 入库直接加wrapper.setSql(count count count);}elseif(count.compareTo(BigDecimal.ZERO)0){// 出库扣减if(!negativeEnable){wrapper.ge(ErpStockDO::getCount,count.abs());// 关键count 扣减量}wrapper.setSql(count count - count.abs());}returnupdate(null,wrapper);// 返回影响行数}Service 层按影响行数兜底——0 行说明被别人扣走了抛异常回滚intupdateCountstockMapper.updateCountIncrement(stock.getId(),count,NEGATIVE_STOCK_COUNT_ENABLE);if(updateCount0){throwexception(STOCK_COUNT_NEGATIVE2,productName,warehouseName);}2.3 版本号乐观锁另一种写法如果不是扣减而是覆盖式更新可以加version字段做乐观锁更新时带版本条件版本不匹配则失败重试UPDATEstockSETcount#{newCount}, version version 1WHEREid#{id} AND version #{oldVersion}2.4 优缺点维度评价一致性⭐⭐⭐⭐ 强一致单行原子性能不加显式锁吞吐高于悲观锁适合场景单行库存扣减、并发中等大多数进销存/后台场景缺点高冲突时大量更新失败需重试多明细整单一致不如悲观锁直观三、方案三Redis 预扣减高并发秒杀3.1 原理当并发量飙升到秒杀级别每秒上万请求数据库扛不住了。这时把库存预热到 Redis用 Redis 的原子操作先扣减再异步同步到数据库。Redis 单线程 原子命令天然防超卖// 库存预热活动开始前把可售库存放进 RedisstringRedisTemplate.opsForValue().set(stock:skuId,1000);// 扣减DECRBY 原子返回扣后值小于 0 说明超卖立即补回publicbooleandeduct(LongskuId,intbuyCount){LongremainstringRedisTemplate.opsForValue().increment(stock:skuId,-buyCount);if(remain!nullremain0){returntrue;// 预扣成功发 MQ 异步落库 创建订单}stringRedisTemplate.opsForValue().increment(stock:skuId,buyCount);// 回补returnfalse;// 库存不足}3.2 用 Lua 脚本保证判断扣减原子上面DECRBY再回补有细微时间窗更严谨的做法是用 Lua 脚本把判断扣减在 Redis 端原子执行-- deduct.lua库存足才扣返回剩余不足返回 -1localstocktonumber(redis.call(GET,KEYS[1]))ifstocknilthenreturn-1endifstocktonumber(ARGV[1])thenreturn-1endreturnredis.call(DECRBY,KEYS[1],ARGV[1])Redis 预扣成功后通过消息队列如 RocketMQ异步创建订单、扣减数据库库存把数据库从抗并发中解放出来只做最终落库。3.3 优缺点维度评价性能⭐⭐⭐⭐⭐ 抗万级 QPS秒杀首选适合场景秒杀、抢购、大促等超高并发瞬时扣减缺点架构复杂需处理 Redis 与 DB 最终一致、超卖回补、缓存预热与重建、宕机数据恢复四、方案四分布式锁Redisson4.1 原理在分布式/微服务环境下悲观锁锁的是单库的行跨服务的复杂业务如扣库存 加积分 记日志要整体串行可以用分布式锁。RuoYi Office 框架内置 Redisson加锁即一行ResourceprivateRedissonClientredissonClient;publicvoiddeduct(LongskuId,intbuyCount){RLocklockredissonClient.getLock(lock:stock:skuId);lock.lock();// 也可 tryLock(等待, 持有, 单位)try{// 临界区查库存、判断、扣减、写其它表整体串行}finally{lock.unlock();}}更优雅的是用框架的Lock注解若提供或 AOP把锁逻辑与业务解耦。4.2 优缺点维度评价适用跨服务/跨库的复杂临界区或需要锁业务而非某一行缺点锁粒度大则并发低需设合理超时与看门狗续期强依赖 Redis 可用性注意分布式锁与数据库锁不是二选一。常见组合是分布式锁锁业务边界 数据库条件 UPDATE 兜底防超卖双保险。五、选型决策什么并发量用什么方案结论先行没有最好的方案只有最匹配并发量与一致性要求的方案。一张对照表说清楚方案并发量级一致性实现复杂度典型场景RuoYi Office 对应条件 UPDATE乐观/CAS中百~千 QPS强⭐ 低进销存、后台扣减、单行ERP 进销存悲观锁 FOR UPDATE中百~千 QPS强⭐⭐ 中一单多明细、移库、复杂事务WMS 仓储Redis 预扣减高万级 QPS最终一致⭐⭐⭐⭐ 高秒杀、抢购、大促可扩展分布式锁 Redisson中高强⭐⭐⭐ 中高跨服务复杂临界区框架内置实战建议绝大多数企业后台系统进销存、ERP、WMS、CRM用条件 UPDATE 或悲观锁就够了——它们是强一致、易理解、零外部依赖的方案。不要一上来就上 Redis 预扣减过度设计。单行扣减优先条件 UPDATE性能好多明细整单一致优先悲观锁逻辑清晰——这正是 RuoYi Office 里 ERP 与 WMS 的分工。只有真到秒杀级并发才需要 Redis 预扣减 MQ 异步落库并接受最终一致的复杂度成本。跨服务复杂业务再叠加分布式锁但仍要用数据库条件更新兜底防超卖。六、避坑清单坑表现对策先查后改非原子偶发超卖用条件 UPDATE 或锁内判断绝不读—判断—写裸奔事务范围过大悲观锁阻塞严重缩短事务、只锁必要行、尽早提交加锁顺序不一致死锁多行加锁按固定顺序如 ID 升序Redis 与 DB 不一致预扣成功但落库失败MQ 重试 对账补偿 超卖回补乐观锁高冲突大量更新失败评估冲突率必要时改悲观锁或排队库存行不存在首次扣减报错不存在则创建唯一索引冲突回查WMS 的做法反向操作不对称反审核退库存出错审核/反审核用对称的加/退流水七、RuoYi Office 的双方案实践RuoYi Office 是少有的在同一套代码里同时演示两种库存并发方案的开源项目非常适合学习对照模块方案核心代码设计动机ERP 进销存条件 UPDATE乐观count count - x WHERE count x单行扣减为主追求吞吐WMS 仓储悲观锁FOR UPDATEselectListByIdsForUpdate批量锁定一单多 SKU、移库改两仓要整单一致两者都做到了强一致防超卖区别只在单行高吞吐还是多明细整单一致的取舍——这恰恰说明方案选型要跟着业务场景走而不是跟着听起来高级走。八、快速体验在线演示http://ruoyioffice.com/web/账号admin/ 密码admin123验证超卖防护进入 ERP 进销存对某产品库存调到很小如 1开两张大额销售出库单先后审核观察第二张被库存不足拦截条件 UPDATE 生效。进入 WMS 仓储对某 SKU 开出库单超量出库观察库存不足拦截悲观锁生效。阅读源码ErpStockMapper.updateCountIncrement与WmsInventoryServiceImpl.changeInventoryList对照两种实现。源码仓库平台地址GitHubhttps://github.com/yuqing2026/ruoyi-officeGitCodehttps://gitcode.com/zhouzhongyan/ruoyi-officeGiteehttps://gitee.com/yqzy1688/ruoyi-office结语库存扣减的并发难题核心就一句话让判断和扣减原子化。实现路径无非两条——加锁串行悲观或写时校验乐观/CAS再叠加 Redis 与分布式锁应对更高并发与更复杂边界。最重要的不是记住四种方案而是建立选型直觉先问并发量级和一致性要求再选方案。企业后台用条件 UPDATE 或悲观锁就稳了秒杀才上 Redis 预扣减——RuoYi Office 的 ERP 与 WMS 双实现就是最好的教材。如果你正在被库存超卖困扰欢迎参考 RuoYi Office 的源码实现也欢迎在评论区聊聊你们线上的库存扣减用的是哪种方案踩过哪些坑常见问题FAQ库存扣减用乐观锁还是悲观锁好看场景。单行库存扣减、并发中等优先条件 UPDATE乐观锁性能好、零依赖一单多明细或移库等需要整单一致的复杂事务优先悲观锁SELECT ... FOR UPDATE。RuoYi Office 里 ERP 用前者、WMS 用后者可对照学习。条件 UPDATE 真能防超卖吗能。UPDATE stock SET count count - x WHERE id ? AND count x利用数据库行级锁在更新瞬间完成判断扣减库存不足时影响 0 行应用层据此回滚并发再高也不会扣成负数。什么时候才需要 Redis 预扣减只有真正的秒杀/抢购级超高并发万级 QPS 瞬时才需要。普通企业进销存、后台系统用数据库锁或条件 UPDATE 完全够用盲目上 Redis 预扣减属于过度设计还要额外处理一致性与回补。RuoYi Office 的库存并发是怎么做的双方案ERP 进销存用条件 UPDATEcount count - x WHERE count x防超卖WMS 仓储用悲观锁selectListByIdsForUpdate批量锁定库存行后内存整体计算校验。两者都强一致防超卖且都开源可参考。分布式锁能替代数据库锁吗不建议完全替代。分布式锁适合锁跨服务的业务边界但仍应用数据库条件 UPDATE 兜底防超卖形成双保险。单纯依赖分布式锁一旦 Redis 抖动就可能失效。想要体验 RuoYi Office 的强大功能在线演示http://ruoyioffice.com/web/账号 admin / admin123源码仓库GitHub | GitCode | Gitee技术咨询添加微信17156169080备注「RuoYi Office」⭐如果觉得不错请给个 Star 支持一下