news 2026/9/23 3:29:54

Java+MySQL实现仓库存储管理系统:表结构、事务与部署实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java+MySQL实现仓库存储管理系统:表结构、事务与部署实战

简介:基于Java+MySQL的Web企业仓库存储管理系统,是一套面向高校课程设计或毕业设计的B/S架构完整项目。系统采用Spring Boot+Java+Maven+MyBatis搭建,覆盖客户、仓库、产品三类基本信息管理,以及入库、出库记录和库存管理,附带用户权限与系统日志功能,适用于仓库管理场景的入门学习与二次开发。资源包共221个文件,压缩后约6.95MB。其中42个Java源码文件构成业务逻辑主体,28个HTML与JS/CSS文件为前端管理界面,另含SQL数据库脚本、XML映射配置及1份说明文档;75个GIF演示图可直观展示各模块操作流程,辅助快速上手。资源已有568人学习浏览。完整的前后端代码、数据库初始化脚本与演示素材打包在一起,目录层级清晰,既能对照演示图理解业务流程,也可直接导入IDEA运行调试,是课程设计或仓库系统开发的有力参考。

1. 企业仓库存储管理系统:为什么 Java + MySQL 的 Web 方案不过时

仓库里堆着几百个 SKU,每天十几张入库单、出库单来回传,靠 Excel 台账硬撑,两个人同时改一个单元格,月底盘库对不上数几乎成了例行公事。基于 Java + MySQL 实现 Web 企业仓库存储管理系统,就是为了解决这个最朴素的问题:把入库、出库、库存查询、盘点和流水追查全部搬到浏览器里,谁在什么时间动了哪件货,数据库里一查便知。这套技术组合看着传统,却正是中小型仓库场景里稳定性最高、人才最好招、后续最好改的起步方案。

对正在做课设或毕设的 Java 学习者来说,这类系统覆盖了 Servlet、JDBC、事务、SQL 设计、部署上线几乎所有核心考点;对想低成本上信息化的运维人员来说,它不需要微服务,不需要分布式,一台普通服务器加一个 Tomcat 就能跑。下面按“数据模型先行、业务代码跟上、部署排障收尾”的节奏展开,尽量把每步的坑都填平。

2. 先立规矩:仓库系统的表结构设计与 MySQL 建库脚本

2.1 入出库的数据流:为什么先写流水再改库存

很多第一次做仓库系统的人会把库存当成唯一核心,写一个goods表,里面放一个qty字段,入库就qty + 1,出库就qty - 1。系统刚上线时挺好用,跑一个月就暴露问题了:某天库存对不上,你想查出是哪张单据导致的,发现数据库里只有当前数量,没有任何历史记录。想复盘,只能靠出库单纸质底根去翻,这等于把 Excel 台账又搬回数据库里。

常见的可靠做法是拆成“两张表”:一张stock只存当前数量,一张stock_flow存每一笔业务流水。入库时先查当前库存,写一条change_type=1的流水,再更新库存表的数量;出库则写change_type=2的流水,同时把库存减掉。核心原则是先写流水、再改库存,而且两个动作必须在同一个事务里完成,否则会出现“流水有了但库存没变”或者反过来库存变了却没记录的对账缺口。

盘点动作也走同一套逻辑,只是change_type=3,数量按“盘点实存数量 - 账面数量”的差值入账。这样任何时候想查“某件商品从入库到现在一共进出多少”,直接对stock_flow做聚合就行,不需要去翻历史快照。这个设计虽然多一张表、多一步操作,但它是整个系统能长期对得上账的地基。

2.2 核心表结构设计:用户表、商品表、库存表、流水表

以最小可用系统为例,四张表足够覆盖核心业务,再按需扩展。第一张是sys_user用户表,字段要包含用户名、密码、真实姓名和角色,角色用TINYINT区分管理员和库管员,比用字符串省空间也更好扩展。第二张是goods商品表,核心字段是商品编码、名称、规格、单位和库存预警下限,注意商品编码要加唯一约束,因为实际业务里编码才是业务主键。

