做食堂采购系统真正难的从来不是页面也不是流程。而是两个字库存。很多团队一开始都觉得库存扣减很简单update inventory set quantity quantity - 10;上线一周后就开始出问题库存变负数多人同时领料数据错乱成本算不准对账永远对不上高峰期直接超卖说句实在话只要库存算法没设计好这套系统就一定跑不久。食堂场景有个特点早上集中入库中午集中出库多窗口同时领料并发非常高这本质就是一个「高并发扣库存」系统。下面我从实战角度把一套能商用落地的库存扣减方案完整拆开讲清楚。技术栈示例SpringBoot MySQL MyBatis Redis一、先搞清楚库存的本质模型很多人一上来就写扣减逻辑这是顺序错了。库存正确模型应该是库存表 当前结果流水表 真实依据必须是有流水 → 才能变库存而不是直接改库存。推荐表结构1 库存主表 inventoryCREATETABLEinventory(idBIGINTPRIMARYKEYAUTO_INCREMENT,goods_idBIGINTNOTNULL,warehouse_idBIGINTNOTNULL,quantityDECIMAL(10,2)DEFAULT0,amountDECIMAL(12,2)DEFAULT0,versionINTDEFAULT0,UNIQUEKEYuk_goods_wh(goods_id,warehouse_id));关键字段quantity 当前库存amount 库存总成本version 乐观锁2 库存流水表 inventory_logCREATETABLEinventory_log(idBIGINTPRIMARYKEYAUTO_INCREMENT,goods_idBIGINT,warehouse_idBIGINT,typeVARCHAR(20),quantityDECIMAL(10,2),priceDECIMAL(10,2),amountDECIMAL(12,2),created_atDATETIME);所有变化都必须记录。这是后期对账的唯一依据。二、最容易踩坑的 3 种错误写法错误写法一直接扣减updateinventorysetquantityquantity-5;问题无并发保护多线程同时扣 → 负数直接淘汰。错误写法二先查再扣Inventoryinvselect();if(inv.getQuantity()5){update();}问题并发时两个线程都读到 10都能扣。结果变 -5。这叫读写分离导致超卖错误写法三只加事务很多人以为Transactional 就安全了。错。事务只能保证单线程一致不能解决并发竞争。三、正确思路三层并发控制模型真正可商用方案一定是第一层数据库乐观锁第二层条件扣减第三层Redis预扣减高并发场景三层叠加才稳。四、核心方案一MySQL乐观锁扣减基础必备这是所有系统的底线方案。扣减SQL核心UPDATEinventorySETquantityquantity-#{qty},amountamount-#{amount},versionversion1WHEREgoods_id#{goodsId}ANDwarehouse_id#{warehouseId}ANDquantity#{qty}ANDversion#{version};重点quantity qty 防止负数version 防止并发覆盖影响行数 1 才成功。Java实现TransactionalpublicvoidstockOut(LonggoodsId,LongwarehouseId,BigDecimalqty){InventoryinvinventoryMapper.select(goodsId,warehouseId);if(inv.getQuantity().compareTo(qty)0){thrownewRuntimeException(库存不足);}BigDecimalavgPriceinv.getAmount().divide(inv.getQuantity(),2,RoundingMode.HALF_UP);BigDecimalamountavgPrice.multiply(qty);introwsinventoryMapper.reduceStock(goodsId,warehouseId,qty,amount,inv.getVersion());if(rows0){thrownewRuntimeException(并发冲突请重试);}inventoryLogMapper.insert(newInventoryLog(goodsId,warehouseId,OUT,qty,avgPrice,amount));}优点实现简单强一致适合中等并发缺点高并发下重试多性能下降五、核心方案二悲观锁强一致但慢如果库存极度敏感可以用SELECT*FROMinventoryWHEREgoods_id?FORUPDATE;锁行再更新。问题是高并发直接阻塞吞吐量低。食堂中午高峰可能直接卡死。所以只建议小并发系统使用。六、核心方案三Redis MySQL 双层扣减高并发推荐当多窗口同时领料上百人同时出库单靠数据库扛不住。必须引入 Redis。思路是先扣 Redis再异步写 MySQLRedis Lua脚本原子扣减localstocktonumber(redis.call(get,KEYS[1]))ifstock0thenreturn-1endredis.call(decrby,KEYS[1],ARGV[1])return1保证原子性无超卖Java调用LongresultredisTemplate.execute(luaScript,Collections.singletonList(stock:goodsId),qty.toString());if(result-1){thrownewRuntimeException(库存不足);}异步落库使用 MQmqProducer.send(newStockMessage(goodsId,qty));消费者再更新数据库。优点极高并发抗压能力强缺点最终一致性实现复杂适合多食堂 集团化 上千并发场景。七、成本算法实现食堂最佳实践食堂不需要复杂批次。推荐加权平均法公式平均价amount/quantity实现BigDecimalavgPriceinv.getAmount().divide(inv.getQuantity(),2,RoundingMode.HALF_UP);简单、稳定、易对账。八、实战选型建议给你一句很现实的选型建议小学校直接 MySQL 乐观锁中型学校乐观锁 索引优化集团/多校区Redis MQ MySQL别一上来就搞复杂架构。技术是为业务服务不是炫技。九、最后的经验总结做食堂采购系统源码这三条是底线原则第一库存只改一张表必须带锁第二所有变更必须写流水第三绝不允许负库存只要守住这三条系统稳定性至少提升一个量级。库存做好了这套系统才算真正可商用。