食堂采购系统源码库存扣减算法与并发控制实现详解

做食堂采购系统,真正难的从来不是页面,也不是流程。

而是两个字:库存。

很多团队一开始都觉得库存扣减很简单:

update inventory set quantity = quantity - 10;

上线一周后就开始出问题:

  • 库存变负数
  • 多人同时领料数据错乱
  • 成本算不准
  • 对账永远对不上
  • 高峰期直接超卖

说句实在话:

只要库存算法没设计好,这套系统就一定跑不久。

食堂场景有个特点:

  • 早上集中入库
  • 中午集中出库
  • 多窗口同时领料
  • 并发非常高

这本质就是一个「高并发扣库存」系统。

下面我从实战角度,把一套能商用落地的库存扣减方案完整拆开讲清楚。

技术栈示例:

SpringBoot + MySQL + MyBatis + Redis

一、先搞清楚库存的本质模型

很多人一上来就写扣减逻辑,这是顺序错了。

库存正确模型应该是:

库存表 = 当前结果

流水表 = 真实依据

必须是:

有流水 → 才能变库存

而不是直接改库存。

推荐表结构

1 库存主表 inventory

sql 复制代码
CREATE TABLE inventory (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  goods_id BIGINT NOT NULL,
  warehouse_id BIGINT NOT NULL,
  quantity DECIMAL(10,2) DEFAULT 0,
  amount DECIMAL(12,2) DEFAULT 0,
  version INT DEFAULT 0,
  UNIQUE KEY uk_goods_wh(goods_id, warehouse_id)
);

关键字段:

  • quantity 当前库存
  • amount 库存总成本
  • version 乐观锁

2 库存流水表 inventory_log

sql 复制代码
CREATE TABLE inventory_log (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  goods_id BIGINT,
  warehouse_id BIGINT,
  type VARCHAR(20),
  quantity DECIMAL(10,2),
  price DECIMAL(10,2),
  amount DECIMAL(12,2),
  created_at DATETIME
);

所有变化都必须记录。

这是后期对账的唯一依据。

二、最容易踩坑的 3 种错误写法

错误写法一:直接扣减

sql 复制代码
update inventory set quantity = quantity - 5;

问题:

  • 无并发保护
  • 多线程同时扣 → 负数

直接淘汰。

错误写法二:先查再扣

java 复制代码
Inventory inv = select();
if(inv.getQuantity() >= 5){
   update();
}

问题:

并发时两个线程都读到 10,都能扣。

结果变 -5。

这叫:读写分离导致超卖

错误写法三:只加事务

很多人以为:

@Transactional 就安全了。

错。

事务只能保证单线程一致,不能解决并发竞争。

三、正确思路:三层并发控制模型

真正可商用方案一定是:

第一层:数据库乐观锁

第二层:条件扣减

第三层:Redis预扣减(高并发场景)

三层叠加,才稳。

四、核心方案一:MySQL乐观锁扣减(基础必备)

这是所有系统的底线方案。

扣减SQL(核心)

sql 复制代码
UPDATE inventory
SET quantity = quantity - #{qty},
    amount = amount - #{amount},
    version = version + 1
WHERE goods_id = #{goodsId}
AND warehouse_id = #{warehouseId}
AND quantity >= #{qty}
AND version = #{version};

重点:

  • quantity >= qty 防止负数
  • version 防止并发覆盖

影响行数 = 1 才成功。

Java实现

java 复制代码
@Transactional
public void stockOut(Long goodsId, Long warehouseId, BigDecimal qty) {

    Inventory inv = inventoryMapper.select(goodsId, warehouseId);

    if (inv.getQuantity().compareTo(qty) < 0) {
        throw new RuntimeException("库存不足");
    }

    BigDecimal avgPrice =
            inv.getAmount().divide(inv.getQuantity(), 2, RoundingMode.HALF_UP);

    BigDecimal amount = avgPrice.multiply(qty);

    int rows = inventoryMapper.reduceStock(
            goodsId, warehouseId, qty, amount, inv.getVersion());

    if (rows == 0) {
        throw new RuntimeException("并发冲突,请重试");
    }

    inventoryLogMapper.insert(
            new InventoryLog(goodsId, warehouseId, "OUT", qty, avgPrice, amount)
    );
}