第三张是stock库存表,goods_id与商品表一对一,用UNIQUE约束保证一个商品只有一行库存记录。这里有一个新手容易忽略的点:库存表不要放冗余的商品名称字段,查询时再去 JOIN 商品表,否则商品改名后库存表里的历史名称会变成脏数据。第四张是stock_flow流水表,字段包含唯一流水号、商品 ID、变动类型、变动数量、变动前数量、变动后数量、操作人 ID、备注和创建时间。

下面是这四张表的字段设计对比,照着建表前先想清楚每列的含义。

表名关键字段约束说明
sys_userusername, password, roleusername 唯一;role 用 TINYINT
goodscode, name, spec, unit, min_stockcode 唯一;min_stock 用于预警
stockgoods_id, qty, update_timegoods_id 唯一且外键引用 goods
stock_flowflow_no, goods_id, change_type, qty, before_qty, after_qtyflow_no 唯一;qty 一律存正数,方向靠 change_type 区分

两个容易踩的设计细节:一是流水表的qty字段不要存负数,入库出库都存正数,用change_type表达方向,否则以后做统计SUM(qty)时逻辑会绕;二是必须存before_qtyafter_qty两个快照值,这是以后核对“那笔操作前后库存到底是多少”的后悔药,没有这两个字段,事务回滚后很难从流水反推现场。

2.3 建库建表脚本:utf8mb4 与 InnoDB 的取舍

建库脚本直接决定后面排障的难易程度。字符集我一律用utf8mb4,不是utf8,这能兼容生僻字和特殊符号;存储引擎用InnoDB,因为要依赖它的事务能力,MyISAM 在这个场景下没有任何优势。下面是一份可以直接执行的 MySQL 8 建库建表脚本。

