
简介这是一套面向Java初学者与课程设计者的库存管理系统完整源码采用Java作为开发语言、SQL数据库负责数据存储帮助理解业务逻辑与数据持久化的结合方式。资源包共173个文件约1005KB以java源文件为主辅以jpg界面截图、jar依赖包、config配置项以及mdf、ldf数据库文件覆盖从代码到数据库的完整结构。已有1166人学习下载适合作为毕业设计、实训项目或自学练手参考。内容围绕库存查询、入库出库、供应商与商品信息管理等核心模块展开涉及Swing或JavaFX界面构建、多线程并发处理、CRUD操作、JOIN关联查询以及ORM映射思路并包含数据模型设计与索引优化等实践要点。读者可据此梳理分层结构、理解Java与SQL的协作方式并对照配置与数据库文件快速还原运行环境为后续功能扩展与性能调优提供可复用的基础。1. 库存管理系统 JAVASQL为什么“能跑”和“敢用”之间隔着一整套设计很多开发者第一次做库存管理系统都是被“进销存”三个字骗进来的。以为无非是商品表、入库表、出库表三张表加几个增删改查接口一周就能收工。真到上线那天才发现两个人同时出同一批货库存扣成了负数盘点时账面数量和实物对不上翻遍日志找不到是哪一笔写错的老板要一张“近30天周转率”报表SQL 写了三小时还没跑出结果。库存管理系统 JAVASQL 这个组合之所以长期挂在搜索热榜上不是因为技术新而是因为它是一个“业务正确性”远重于“功能数量”的典型场景。它适合两类人一类是刚学完 SSM 或 Spring Boot 想找个真实项目练手的开发者另一类是被 Excel 库存表折磨到崩溃、想自己搭一套轻量系统的小团队技术负责人。这篇文章不讲空泛的架构图只讲怎么用 JAVA 做服务层、用 SQL 做数据层把库存这件事从“能跑”做到“敢用”。2. 表结构定生死库存管理系统的 SQL 建模与字段取舍库存系统的绝大多数 bug根因不在 Java 代码而在建表时少想了一步。这一章先把数据模型立住后面所有的并发控制、报表查询、对账逻辑才有落脚点。2.1 商品表、库存表、流水表三张核心表的职责边界常见做法是把库存数量直接放在商品表里一个stock字段搞定。小规模能用但只要出现“同一商品多仓库”或“需要追溯每一次变动”这个设计就会崩。我一般会拆成三张表product商品基础信息只放名称、规格、单位、条码不放数量。inventory商品与仓库的库存快照记录当前可用量、锁定量。stock_record每一次入库、出库、盘点的流水只增不改。这样拆的好处是库存数量永远可以从流水重算流水表就是库存系统的“黑匣子”对不上账时能倒查。-- 商品表只存静态属性 CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_code VARCHAR(64) NOT NULL UNIQUE COMMENT 商品编码业务唯一键, product_name VARCHAR(128) NOT NULL, unit VARCHAR(16) NOT NULL DEFAULT 件, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 库存快照表商品仓库维度唯一 CREATE TABLE inventory ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, available_qty INT NOT NULL DEFAULT 0 COMMENT 可用库存, locked_qty INT NOT NULL DEFAULT 0 COMMENT 已锁定未出库, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, UNIQUE KEY uk_product_warehouse (product_id, warehouse_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 库存流水表只增不改所有变动留痕 CREATE TABLE stock_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, change_type TINYINT NOT NULL COMMENT 1入库 2出库 3锁定 4解锁 5盘点, change_qty INT NOT NULL COMMENT 正数增加负数减少, before_qty INT NOT NULL, after_qty INT NOT NULL, biz_no VARCHAR(64) NOT NULL COMMENT 业务单号用于幂等, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_product_time (product_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明inventory表上的UNIQUE KEY是防止同一商品同一仓库出现两条记录这是很多新手建表时漏掉的一旦漏掉后续UPDATE会随机更新一条库存直接错乱。stock_record里的before_qty和after_qty是排查问题时最值钱的两个字段没有它们你只能看到“变了”看不到“从多少变到多少”。biz_no配合唯一索引可以做幂等防止网络重试导致重复扣减。参数说明available_qty和locked_qty分开是必须的。下单未付款时锁库存付款后才真正扣减。如果只有一个stock字段你无法区分“被订单占用”和“实际可卖”。version字段留给乐观锁下一章会用到。2.2 用 SQL 约束兜住业务规则而不是全靠 Java 判断很多团队把所有校验写在 Service 层SQL 层毫无约束。结果是定时任务、数据修复脚本、其他服务直连数据库时全部绕过校验脏数据就是这么来的。我的习惯是能在数据库层表达的规则绝不只写在 Java 里。-- 库存不能为负用 CHECK 约束MySQL 8.0.16 支持 ALTER TABLE inventory ADD CONSTRAINT chk_available_non_negative CHECK (available_qty 0), ADD CONSTRAINT chk_locked_non_negative CHECK (locked_qty 0); -- 流水表业务单号类型唯一保证同一笔业务只记一次 ALTER TABLE stock_record ADD UNIQUE KEY uk_biz_type (biz_no, change_type, product_id);逻辑说明CHECK约束是最后一道防线。Java 层判断available_qty qty之后到真正UPDATE之间有时间窗口并发下仍可能扣成负数。加上数据库约束后即使代码有漏洞最坏结果是抛异常回滚而不是写出负库存。uk_biz_type保证同一张出库单对同一商品只扣一次重试接口不会重复扣减。参数说明MySQL 8.0.16 之前CHECK是被忽略的如果用的是 5.7需要用触发器或应用层加锁替代。这一点在选版本时就要确认别写完才发现约束没生效。2.3 索引怎么建让库存查询和流水对账都不全表扫描库存系统有两类高频查询一是按商品查当前库存二是按时间段查某商品的流水。索引建错数据量上到十万级就开始卡。-- 按商品仓库查库存走唯一索引 uk_product_warehouse天然覆盖 -- 按商品查流水并按时间倒序建联合索引 ALTER TABLE stock_record ADD KEY idx_product_created (product_id, created_at DESC); -- 按业务单号反查流水对账常用 ALTER TABLE stock_record ADD KEY idx_biz_no (biz_no);逻辑说明idx_product_created让“查某商品最近30天流水”直接走索引范围扫描避免全表排序。idx_biz_no用于对账时根据单号拉出所有变动。注意不要给change_type单独建索引区分度太低优化器通常不会选。参数说明created_at DESC在 MySQL 8.0 支持降序索引5.7 会忽略 DESC 但仍可用。如果流水表预计超过千万行考虑按月分表但那是另一个话题初期不必过度设计。3. JAVA 服务层怎么写扣减库存的并发控制与事务边界表建好了接下来是 Java 层。库存系统最核心的一段代码就是“扣减库存”写错这一处前面所有设计都白费。这一章把并发场景拆开讲。3.1 乐观锁扣减UPDATE 带 version 条件的最小实现高并发下扣库存常见做法有两种悲观锁SELECT ... FOR UPDATE和乐观锁UPDATE ... WHERE version ?。前者锁行时间长后者重试成本高但吞吐更好。库存场景我一般优先乐观锁因为冲突概率通常可控。Service public class InventoryService { Autowired private InventoryMapper inventoryMapper; /** * 扣减可用库存乐观锁实现 * param productId 商品ID * param warehouseId 仓库ID * param qty 扣减数量正数 * return 是否成功 */ public boolean deductStock(Long productId, Long warehouseId, int qty) { // 最多重试3次避免无限循环 for (int i 0; i 3; i) { Inventory inv inventoryMapper.selectByProductAndWarehouse(productId, warehouseId); if (inv null || inv.getAvailableQty() qty) { return false; // 库存不足直接失败 } int affected inventoryMapper.deductWithVersion( productId, warehouseId, qty, inv.getVersion()); if (affected 0) { return true; // 扣减成功 } // affected 0 说明版本被其他线程改了重试 } return false; } }对应的 Mapper SQLupdate iddeductWithVersion UPDATE inventory SET available_qty available_qty - #{qty}, version version 1 WHERE product_id #{productId} AND warehouse_id #{warehouseId} AND version #{version} AND available_qty #{qty} /update逻辑说明WHERE里同时带version和available_qty qty是关键。前者保证没有其他线程改过这条记录后者是数据库层的兜底防止在读取和更新之间库存被扣光。affected 0有两种可能版本变了或者库存不够了。这里统一按重试处理重试时重新读取会自然发现库存不足并返回 false。参数说明重试次数设 3 次是经验值。冲突率低于 10% 时3 次重试成功率已经很高如果冲突率超过 30%说明热点商品太集中应该考虑把库存拆成分段如按仓库再分片或改用队列串行化。qty必须是正数扣减方向由 SQL 里的减号决定不要传负数否则逻辑会乱。3.2 事务边界扣库存、写流水、更新订单必须在一个事务里扣库存成功但流水没写、或者订单状态没更新是库存系统最典型的“半成功”故障。解决办法是把这三步放进同一个Transactional方法。Transactional(rollbackFor Exception.class) public void createOutboundOrder(OutboundRequest req) { // 1. 扣减库存 boolean deducted deductStock(req.getProductId(), req.getWarehouseId(), req.getQty()); if (!deducted) { throw new BizException(库存不足); } // 2. 写流水 stockRecordMapper.insert(buildRecord(req)); // 3. 更新订单状态 orderMapper.updateStatus(req.getOrderId(), OrderStatus.OUTBOUND); }逻辑说明rollbackFor Exception.class确保任何异常都回滚包括受检异常。默认 Spring 只回滚RuntimeException如果中间抛了IOException之类事务不会回滚库存扣了但流水没写。这个坑我踩过排查了半天。参数说明事务方法里不要做远程调用或发消息否则事务时间被拉长锁持有时间增加并发能力下降。正确做法是用本地消息表或事务消息把“发消息”变成事务内的一次 insert再由后台任务投递。3.3 幂等设计同一笔出库请求重复提交怎么办前端重复点击、网关重试、消息重复消费都会导致同一笔出库请求到达两次。没有幂等库存就被扣两次。public void handleOutbound(String bizNo, Long productId, int qty) { // 先查流水表该业务单号是否已处理 int count stockRecordMapper.countByBizNo(bizNo, productId); if (count 0) { return; // 已处理直接返回 } // 未处理走正常扣减流程 createOutboundOrder(...); }逻辑说明这里依赖第 2 章建的uk_biz_type唯一索引。即使两个线程同时通过count 0检查最终只有一个能插入成功另一个会因唯一键冲突抛异常捕获后当作“已处理”返回即可。这是“先查后插”加“唯一约束兜底”的标准组合。参数说明bizNo必须由上游生成且全局唯一常见是订单号或“订单号商品ID”。不要用时间戳或随机数那样每次重试都是新单号幂等失效。4. 避坑与排查库存系统上线后最容易翻车的 5 个场景这一章全是血泪经验每条都按“现象 → 原因 → 解决”写遇到对应问题时可以直接对照。4.1 库存扣成负数日志里却找不到哪一笔扣的现象盘点时发现某商品库存为 -5但流水表里所有记录加起来不等于 -5。原因早期版本没有before_qty和after_qty只有change_qty。一旦有并发写入流水顺序和实际执行顺序不一致无法还原。更糟的是如果某次扣减没写流水事务边界错误流水总和和库存快照永远对不上。解决流水表必须记录变动前后值且扣减和写流水在同一事务。已经上线的系统可以写一个对账脚本用流水重算库存和快照比对差异记录人工核查。4.2 乐观锁重试次数用完了用户看到“系统繁忙”现象大促时部分用户下单失败提示系统繁忙但库存明明还有。原因热点商品冲突率过高3 次重试不够。乐观锁在冲突激烈时退化成“不断重试”吞吐反而下降。解决对热点商品改用悲观锁SELECT ... FOR UPDATE或者把库存拆成多段如 100 件拆成 10 段每段 10 件扣减时随机选一段降低单行冲突。另一种做法是引入 Redis 预扣减数据库只做最终落账但复杂度更高初期不建议。4.3 报表 SQL 跑几分钟拖垮整个数据库现象运营点一下“库存周转率”数据库 CPU 飙到 100%其他接口全部超时。原因报表 SQL 直接查流水表做聚合数据量大时全表扫描加排序和业务查询抢资源。解决报表走单独的只读从库或者预计算。常见做法是每天凌晨跑定时任务把周转率算好写入report_daily表前端查报表只读这张小表。如果必须实时至少给报表查询加超时和限流。4.4 盘点时锁全表业务全部卡死现象盘点功能一执行所有出入库接口都超时。原因盘点逻辑用了LOCK TABLES或者长事务把整张inventory表锁住。解决盘点不要锁表而是按商品分批处理每批一个短事务。盘点期间允许出入库用“盘点基准时间 期间流水”来修正结果。具体做法是记录盘点开始时间点盘点结束后把该时间点之后的流水重新应用到盘点结果上。4.5 数据库连接池被打满报 “too many connections”现象高峰期接口大量报错日志显示获取连接超时。原因慢 SQL 持有连接不释放或者事务方法里做了远程调用连接被长时间占用。解决先开慢查询日志找出耗时最长的 SQL通常是缺索引或报表查询。然后检查事务方法把远程调用、文件 IO 移出事务。连接池大小不是越大越好一般设为CPU核数 * 2 磁盘数配合合理的超时时间。5. 从能用到好用库存对账脚本与压测验证的具体做法前面把系统搭起来了但怎么证明它“敢用”我的习惯是两件事写一个对账脚本能随时验证库存快照和流水是否一致做一次并发压测看扣减在冲突下是否还正确。对账脚本的核心逻辑是用流水重算库存和快照比对。下面是一个可以直接跑的 SQL 版本适合数据量不大的场景-- 按商品仓库汇总流水得到理论库存 SELECT product_id, warehouse_id, SUM(change_qty) AS calc_qty FROM stock_record GROUP BY product_id, warehouse_id HAVING calc_qty 0; -- 与快照表比对找出不一致的记录 SELECT i.product_id, i.warehouse_id, i.available_qty i.locked_qty AS snapshot_qty, COALESCE(r.calc_qty, 0) AS record_qty FROM inventory i LEFT JOIN ( SELECT product_id, warehouse_id, SUM(change_qty) AS calc_qty FROM stock_record GROUP BY product_id, warehouse_id ) r ON i.product_id r.product_id AND i.warehouse_id r.warehouse_id WHERE i.available_qty i.locked_qty COALESCE(r.calc_qty, 0);逻辑说明第一条 SQL 先看流水本身是否平衡理论上所有变动加起来应该等于当前库存但如果流水从中间开始记录calc_qty不等于库存是正常的所以这条更多是辅助。第二条是真正的对账快照的available_qty locked_qty应该等于流水汇总。不一致的记录就是需要人工核查的。注意locked_qty也要算进去因为锁定也是一次流水变动。参数说明如果流水表数据量很大这个查询会很慢。生产环境建议按商品分批跑或者用定时任务增量对账。对账频率不用太高每天一次足够重点是发现差异后能定位到具体单号。压测验证更直接。用 JMeter 或 wrk 对扣减接口发并发请求比如 100 个线程同时扣同一商品初始库存 50预期结果是 50 次成功、50 次失败最终库存为 0且流水正好 50 条。如果最终库存是负数或者成功次数超过 50说明并发控制有漏洞。这个测试我每次改完扣减逻辑都会跑一遍比看代码可靠得多。最后一个习惯所有库存变动接口入参里必须带业务单号且日志里打印单号、商品、数量、变动前后值。出问题时一条grep就能拉出完整链路。库存系统不怕出问题怕的是出了问题查不到原因。希望帮到你。本文还有配套的精品资源点击获取