优点:

  • 实现简单
  • 强一致
  • 适合中等并发

缺点:

高并发下重试多,性能下降

五、核心方案二:悲观锁(强一致但慢)

如果库存极度敏感,可以用:

sql 复制代码
SELECT * FROM inventory
WHERE goods_id = ?
FOR UPDATE;

锁行再更新。

问题是:

高并发直接阻塞,吞吐量低。

食堂中午高峰可能直接卡死。

所以:

只建议小并发系统使用。

六、核心方案三:Redis + MySQL 双层扣减(高并发推荐)

当:

  • 多窗口同时领料
  • 上百人同时出库

单靠数据库扛不住。

必须引入 Redis。

思路是:

先扣 Redis

再异步写 MySQL

Redis Lua脚本(原子扣减)

sql 复制代码
local stock = tonumber(redis.call('get', KEYS[1]))

if stock <= 0 then
    return -1
end

redis.call('decrby', KEYS[1], ARGV[1])
return 1

保证:

  • 原子性
  • 无超卖

Java调用

java 复制代码
Long result = redisTemplate.execute(luaScript,
        Collections.singletonList("stock:" + goodsId),
        qty.toString());

if(result == -1){
    throw new RuntimeException("库存不足");
}

异步落库

使用 MQ:

java 复制代码
mqProducer.send(new StockMessage(goodsId, qty));

消费者再更新数据库。

优点:

  • 极高并发
  • 抗压能力强

缺点:

  • 最终一致性
  • 实现复杂

适合:

多食堂 + 集团化 + 上千并发场景。

七、成本算法实现(食堂最佳实践)

食堂不需要复杂批次。

推荐:

加权平均法

公式:

java 复制代码
平均价 = amount / quantity

实现:

java 复制代码
BigDecimal avgPrice =
    inv.getAmount().divide(inv.getQuantity(), 2, RoundingMode.HALF_UP);

简单、稳定、易对账。

八、实战选型建议

给你一句很现实的选型建议:

小学校

直接 MySQL 乐观锁

中型学校

乐观锁 + 索引优化

集团/多校区

Redis + MQ + MySQL

别一上来就搞复杂架构。

技术是为业务服务,不是炫技。

九、最后的经验总结

做食堂采购系统源码,这三条是底线原则:

第一

库存只改一张表,必须带锁

第二

所有变更必须写流水

第三

绝不允许负库存

只要守住这三条,系统稳定性至少提升一个量级。

库存做好了,这套系统才算真正可商用。

相关推荐
程序员agions1 小时前
2026年,微前端终于“死“了
前端·状态模式
独断万古他化2 小时前
【Spring 原理】Bean 的作用域与生命周期
java·后端·spring
张登杰踩2 小时前
MCR ALS 多元曲线分辨算法详解
算法
程序员猫哥_2 小时前
HTML 生成网页工具推荐:从手写代码到 AI 自动生成网页的进化路径
前端·人工智能·html
龙飞052 小时前
Systemd -systemctl - journalctl 速查表:服务管理 + 日志排障
linux·运维·前端·chrome·systemctl·journalctl
*小海豚*2 小时前
在linux服务器上DNS正常,但是java应用调用第三方解析域名报错
java·linux·服务器
冉冰学姐2 小时前
SSM智慧社区管理系统jby69(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·管理系统·智慧社区·ssm 框架
我爱加班、、2 小时前
Websocket能携带token过去后端吗
前端·后端·websocket
AAA阿giao2 小时前
从零拆解一个 React + TypeScript 的 TodoList:模块化、数据流与工程实践
前端·react.js·ui·typescript·前端框架