从零构建eclipse+MySQL超市管理系统:JDBC事务与排坑实战

发布时间:2026/10/9 6:09:31
从零构建eclipse+MySQL超市管理系统:JDBC事务与排坑实战
简介这份Java超市管理系统是一套基于Eclipse与MySQL的完整课程设计/毕业设计资源适合Java初学者、计算机专业学生及需要快速搭建进销存项目的开发者。压缩包共378个文件包含56个JSP页面、49个Java源文件、98个class编译文件、SQL数据库脚本、XML配置文件及前端CSS/JS等覆盖商品销售、采购管理、数据统计、管理员与收银员双角色权限等核心模块包体仅10.37MB结构紧凑便于导入学习。目前已有2384人学习下载可用作数据库设计与Servlet开发参考也可在此基础上扩展报表、优化界面或对接真实零售业务流程帮助理解JavaWeb项目从分层编码到部署运行的完整路径。1. 一个 eclipseMySQL 的 Java 超市管理系统听起来像课程设计实际是练 JDBC 功底的硬核项目你有没有见过那种“学习资料”里躺着的 Java 超市管理系统用 Eclipse 写界面用 MySQL 存数据没有 Spring Boot没有 MyBatis看起来土掉渣但它恰恰是理解 Java 数据库编程最扎实的起点。这个标题背后要解决的是“一个 Java 应用如何连上 MySQL做商品进销存和订单结算”常见于课程设计、毕业设计或者小团队内部工具。它能覆盖 JDBC 连接、DAO 分层、事务控制、库存扣减这些核心技能而且不用引入一堆依赖适合新手一步步跟熟手用来回顾底层原理也够味。别小看这个项目真把它做到能跑、不出乱码、不丢数据你已经比很多只会拷贝 Spring Boot demo 的人强了。为什么这么说因为它的难点不在“功能多”而在“细节密”。连接串拼错一个参数就报错日期时区不对就崩库存并发扣减更是让人头疼。接下来我会把整个项目从环境搭建到数据建模再到排错经验按实际做过的路径讲一遍你照着敲完就能交出一份“老师挑不出毛病”的作业或者一套能直接拿去用的内部小系统。2. 技术选型与环境搭建为什么现在还会用 eclipseMySQL 做管理系统2.1 选型理由这个组合适合什么场景不适合什么场景先说结论eclipseMySQL 的组合适合“一个人 3 天内做完并讲清楚原理”的小型管理信息系统尤其适合 Java 初学者理解 JDBC。它不适合高并发、大规模部署、复杂权限控制的企业级系统那种场景你会被繁琐的纯 JDBC 代码写死。但恰恰是这种“繁琐”逼迫你把 SQL、事务、连接管理这些基本功练扎实。很多人一上来就上 Spring Boot MyBatis Plus反而不知道一条 SQL 到底是怎么发到 MySQL 再返回来的。而用 eclipse 写 Java 项目用 MySQL 存储纯 JDBC 的每一步都是显式的加载驱动、获取连接、执行语句、处理结果集、关闭资源。你能看到全流程也能在翻车时快速定位是驱动问题、URL 问题还是 SQL 问题。这也是为什么很多高校和培训班的项目实战仍然保留这个组合。此外Eclipse 是免费开源的 IDE对老电脑友好MySQL 是使用率最高的开源数据库招聘要求里也几乎不会避开它。用这个组合做完一个超市管理系统你会顺手掌握 eclipse 导入项目、配置 build path、关联数据库插件这一套操作这些技能在你以后换 IDEA、换框架时照样复用。2.2 环境安装JDK、Eclipse、MySQL 的版本搭配和最小安装步骤先说版本搭配。JDK 用 8 或 11 都行Eclipse 用 2022 之后的版本即可MySQL 用 5.7 或 8.0。这里我建议直接上 MySQL 8.0因为 5.7 已经进入生命周期后期而且 8.0 的驱动和连接串在教程里更常见。如果你是新装环境按这个顺序来安装 JDK配置 JAVA_HOME 和 PATH。下载 Eclipse IDE for Java Developers解压即用。安装 MySQL Server选择 Server Only执行默认配置。下载并配置 JDBC 驱动 jar。这里有个关键点往下看。对于 JDK我一般用 Temurin 的安装包安装后打开命令行输入java -version看到版本号就说明成功。对于 MySQL安装时设置 root 密码最好用一个简单好记的比如root或123456本机学习没问题。但注意如果你的电脑已经装了 MySQL并且还启用了服务后续项目连接时要注意端口冲突常见的坑是 3306 被占用我们后面还会提到。至于 Eclipse新手容易在“安装插件”上浪费时间。实际上写 JDBC 项目不需要任何额外插件只需要把 MySQL 驱动 jar 放到项目的 classpath 里。常见的做法是在项目右键 - Build Path - Configure Build Path - Add External JARs选择驱动 jar 文件。这一步不做好代码里写再完美的Class.forName也没用。提示MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver连接 URL 中必须带serverTimezone参数否则会报时区异常。2.3 第一个 JDBC 连接程序让 Java 连上 MySQL 的最小代码很多人用 eclipse 建项目后上来就写业务代码结果第一步就卡在数据库连接上。下面这是一个最小可运行的连接测试代码我建议你单独建一个类先把数据库连通了再开发业务。import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBConnectionTest { public static void main(String[] args) { // JDBC URL 格式jdbc:mysql://主机:端口/数据库名?参数 String url jdbc:mysql://localhost:3306/supermarket?useSSLfalseserverTimezoneAsia/Shanghai; String user root; String password 123456; Connection conn null; try { // 1. 加载驱动MySQL 8.0 之后可省略但保留更稳妥 Class.forName(com.mysql.cj.jdbc.Driver); // 2. 获取连接 conn DriverManager.getConnection(url, user, password); // 3. 验证连接是否成功 if (conn ! null) { System.out.println(数据库连接成功); } } catch (ClassNotFoundException e) { System.out.println(找不到驱动类请检查 jar 是否导入); e.printStackTrace(); } catch (SQLException e) { System.out.println(连接失败请检查用户名密码和 URL); e.printStackTrace(); } finally { // 4. 关闭连接 try { if (conn ! null) { conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } } }代码里最重要的参数是 URL。jdbc:mysql://localhost:3306/supermarket前半段表示数据库地址supermarket是你的数据库名必须先在 MySQL 里建好。useSSLfalse是避免本机连接时的警告serverTimezoneAsia/Shanghai是 MySQL 8.0 必须要加的不然你会看到“The server time zone value йʱ is unrecognized”这种让人崩溃的报错。驱动加载的Class.forName在 JDK 高版本其实可以省略因为 DriverManager 能自动发现 jar 里的驱动。但保留它有一个好处如果你连驱动类都找不到它会在第一步就报ClassNotFoundException而不是在获取连接时才报这样排查问题更快。这就是为什么我建议新手别删掉这一行。还有一个容易被忽略的细节finally块里关闭资源。有人觉得程序就结束无所谓但如果你后面写的是 servlet 或长期运行的工具不关连接会把连接池耗尽。养成每个连接用完后都关闭的习惯血泪经验都是从忘了关闭连接开始的。3. 超市管理系统的核心设计数据库建模与 JDBC 代码分层3.1 数据库设计商品、库存、订单、用户四张核心表超市管理系统虽小但必须有清晰的数据模型。用 eclipseMySQL 做这个项目数据库设计往往是评分占比最高的部分同时也是后患最多的部分。我建议最少四张表user存登录账号product存商品基本信息stock存库存数量order存销售订单再加一张order_item存订单明细一共五张也完全合理。先看表结构设计表名字段类型说明userid, username, password, roleINT, VARCHAR, VARCHAR, VARCHAR用户表role 区分管理员和收银员productid, barcode, name, spec, price, unitINT, VARCHAR, VARCHAR, VARCHAR, DECIMAL(10,2), VARCHAR商品表barcode 最好设唯一索引stockid, product_id, quantity, min_quantityINT, INT, INT, INT库存表product_id 外键索引ordersid, order_no, user_id, total_amount, create_timeINT, VARCHAR, INT, DECIMAL(10,2), DATETIME订单主表order_itemid, order_id, product_id, price, quantityINT, INT, INT, DECIMAL(10,2), INT订单明细表注意order是 MySQL 的保留字建表时必须用反引号包起来或者直接叫orders。这里我用orders避免踩坑。还有unit也是保留字但它不是 MySQL 的关键字现代版本可以用不过为了安全最好改成unit_name。下面是建表 SQL可以直接在 MySQL Workbench 里执行CREATE DATABASE IF NOT EXISTS supermarket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE supermarket; CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role VARCHAR(20) DEFAULT admin ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE product ( id INT AUTO_INCREMENT PRIMARY KEY, barcode VARCHAR(20) NOT NULL, name VARCHAR(100) NOT NULL, spec VARCHAR(50), price DECIMAL(10,2) NOT NULL, unit_name VARCHAR(10) DEFAULT 个, UNIQUE KEY idx_barcode (barcode) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE stock ( id INT AUTO_INCREMENT PRIMARY KEY, product_id INT NOT NULL, quantity INT DEFAULT 0, min_quantity INT DEFAULT 10, FOREIGN KEY (product_id) REFERENCES product(id) ON DELETE CASCADE, UNIQUE KEY uk_product (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL, user_id INT NOT NULL, total_amount DECIMAL(10,2) DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_item ( id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, product_id INT NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE, FOREIGN KEY (product_id) REFERENCES product(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 SQL 里几个参数值得说清楚。ENGINEInnoDB必须用InnoDB 支持事务这是我们后面做扣库存时回滚的前提如果默认是 MyISAM事务就失效了。DEFAULT CHARSETutf8mb4是因为 MySQL 8.0 默认就是它但显式写出来可以防止某些旧环境下使用 latin1 导致的中文乱码。UNIQUE KEY idx_barcode保证同一个条码不能出现两次避免收银时数据歧义。FOREIGN KEY外键在这里不是必须的但加上可以让作业评分看到你学过约束。我建议你先执行这段 SQL然后用一条简单的 INSERT 插入测试数据确认能用 SELECT 查出来再开始写 Java 代码。不要一上来就写代码很多人的项目最后翻车都是因为数据库表结构被反复改动代码跟着返工。3.2 代码分层实体类、DAO、Service、UI 怎么拆很多初学者的“超市管理系统”是一坨代码把 SQL 写在窗口类里一个方法里既做查询又做界面弹窗。这种代码跑起来没问题但没法维护。更合理的分层是这样entity实体类对应数据库表dao数据访问对象只负责 SQL 操作service业务逻辑层处理事务和判断ui窗口组件只负责展示和接收输入这样的分层有什么实际好处你换界面的时候不碰 SQL你换数据库方言的时候不碰界面。对于 eclipseMySQL 这个级别不用引入 Spring手动 new 对象就能实现控制反转但结构上足够清晰。以商品管理为例先定义一个商品实体类package entity; import java.math.BigDecimal; public class Product { private int id; private String barcode; private String name; private String spec; private BigDecimal price; private String unitName; // 每个字段都要 getter 和 setter public int getId() { return id; } public void setId(int id) { this.id id; } public String getBarcode() { return barcode; } public void setBarcode(String barcode) { this.barcode barcode; } public String getName() { return name; } public void setName(String name) { this.name name; } public String getSpec() { return spec; } public void setSpec(String spec) { this.spec spec; } public BigDecimal getPrice() { return price; } public void setPrice(BigDecimal price) { this.price price; } public String getUnitName() { return unitName; } public void setUnitName(String unitName) { this.unitName unitName; } }然后写一个 DAO 类只负责对product表的增删改查package dao; import java.math.BigDecimal; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import entity.Product; public class ProductDao { private String url jdbc:mysql://localhost:3306/supermarket?useSSLfalseserverTimezoneAsia/Shanghai; private String user root; private String password 123456; public boolean insert(Product p) { String sql INSERT INTO product (barcode, name, spec, price, unit_name) VALUES (?,?,?,?,?); try (Connection conn DriverManager.getConnection(url, user, password); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, p.getBarcode()); ps.setString(2, p.getName()); ps.setString(3, p.getSpec()); ps.setBigDecimal(4, p.getPrice()); ps.setString(5, p.getUnitName()); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } } public Product findByBarcode(String barcode) { String sql SELECT * FROM product WHERE barcode ?; try (Connection conn DriverManager.getConnection(url, user, password); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, barcode); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { Product p new Product(); p.setId(rs.getInt(id)); p.setBarcode(rs.getString(barcode)); p.setName(rs.getString(name)); p.setSpec(rs.getString(spec)); p.setPrice(rs.getBigDecimal(price)); p.setUnitName(rs.getString(unit_name)); return p; } } } catch (SQLException e) { e.printStackTrace(); } return null; } public boolean updatePrice(int id, BigDecimal price) { String sql UPDATE product SET price ? WHERE id ?; try (Connection conn DriverManager.getConnection(url, user, password); PreparedStatement ps conn.prepareStatement(sql)) { ps.setBigDecimal(1, price); ps.setInt(2, id); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } } }这里用到了 try-with-resourceseclipse 会自动提示你Connection和PreparedStatement已经实现了AutoCloseable所以 try 块结束后会自动关闭资源不用手写 finally。这是 JDK 7 以后推荐的写法比手动关闭更不容易漏。参数方面setBigDecimal就是把 Java 里BigDecimal映射到数据库的DECIMAL价格计算必须用这个类型不能图省事用double否则在小数计算时会得到 19.99 变成 20.000000000000004 这种尴尬结果。注意看findByBarcode用了PreparedStatement而不是Statement。这是关键习惯第一它会在 MySQL 服务端预编译相同结构的 SQL 第二次执行更快第二参数用占位符?绑定能堵住 SQL 注入。你可能觉得自己做的内部系统没人攻击但作业检查时有没有用 PreparedStatement 是一条明确的扣分点面试时也几乎必问。3.3 业务核心销售下单与库存扣减的事务控制超市管理系统的核心是“卖一件货库存就要减一同时生成订单”。如果只是简单地把 INSERT 订单和 UPDATE 库存分开执行就会出现一个灾难订单写进去了但库存没减或者反过来库存减了订单没生成。解决这个问题必须用事务。先看业务逻辑代码这里我用一个SaleService来模拟收银台一键下单package service; import java.math.BigDecimal; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Random; public class SaleService { private String url jdbc:mysql://localhost:3306/supermarket?useSSLfalseserverTimezoneAsia/Shanghai; private String user root; private String password 123456; public boolean saleOrder(int productId, int quantity, int userId) { Connection conn null; try { conn DriverManager.getConnection(url, user, password); // 关键脏关闭自动提交事务开始 conn.setAutoCommit(false); String orderNo SO System.currentTimeMillis() new Random().nextInt(1000); // 1. 插入订单主表 String sqlOrder INSERT INTO orders (order_no, user_id, total_amount) VALUES (?,?,?); PreparedStatement psOrder conn.prepareStatement(sqlOrder, PreparedStatement.RETURN_GENERATED_KEYS); psOrder.setString(1, orderNo); psOrder.setInt(2, userId); psOrder.setBigDecimal(3, new BigDecimal(0)); // 先占位后面再算 int r1 psOrder.executeUpdate(); int orderId 0; try (ResultSet rs psOrder.getGeneratedKeys()) { if (rs.next()) orderId rs.getInt(1); } psOrder.close(); // 2. 查询商品价格 String sqlPrice SELECT price FROM product WHERE id ?; PreparedStatement psPrice conn.prepareStatement(sqlPrice); psPrice.setInt(1, productId); ResultSet rsPrice psPrice.executeQuery(); BigDecimal price BigDecimal.ZERO; if (rsPrice.next()) { price rsPrice.getBigDecimal(price); } rsPrice.close(); psPrice.close(); // 3. 插入明细 String sqlItem INSERT INTO order_item (order_id, product_id, price, quantity) VALUES (?,?,?,?); PreparedStatement psItem conn.prepareStatement(sqlItem); psItem.setInt(1, orderId); psItem.setInt(2, productId); psItem.setBigDecimal(3, price); psItem.setInt(4, quantity); int r2 psItem.executeUpdate(); psItem.close(); // 4. 扣减库存 String sqlStock UPDATE stock SET quantity quantity - ? WHERE product_id ?; PreparedStatement psStock conn.prepareStatement(sqlStock); psStock.setInt(1, quantity); psStock.setInt(2, productId); int r3 psStock.executeUpdate(); psStock.close(); // 5. 更新订单总额 BigDecimal total price.multiply(new BigDecimal(quantity)); String sqlTotal UPDATE orders SET total_amount ? WHERE id ?; PreparedStatement psTotal conn.prepareStatement(sqlTotal); psTotal.setBigDecimal(1, total); psTotal.setInt(2, orderId); psTotal.executeUpdate(); psTotal.close(); // 全部成功才提交 conn.commit(); return r1 0 r2 0 r3 0; } catch (SQLException e) { e.printStackTrace(); // 出错回滚不留半截数据 try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } return false; } finally { try { if (conn ! null) { conn.setAutoCommit(true); // 恢复默认 conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } } }这里重点是conn.setAutoCommit(false)从这一行开始后面所有 SQL 都暂不生效直到conn.commit()一次性生效。如果在任何步骤抛异常catch块里的conn.rollback()能把前面已执行的 INSERT 和 UPDATE 全部撤销。这就是事务的原子性。有人会问order_no为什么要拼时间戳和随机数因为订单号一般要保证唯一数据库里也做了唯一索引。如果直接用一个自增 id 当订单号别人一眼能看出你的业务量而且不好看。这里生成的SO 时间戳 随机数基本不会重复但你在实际做时要注意如果多个线程同时调用Random.next()可能会碰撞所以更稳妥的方法是使用UUID.randomUUID().toString().replace(-, )的前几位或者直接用数据库自带的主键。getGeneratedKeys也是常用技巧它可以拿到自增主键的值方便把order_id插到明细表里。如果你在 MySQL 里没有定义 AUTO_INCREMENT这一步会返回空结果对应的orderId就是 0后续明细表外键就错了。所以建表时的自增主键很重要。4. eclipseMySQL 项目常见坑5 个我差点把电脑砸了的报错与解决办法4.1 eclipse 里“找不到或无法加载主类 org.apache.catalina.startup.Bootstrap”这是很多人的噩梦尤其当你把项目从网上导入时明明看着是 Java 项目一运行却报这个错。现象是控制台提示“错误: 找不到或无法加载主类 org.apache.catalina.startup.Bootstrap”但你的代码里根本没有这个类。原因通常是你导入的项目原本是 Dynamic Web Project依赖了 Tomcat 运行时而当前工作区的 Tomcat 服务器没有配置或者版本不匹配eclipse 在清理/重新编译后无法找到启动类。另一个常见原因是 build path 中 classpath 损坏或项目根目录不对。解决办法分两步先检查项目是不是 Java 项目右键项目 - Properties - Project Facets取消勾选比如 Dynamic Web Module 或者重新指定 Tomcat 运行时如果你只是做控制台项目最简单的是新建一个普通 Java 项目把源码和 jar 手动拷贝进去而不是直接 import 旧项目。另一个方法是把当前 Tomcat 从 Server 视图里删掉重新添加并且检查 Java Build Path 中的 Libraries 里有没有把 servlet-api.jar 包含进来。这个报错和 MySQL 无关但它特别容易在“网上下载的 javaweb 项目”里出现所以单独列出来。4.2 MySQL 连接时“Public Key Retrieval is not allowed”这是一个很新又很烦人的报错MySQL 8.0 默认使用 caching_sha2_password 认证JDBC 驱动第一次连接时需要在非 SSL 情况下获取公钥但默认 URL 参数没允许。现象是一连数据库就抛“Public Key Retrieval is not allowed”但你在命令行用相同用户密码又能登录。原因是在 JDBC URL 里缺少allowPublicKeyRetrievaltrue。解决办法很简单把连接串改成jdbc:mysql://localhost:3306/supermarket?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue这里的allowPublicKeyRetrieval是允许客户端向服务器请求公钥在开发环境完全可以开。如果你对安全性有执念可以改用 SSL但本机调试没必要。注意这个参数只对 MySQL 8.0 有效5.7 不会报这个错所以如果你是用 5.7 的驱动连 8.0 的库也会出现奇奇怪怪的兼容问题。我建议你直接下载 mysql-connector-j 8.0.33 之类的通用驱动避免老驱动连新版库。4.3 保存中文变乱码查询结果全是“”乱码问题在中文项目里几乎是必踩的坑。现象有多种界面上输入“苹果”存入 MySQL 变成“??”直接查询表数据显示正常但 Java 控制台输出乱码或者反过来。原因通常是三层编码不一致数据库表字符集、JDBC 连接字符集、Java 文件本身的编码。我的做法是统一三层全部使用 UTF-8。第一步建表时指定 DEFAULT CHARSETutf8mb4前面已经做了。第二步在 JDBC URL 里加characterEncodingutf8比如jdbc:mysql://localhost:3306/supermarket?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8第三步在 eclipse 里设置项目编码右键项目 - Properties - Resource - Text file encoding改成 UTF-8。第四步如果还有什么地方不对在连接后执行SET NAMES utf8mb4强制指定客户端字符集。记住乱码不是一处错的是链路中每一处都要设为 UTF-8缺一个环节就乱。4.4 UPDATE 或 DELETE 操作返回受影响行数为 0但好像没生效有同学反馈“我明明执行了 UPDATE product SET price19.99 WHERE id1返回 1但再查还是原价。”这时先检查事务是否提交。如果在代码里手动调用了conn.setAutoCommit(false)但执行完executeUpdate()后没有调用conn.commit()那么当前连接下自己查可能看不到结果因为默认隔离级别下未提交变更对其他连接不可见。另一个更隐蔽的原因是你操作之后没有关闭连接而同一个连接又是从连接池里复用的把未提交的事务带到下次操作产生幻觉。解决方法是执行更新后立刻conn.commit()或者干脆不要在这条语句之前关掉自动提交。还有一个类似情况executeUpdate()返回值代表受影响的行数如果你的数据库里数据本身没变化比如你把价格从 19.99 改成 19.99MySQL 返回的是 0匹配行数不为 0但受影响行数因值未变为 0。这不是 bug但如果你的代码用返回值判断“是否更新成功”就会误判。正确做法是使用executeUpdate()返回者 检查 WHERE 条件是否符合预期而不是依赖返回值 0。4.5 库存卖到负数销售单却照常生成这个坑不是报错但比报错更严重。现象是高并发下多个线程同时卖同一件商品库存被扣到负数订单照样生成。原因是我的 UPDATE 语句是UPDATE stock SET quantity quantity - ? WHERE product_id ?但没有带库存足够的条件。解决办法是在 SQL 层面加一个约束UPDATE stock SET quantity quantity - ? WHERE product_id ? AND quantity ?这条语句利用 MySQL 行级锁和条件判断当库存不足时匹配行为 0executeUpdate()返回 0代码里就能拿到“库存不足”的信号。然后在 Java 里判断返回值如果为 0 就回滚并提示前台。这个方法比先 SELECT 再判断再 UPDATE 更可靠因为两个请求同时读到库存 5先执行 UPDATE 的会先拿到锁后执行的会因为条件不满足直接返回 0不会出现都读到 5 然后都减到 4 的“幻影扣减”。如果你嫌这个条件写法可读性差也可以改成先 SELECT 库存然后带版本号更新乐观锁但这对于简单的课程项目有点过度设计。用条件 UPDATE 已经能解决单机并发问题。注意如果不开启事务条件 UPDATE 也是每个命令自成一个事务最终还是可能把两条并发更新串行化所以事务依然要保留。5. 最后的一公里从“能跑通”到“能答辩”——验证技巧与两个保命习惯项目做完不是终点你要能验证它没有隐性问题也要能在别人问你“为什么这么写”时答得上来。我强烈建议你给自己写一个“自毁测试”什么意思故意在销售下单时把库存改成 0看看系统会不会报“库存不足”而不是生成空订单再故意把一条 SQL 写错观察日志里有没有回滚。这套验证比多写十个功能都值钱。另一个实用技巧是给 DAO 层加上带参数调整的查询方法。比如按商品名称模糊查询这个功能在超市管理里几乎必用代码也很简单public ListProduct searchByName(String keyword) { String sql SELECT * FROM product WHERE name LIKE ?; try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); try (ResultSet rs ps.executeQuery()) { ListProduct list new ArrayListProduct(); while (rs.next()) { list.add(mapRow(rs)); } return list; } } catch (SQLException e) { e.printStackTrace(); return new ArrayListProduct(); } }这段代码里把%拼进参数而不是拼进 SQL既是一个好习惯也能让你顺手复习LIKE的模糊配匹。如果你用的是Statement直接拼接字符串遇到名字里带单引号会立刻报错这就是 SQL 注入的入门场景。再有一个保命习惯不要用new BigDecimal(String)之外的方式初始化金额。如果你在代码里写new BigDecimal(19.99)你会得到一个近似小数这是新手最容易忽视的黑匣子。正确做法是new BigDecimal(19.99)。我当年就因为用 double 算价格导致系统里出现 19.990000000000002查到凌晨才找到根因。后来我给自己定了个规矩凡是涉及钱的地方一律用 String 构造函数初始化 BigDecimal。最后把项目里所有硬编码的数据库连接串抽到一个单独的工具类DbUtil里这是你未来迁移连接池、改配置的唯一入口。我做这个项目时一开始图省事每个 DAO 都抄了一份连接参数后来要换成带连接池的连接方式时改了二十个文件。从那以后我养成了先写工具类的习惯。这个 eclipseMySQL 的超市管理系统说小也小说深也深。你如果老老实实把它从建库到事务写完再去掉几个坑你会发现后面学 MyBatis 也好学 Spring 也罢那些连不上数据库、乱码、事务不回滚的问题都会很容易举一反三。希望帮到你。本文还有配套的精品资源点击获取