-- 建库:字符集必须用 utf8mb4,排序规则选 unicode_ci CREATE DATABASE IF NOT EXISTS depot_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE depot_db; CREATE TABLE sys_user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(30), role TINYINT NOT NULL DEFAULT 2 COMMENT '1管理员 2库管员', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE goods ( id INT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(30) NOT NULL UNIQUE COMMENT '商品编码,业务唯一', name VARCHAR(100) NOT NULL, spec VARCHAR(100), unit VARCHAR(10) DEFAULT '件', min_stock INT NOT NULL DEFAULT 0 COMMENT '库存预警下限', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE stock ( id INT AUTO_INCREMENT PRIMARY KEY, goods_id INT NOT NULL UNIQUE, qty INT NOT NULL DEFAULT 0, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_stock_goods FOREIGN KEY (goods_id) REFERENCES goods(id) ) ENGINE=InnoDB; CREATE TABLE stock_flow ( id BIGINT AUTO_INCREMENT PRIMARY KEY, flow_no VARCHAR(32) NOT NULL UNIQUE, goods_id INT NOT NULL, change_type TINYINT NOT NULL COMMENT '1入库 2出库 3盘点', qty INT NOT NULL, before_qty INT NOT NULL, after_qty INT NOT NULL, operator_id INT NOT NULL, remark VARCHAR(200), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_flow_goods FOREIGN KEY (goods_id) REFERENCES goods(id) ) ENGINE=InnoDB;

这份脚本里有几个参数值得单独说明。NOT NULL用在所有业务关键字段上,避免代码里因为NULL判断写出一堆分支;UNIQUE用在usernamecodeflow_no三个业务唯一键上,比在代码里先查再插更可靠;外键约束在这个规模下开着没问题,能防止删除商品时留下孤儿库存数据,但如果以后分库分表,外键就得去掉。数量字段全部用INT,别用VARCHAR存数字,MySQL 对字符串排序会按字典序排,9会排在10后面。如果系统涉及金额,记住DECIMAL(10,2),禁止用FLOAT

3. 用 Java JDBC 打通入库出库:事务代码与参数细节

3.1 Servlet + JDBC 的选型理由:课设与小型项目最稳的路径

标题只写了 Java + MySQL + Web,框架层面其实有很多选择:Spring Boot + MyBatis、Spring MVC + JPA、Servlet + JDBC。我一般会建议课程设计和快速落地的小型仓库系统优先考虑 Servlet + JDBC 这条朴素路径,原因不是它新,而是它把 Java Web 的核心链路全部暴露出来:请求怎么进 Servlet、SQL 怎么拼、事务怎么控制、连接怎么关。用 Spring Boot 虽然开发快,但很多初学者调一个依赖冲突就要折腾一晚上,反而不容易看清问题本质。

JDBC 的部分很多人觉得低级,其实它就是所有框架的地基。MyBatis 帮你生成的PreparedStatement、帮你管理的事务,本质和手写 JDBC 是一样的逻辑,只是封装了。如果你先把 JDBC 版本跑通,再去看 MyBatis 源码,很多“玄学”问题会一下想通。这一章就以 JDBC 为主,把入库、出库两个最核心的 Service 方法完整走一遍。

3.2 JdbcUtils 与连接池参数:先把数据库连接管好

写 JDBC 最容易翻车的位置就是连接管理:拿到连接不关、ResultSet不释放,跑半天后连接池被打满,系统假死。常见做法是封装一个JdbcUtils工具类,统一负责获取连接和关闭资源,连接池用 Druid 或 HikariCP 都行。下面这段是完整可用的工具类骨架。

public class JdbcUtils { private static DataSource dataSource; static { try (InputStream in = JdbcUtils.class.getClassLoader() .getResourceAsStream("jdbc.properties")) { Properties props = new Properties(); props.load(in); dataSource = DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt != null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } } }

关键点有两个。一是静态代码块里初始化数据源,类加载时只执行一次,避免每个请求都去读配置文件。二是关闭顺序必须从里往外:先关ResultSet,再关Statement,最后关Connection,顺序反了会导致某些数据库驱动报错。连接池的jdbc.properties放到src/main/resources下,驱动类名在 MySQL 8 里必须写com.mysql.cj.jdbc.Driver,这是很多人从 5.x 升级后翻车的头号原因,后面避坑章节还会专门讲。

3.3 入库出库的事务代码:先锁行,再更新库存,再写流水

入库操作的业务逻辑不复杂,但必须是一个原子操作。下面代码展示的是完整事务写法,不用@Transactional注解,手写commitrollback,这样能清楚看到事务边界在哪里。

public void inbound(StockFlowDTO dto) throws Exception { Connection conn = null; PreparedStatement psLock = null; PreparedStatement psUpdate = null; PreparedStatement psFlow = null; ResultSet rs = null; try { conn = JdbcUtils.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 锁定商品对应的库存行,防止并发下算错库存 psLock = conn.prepareStatement( "SELECT id, qty FROM stock WHERE goods_id = ? FOR UPDATE"); psLock.setInt(1, dto.getGoodsId()); rs = psLock.executeQuery(); int oldQty = 0; int stockId = 0; if (rs.next()) { stockId = rs.getInt("id"); oldQty = rs.getInt("qty"); } // 2. 更新库存表 int newQty = oldQty + dto.getQty(); psUpdate = conn.prepareStatement( "UPDATE stock SET qty = ? WHERE id = ?"); psUpdate.setInt(1, newQty); psUpdate.setInt(2, stockId); psUpdate.executeUpdate(); // 3. 写流水表 psFlow = conn.prepareStatement( "INSERT INTO stock_flow" + "(flow_no, goods_id, change_type, qty, before_qty, after_qty, operator_id, remark) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); psFlow.setString(1, generateFlowNo()); // 全局唯一流水号 psFlow.setInt(2, dto.getGoodsId()); psFlow.setInt(3, 1); // 1代表入库 psFlow.setInt(4, dto.getQty()); psFlow.setInt(5, oldQty); psFlow.setInt(6, newQty); psFlow.setInt(7, dto.getOperatorId()); psFlow.setString(8, dto.getRemark()); psFlow.executeUpdate(); conn.commit(); } catch (Exception e) { if (conn != null) conn.rollback(); throw e; } finally { if (conn != null) conn.setAutoCommit(true); JdbcUtils.close(conn, psUpdate, rs); try { if (psFlow != null) psFlow.close(); } catch (SQLException ignored) {} try { if (psLock != null) psLock.close(); } catch (SQLException ignored) {} } }

这段代码里最重要的不是UPDATE,而是FOR UPDATE这一句。它把stock表的对应行锁住,直到事务提交或回滚才释放,这样两个用户同时对同一商品入库时,第二个请求会等第一个提交后再读取,不会出现“两个人都读到库存是 100,分别加 10,最后库存变成 110 而不是 120”的经典并发问题。加锁会牺牲一点性能,但在仓库业务这种低频高正确的场景下非常值得。

generateFlowNo()需要你自己实现,我一般用yyyyMMddHHmmss + 三位随机数,或者直接用数据库主键替代。现实里更严谨的做法是独立一张流水号序列表,用SELECT ... FOR UPDATE取号,但这种小系统里时间戳加随机数已经够用。注意UPDATE stock之前一定要先SELECT ... FOR UPDATE拿到旧值,否则before_qtyafter_qty没数据可填,流水就失去了快照意义。

3.4 防止出库成负数:条件更新是最后一道防线

出库和入库的代码结构一样,唯一不同的是更新语句必须带上qty >= ?条件,这是防止库存扣成负数的最后一道防线。有些同学只在 Service 里先查一下数量,判断够不够,再执行更新,这在并发场景下是挡不住的:两个请求同时查出库存 10,都判断够出 8,然后一起执行更新,最后库存变成 -6。

// 出库:把 WHERE qty >= ? 作为第二道保险 psUpdate = conn.prepareStatement( "UPDATE stock SET qty = qty - ? WHERE id = ? AND qty >= ?"); psUpdate.setInt(1, dto.getQty()); psUpdate.setInt(2, stockId); psUpdate.setInt(3, dto.getQty()); int rows = psUpdate.executeUpdate(); if (rows == 0) { throw new BusinessException("库存不足,出库失败"); }

executeUpdate()返回 0 表示没有匹配的更新,也就是库存不足,这时候直接抛异常让事务回滚,流水也不会写进去。这里我再强调一次:判断库存是否充足不能只靠SELECT,要以UPDATE的影响行数为准,数据库的条件更新才是最可靠的判断。这种“先乐观尝试、失败再回滚”的思路,比先查再更新的 check-then-act 模式要结实得多。

登录和权限部分不用展开讲太多,做一个简单的Filter,检查session里有没有用户对象,没有就重定向到登录页,有就放行。仓库系统不像电商平台需要复杂权限模型,sys_user表里的role字段配合一套页面按钮的显隐控制,基本就够用了。

4. 部署上线:JDK、MySQL 连通性与 Tomcat 运行参数

4.1 环境准备:JDK 环境变量、MySQL 安装配置与 Tomcat 版本对照

代码写完只是第一步,真正让系统跑起来还要过部署这一关。先从基础环境说起:JDK 装好后,JAVA_HOME环境变量指向 JDK 根目录,PATH里加上%JAVA_HOME%\bin,在命令行执行java -version能输出版本号才算配好。这里有个容易踩的细节:如果系统里同时装了 JRE 和 JDK,PATH里排在前面的那个会被优先执行,导致javac命令找不到。

MySQL 的安装建议用 msyql-installer 或各系统的包管理工具,装完记得确认服务已启动。MySQL 8 默认的认证插件是caching_sha2_password,老版本驱动不支持时还会额外踩坑,后面避坑章节展开。Tomcat 版本要和 JDK 对应,Tomcat 9 对应 JDK 8 及以上,Tomcat 10 对应 JDK 11 及以上,而且 Tomcat 10 把包名从javax.*改成了jakarta.*,如果代码是按老规范写的,直接用 Tomcat 10 会编译报错。我一般稳妥选择 Tomcat 9。

4.2 jdbc.properties 的正确写法:那些一眼看不懂的参数

数据库连接配置是部署前最容易出错的地方,下面是一份经过实测可用的 MySQL 8 配置,每个参数我都标注了含义。

driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/depot_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username=root password=your_password initialSize=5 maxActive=20 maxWait=10000

serverTimezone=Asia/Shanghai是必须的,MySQL 8 不设时区时会报一个标准的时区错误,错误信息里全是一串乱码,第五章细说。useSSL=false是本地开发必备,不然连接时会有一串证书告警,甚至握手失败。allowPublicKeyRetrieval=true解决的是某些 MySQL 8 版本下连接报Public Key Retrieval is not allowed的问题;如果不用这个参数,也可以把密码认证改成mysql_native_passwordcharacterEncoding=utf8保证中文写入数据库不乱码,注意和建库时的utf8mb4不冲突,一个管连接层,一个管存储层。

连接池的三个参数initialSizemaxActivemaxWait里,maxWait是获取连接的最大等待毫秒数,设太短高峰期会大量报获取连接超时,设太长用户操作会卡死毫无反馈。仓库系统并发不高,maxWait=10000是常见取值,真到了不够用的时候先看监控再调。

4.3 从源码到 war 包:Tomcat 部署的完整命令链路

把项目打包成 war 包并部署到 Tomcat,整个过程可以全部用命令完成,不依赖 IDE 导出。先用 Maven 清理并打包,然后停掉 Tomcat,把旧包清掉,再拷贝新包启动。

# 1. 项目根目录执行,跳过测试能省几分钟 mvn clean package -DskipTests # 2. 把 war 拷贝到 Tomcat 的 webapps 目录 cp target/depot-web.war /opt/apache-tomcat-9/webapps/ # 3. 启动 Tomcat,catalina.out 里能看到启动日志 /opt/apache-tomcat-9/bin/startup.sh tail -f /opt/apache-tomcat-9/logs/catalina.out

部署完成后访问http://localhost:8080/depot-web/,Tomcat 会自动解压 war 包。如果访问报 404,先看webapps目录下有没有生成解压后的文件夹;如果报 403,多半是目录索引被禁用了或者 Web 应用没有欢迎页。这里有个经常被忽略的坑:重复部署时 Tomcat 可能因为 war 包更新而自动解压,但老版本会保留旧文件,多次迭代后容易混进陈旧 class,我习惯每次部署前手动删除解压目录,再启动。

4.4 生产环境的最小改动:端口、数据库账号与日志

开发环境跑通后,部署到服务器前最值得改的三件事是端口、数据库账号和日志级别。Tomcat 默认端口8080,想改就编辑conf/server.xml里的Connector portroot账号直接连生产库是安全大忌,建一个最小权限账号只授权depot_db库。日志级别至少把项目的日志框架设为INFO,不然出问题的时候抓不到细节。

<Connector port="8088" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

在这段配置里,connectionTimeout如果设太短,客户端慢网速下传个大文件就会被掐断;设太长则会被耗死线程。acceptCount是等待队列长度,小并发场景默认 100 够用。改完server.xml必须重启 Tomcat 才生效,这步常有人忘记,然后各种找问题,属于典型的人为操作遗漏。

5. 高频避坑:从时区报错到库存负数,五个必查位置

5.1 时区报错:一串乱码背后的真相

现象:启动项目后第一次访问数据库就抛异常,报错信息类似The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,后面还跟着一串看不懂的字符。没经验的人会以为编码坏了,其实这是 MySQL 8 和 JDBC 驱动对时区认知不一致导致的。

原因:MySQL 8 的时区默认取系统时区,而连接参数里没指定服务器时区,驱动不知道按哪个时区解释时间。解决方法是连接 URL 里显式加上serverTimezone=Asia/Shanghai。如果服务器本身时区是 UTC,也可以在 MySQL 里执行SET GLOBAL time_zone = '+08:00',但改代码里的 URL 是最快最不容易影响其他人的做法。改完记得重启应用,连接池里旧的连接不会自动拿到新参数。

5.2 驱动类名找不到:MySQL 8 的兼容性陷阱

现象:启动时抛ClassNotFoundException: com.mysql.jdbc.Driver,或者执行 SQL 时报Failed to load driver class

原因:MySQL 8 的驱动包把主类从com.mysql.jdbc.Driver改名成了com.mysql.cj.jdbc.Driver,旧类名在驱动包里已经不在了。网上很多老教程还在教 5.x 的写法,照着抄就容易中招。解决方法是把驱动配置改成com.mysql.cj.jdbc.Driver,同时确认pom.xml里的版本是 8.x。还有一个小坑:如果你的项目里同时存在老驱动和新驱动的依赖冲突,Maven 会挑一个加载,排查时直接看依赖树mvn dependency:tree,比瞎猜快得多。

5.3 中文乱码:三层字符集必须一致

现象:页面上传的商品名称,存到数据库里变成???,或者从数据库查出来到页面上是乱码。排查一圈发现字符集配置到处都对了,还是乱。

原因:字符集链路上有三个环节缺一不可。第一是数据库和表结构用utf8mb4;第二是 JDBC URL 里带characterEncoding=utf8;第三是 Web 层的请求和响应编码一致。只调其一没用,三个必须同时到位。我常用的做法是写一个CharacterEncodingFilter,强制所有请求和响应都用UTF-8,在web.xml里配置并让它最先执行,这样至少能保证入站参数不乱。如果还是乱,用SHOW CREATE TABLE goods;看表的 charset 是不是utf8mb4,很多老库建表时默认沿用了latin1,这种只能改表结构。

5.4 库存变负数:查不到原因时先看是不是并发

现象:系统用了一阵,某件商品的库存突然变成负数,但你翻流水记录发现每一笔出库当时库存都是够的。

原因:这是最典型的并发问题。两个出库请求同时读取库存为 10,都判断可以出 8,先后执行更新,结果就是先减后减,最终变成负数。解决要分两层:第一层是事务里用SELECT ... FOR UPDATE锁行;第二层是UPDATE stock SET qty = qty - ? WHERE id = ? AND qty >= ?做条件防御。前者保证读的时候不被别人改,后者保证写的时候不会越界。如果这两层都做了还出现负数,就去查应用日志里有没有异常后未回滚的连接,那种情况会让事务失效。

5.5 连接被拒:先分清 MySQL 还是防火墙的锅

现象:应用部署在服务器上,本地连不上数据库,报Communications link failureCan't connect to MySQL server。新手第一反应是改防火墙,但很多时候问题根本不在防火墙。

原因:常见的可能有三种:MySQL 服务没启动;监听地址是127.0.0.1,外部访问不到;账号权限不允许远程连接。排查顺序我一般这样走:先在本机执行mysql -u root -p确认服务正常;再用netstat -an | grep 3306看监听地址;最后用SHOW GRANTS FOR 'root'@'%';检查远程权限。都确认没问题再碰防火墙规则,否则容易把防火墙壁垒越改越松,系统安全性也一起下降了。

6. 进阶技巧:库存预警、流水追查与慢查询优化

6.1 库存预警:一条 SQL 找到要补货的商品

goods表加了min_stock字段后,库存预警就变得很简单。写一个定时任务,或者直接在库存查询页加一个“预警列表”按钮,执行下面这条 SQL,把当前库存低于下限的商品一次性捞出来。

SELECT g.code, g.name, s.qty, g.min_stock FROM stock s JOIN goods g ON s.goods_id = g.id WHERE s.qty <= g.min_stock ORDER BY (g.min_stock - s.qty) DESC;

核心逻辑是在查询阶段统一判断,不用在 Java 代码里遍历每条商品的库存再做 if 判断。如果预警数量需要频繁查,可以给min_stockqty的差值建一个虚拟列,以后直接按虚拟列排序,效果一样但 SQL 更清晰。

6.2 流水表越查越慢:复合索引怎么加

系统跑了大半年,流水表数据破百万后,按商品查历史进出明细明显变慢,页面转圈几秒钟。原因很直接:流水表的索引只有主键id和唯一键flow_no,按goods_id查时全表扫描。用EXPLAIN看一下执行计划,rows字段几十万,索引优化方向就很明确了。

ALTER TABLE stock_flow ADD INDEX idx_goods_time (goods_id, created_at);

加完索引后再用EXPLAIN验证,key列会从NULL变成idx_goods_time。这个复合索引既支持按商品查,也支持按商品加时间范围查,比单独给goods_id建一个索引更高效,因为查询条件里最常见的组合就是“某商品某段时间的流水”。注意索引不是越多越好,流水表的核心查询就这几个,够用即可,无脑加索引会让插入变慢,日志量大时会拖累整个系统。

这套系统我自己从头写过不止一次,最大的教训是:库存能对上账,比功能花哨重要得多。着一开始我也犯过“只更新库存不写流水”的错,月底盘库对不上时翻遍代码也找不到问题,后来老老实实把流水表补齐,对账才变成一件十分钟的事。希望这份从表设计到部署排障的实践笔记能帮到你,让你少走我走过的弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 3:29:32

0基础速通C语言:环境搭建、scanf/printf与const指针实战

简介&#xff1a;这是一份面向零基础编程初学者的C语言速通PDF教程&#xff0c;适合想快速入门C语言、系统梳理语法知识点的学生与自学者。内容精简实用、覆盖面广&#xff0c;从C语言的历史背景、优缺点讲起&#xff0c;逐步展开IDE选择建议、程序基本结构、头文件引入方式&am…

作者头像 李华
网站建设 2026/9/23 3:29:04

Dify私有化部署保姆级教程:手把手构建企业AI知识库

先聊个实在的&#xff1a;你把公司内部的制度文件、客户资料、项目文档&#xff0c;放进别人家的SaaS平台里&#xff0c;晚上真的睡得着吗&#xff1f;如果答案是“不太踏实”&#xff0c;那你大概率也需要一套跑在自己服务器上的AI知识库底座。Dify作为这两年开源圈里最活跃的…

作者头像 李华
网站建设 2026/9/23 3:28:23

Qt/C++德州扑克开发实战:状态机、牌型判断与信号槽避坑指南

简介&#xff1a;基于QTC开发的德州扑克游戏项目完整源码与配套文档&#xff0c;是面向计算机、通信、人工智能、自动化等专业学生及从业者的毕业设计级资料&#xff0c;适合作为期末课程设计、课程大作业或毕设参考。项目经过调试测试&#xff0c;核心机制可正常运行&#xff…

作者头像 李华
网站建设 2026/9/23 3:26:51

MySQL连接不上?从服务、连接到架构彻底搞懂这些基础

很多刚开始接触 MySQL 的人&#xff0c;都卡在同一个地方&#xff1a;明明按教程把 MySQL 装好了&#xff0c;密码也设置了&#xff0c;打开客户端工具却怎么也连不上。前段时间还有个朋友问我&#xff0c;说 Navicat 报 2002 错误&#xff0c;我让他先在终端里跑一句mysql -ur…

作者头像 李华
网站建设 2026/9/23 3:26:42

开题答辩全流程复盘:红色教育网站毕设的答辩技巧与问答指南

开题答辩这件事&#xff0c;很多同学把它当成一场“审判”&#xff0c;总觉得评委老师是来挑毛病的。我带过不少做红色教育网站这类毕设的学生&#xff0c;说实话&#xff0c;开题答辩更像一次“方案评审会”——老师真正想知道的不是“你做了多少”&#xff0c;而是“你有没有…

作者头像 李华