Java+MySQL图书管理系统:事务一致性与并发控制实战
2026/9/13 19:56:57 网站建设 项目流程

简介:这是一份面向Java Web初学者与毕业设计学生的完整图书管理系统实战项目资源,基于Java+MySQL技术栈,解决高校图书馆、小型书屋或个人藏书的数字化管理需求。资源包含212个文件,主体为31个核心Java源码、112个编译后Class文件(如BookAddIFrame.class、BookBorrowIFrame.class等体现MVC分层结构的界面与业务类)、52张界面截图(jpg)及MySQL数据库文件(db、mdf),整体压缩包仅3.69MB,轻量易部署。已有84人学习下载,适合快速上手Servlet+JSP+JDBC开发流程。读者可直接导入Eclipse/IDEA,在Tomcat中运行,完整复现含用户权限分级(管理员/读者)、图书CRUD、借阅归还、统计查询等功能的Web应用,并通过源码深入理解MVC模式落地、DAO层封装、事务控制及异常处理机制。

1. 这不是又一个“增删改查”Demo:用Java+MySQL落地真实可用的图书管理系统,毕设答辩前必须绕开的3类典型失分点

很多同学拿到“图书管理系统的设计与实现(Java+MySQL)”这个毕设题目时,第一反应是:不就是Swing窗体+JDBC连个数据库,写几条SQL做借阅记录?结果答辩现场被老师一句“用户并发借书时怎么保证库存不超发?”问住,或者演示时连续点击“借出”按钮导致同一本书被借走三次——系统当场崩掉。这暴露了本质问题:它不是教学示例,而是对事务一致性、边界校验、数据建模合理性的综合检验。本项目面向计算机专业本科毕业设计场景,核心目标是交付一套能通过功能测试、具备基础健壮性、代码结构清晰可维护的本地部署系统。它不追求Spring Boot微服务架构或Vue前端,但必须让MySQL真正承担起数据守门人角色,让Java层逻辑经得起多线程模拟压力。下面将从数据库设计底层逻辑出发,手把手构建可运行、可验证、可答辩的最小可行系统。

2. 用ER图锚定业务边界:为什么Book和BorrowRecord之间必须是“一对多”,而User表不能直接存密码明文

2.1 图书管理的核心实体关系必须反映真实借阅流程

图书管理系统绝非简单的“书名+作者+库存”三字段堆砌。真实场景中,一本《深入理解Java虚拟机》可能有5本馆藏副本,每本都有独立编号(barcode),其中3本已被借出,2本在架。若将库存数(stock)作为Book表字段,当多个用户同时发起借阅请求时,极易因读-改-写竞争导致超借(如库存为1,两个线程同时读到1,各自减1后都写回0)。因此,必须拆分实体:Book表描述图书元数据(ISBN、书名、作者、出版社),BookCopy表描述物理副本(id、book_id、barcode、status),BorrowRecord表记录每次借阅行为(id、user_id、copy_id、borrow_date、return_date)。这种设计天然支持“一书多册、一册一借、一借一还”的原子操作,避免库存字段成为并发瓶颈。

2.2 MySQL建表脚本必须显式声明约束与索引

以下为关键表创建语句,每行约束均有明确业务含义:

-- 用户表:密码必须加盐哈希,禁止明文存储 CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '登录账号,唯一', `password_hash` CHAR(64) NOT NULL COMMENT 'SHA-256哈希值,加盐处理', `real_name` VARCHAR(30) NOT NULL, `role` ENUM('admin','student','teacher') DEFAULT 'student' COMMENT '权限角色', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 图书元数据表 CREATE TABLE `book` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `isbn` VARCHAR(17) NOT NULL UNIQUE COMMENT '国际标准书号,带分隔符', `title` VARCHAR(200) NOT NULL, `author` VARCHAR(100) NOT NULL, `publisher` VARCHAR(100), `publish_year` YEAR, `category` VARCHAR(50) COMMENT '如:计算机/文学/历史' ); -- 图书副本表:每本物理书对应一条记录 CREATE TABLE `book_copy` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `book_id` INT NOT NULL, `barcode` VARCHAR(20) NOT NULL UNIQUE COMMENT '图书馆条形码,全局唯一', `status` ENUM('available','borrowed','lost','damaged') DEFAULT 'available', `acquired_date` DATE COMMENT '入馆日期', FOREIGN KEY (`book_id`) REFERENCES `book`(`id`) ON DELETE CASCADE ); -- 借阅记录表:核心业务流水 CREATE TABLE `borrow_record` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` INT NOT NULL, `copy_id` INT NOT NULL, `borrow_date` DATETIME DEFAULT CURRENT_TIMESTAMP, `due_date` DATE NOT NULL COMMENT '应还日期,通常为borrow_date+30天', `return_date` DATETIME NULL COMMENT '实际归还时间,NULL表示未还', `fine_amount` DECIMAL(8,2) DEFAULT 0.00 COMMENT '逾期罚款金额', FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT, FOREIGN KEY (`copy_id`) REFERENCES `book_copy`(`id`) ON DELETE RESTRICT, INDEX `idx_user_borrow` (`user_id`, `borrow_date`), INDEX `idx_copy_status` (`copy_id`, `return_date`) );

提示:ON DELETE RESTRICT 是关键安全阀
borrow_record表中,对copy_id使用ON DELETE RESTRICT而非CASCADE,确保物理副本被误删时,系统会抛出外键约束异常而非静默删除历史借阅记录。这是答辩时体现数据治理意识的细节。

2.3 为什么BookCopy表的status字段必须用ENUM而非INT

ENUM('available','borrowed','lost','damaged')替代TINYINT看似多占字节,实则带来三重收益:

  1. 业务语义自解释:代码中直接写copy.setStatus("borrowed")copy.setStatus(2)更易懂;
  2. 数据库层校验:插入status='sold'会报错,杜绝非法状态;
  3. SQL查询可读性SELECT * FROM book_copy WHERE status = 'available'WHERE status = 1更符合自然语言思维。
    此设计在毕设答辩中常被忽略,却是评审老师考察“是否理解业务与数据映射”的隐性指标。

3. Java层事务控制:用JDBC原生API实现“借书”操作的原子性,拒绝简单try-catch包裹SQL

3.1 “借书”业务的完整事务边界必须覆盖三步操作

用户点击“借阅”按钮时,系统需在单个数据库事务内完成:
① 校验该副本当前状态是否为available
② 将book_copy.status更新为borrowed
③ 向borrow_record插入新记录。
任何一步失败,全部回滚。若用三层if嵌套判断再执行SQL,必然出现状态检查通过但更新失败的中间态(如网络中断),导致数据不一致。

3.2 使用JDBC手动事务的最小可靠实现

以下为BorrowService.java核心方法,严格遵循ACID原则:

public class BorrowService { private final Connection connection; public BorrowService(Connection connection) { this.connection = connection; } /** * 借书操作:返回true表示成功,false表示失败(含原因已记录日志) */ public boolean borrowBook(int userId, int copyId) { String sqlCheck = "SELECT status FROM book_copy WHERE id = ? FOR UPDATE"; String sqlUpdate = "UPDATE book_copy SET status = 'borrowed' WHERE id = ? AND status = 'available'"; String sqlInsert = "INSERT INTO borrow_record (user_id, copy_id, due_date) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 30 DAY))"; try { connection.setAutoCommit(false); // 关闭自动提交 // 步骤1:加行锁查询副本状态(FOR UPDATE防止并发修改) PreparedStatement psCheck = connection.prepareStatement(sqlCheck); psCheck.setInt(1, copyId); ResultSet rs = psCheck.executeQuery(); if (!rs.next()) { throw new RuntimeException("副本ID不存在: " + copyId); } String currentStatus = rs.getString("status"); if (!"available".equals(currentStatus)) { throw new RuntimeException("副本不可借阅,当前状态: " + currentStatus); } // 步骤2:条件更新,确保仅当状态仍为available时才修改 PreparedStatement psUpdate = connection.prepareStatement(sqlUpdate); psUpdate.setInt(1, copyId); int updateCount = psUpdate.executeUpdate(); if (updateCount != 1) { throw new RuntimeException("并发冲突:副本状态已被其他事务修改"); } // 步骤3:插入借阅记录 PreparedStatement psInsert = connection.prepareStatement(sqlInsert); psInsert.setInt(1, userId); psInsert.setInt(2, copyId); psInsert.executeUpdate(); connection.commit(); // 全部成功,提交事务 return true; } catch (SQLException e) { try { connection.rollback(); // 异常时回滚 } catch (SQLException rollbackEx) { System.err.println("回滚失败: " + rollbackEx.getMessage()); } System.err.println("借书失败: " + e.getMessage()); return false; } finally { try { connection.setAutoCommit(true); // 恢复自动提交 } catch (SQLException e) { System.err.println("恢复自动提交失败: " + e.getMessage()); } } } }

注意:FOR UPDATE与条件更新的双重保险
SELECT ... FOR UPDATE在InnoDB中会对选中行加写锁,阻止其他事务修改;而UPDATE ... WHERE status = 'available'的条件更新确保即使锁失效(极小概率),也能通过WHERE子句拦截非法状态变更。两者结合构成高可靠事务屏障。

3.3 连接池配置与事务传播的常见误区

许多毕设项目直接new Connection(),导致连接无法复用且事务隔离失效。必须使用HikariCP等轻量连接池,并在DataSource初始化时设置:

HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/library?useSSL=false&serverTimezone=Asia/Shanghai"); config.setUsername("root"); config.setPassword("123456"); config.setMaximumPoolSize(10); // 毕设场景10足够,避免MySQL连接数超限 config.setConnectionTimeout(3000); // 3秒超时,防死锁 config.addDataSourceProperty("cachePrepStmts", "true"); // 开启预编译语句缓存

提示:MySQL时区参数必须显式声明
若不加serverTimezone=Asia/Shanghai,Java的NOW()函数可能返回UTC时间,导致due_date计算错误。这是答辩高频扣分点。

4. 毕设答辩必验场景:用JMeter模拟100用户并发借书,验证库存一致性与错误率

4.1 构建可重复的压力测试脚本

单纯功能测试无法暴露并发缺陷。需用JMeter创建线程组,模拟真实用户行为:

  • 线程数:100(代表100学生同时抢热门教材)
  • Ramp-Up Period:10秒(渐进加压,避免瞬间打垮)
  • 循环次数:1次(每个用户只借1本书)
  • HTTP请求:POST/borrow?userId=101&copyId=5001

关键在于copyId必须动态化:在JMeter中添加“CSV Data Set Config”,准备copies.csv文件,内容为100个不同且状态为available的副本ID,确保测试覆盖多副本竞争。

4.2 MySQL端实时监控关键指标

在压力测试运行时,登录MySQL执行以下命令,观察事务健康度:

-- 查看当前正在运行的事务(重点关注state为'LOCK WAIT'的阻塞事务) SELECT trx_id, trx_state, trx_started, trx_wait_started, trx_mysql_thread_id, trx_query FROM information_schema.INNODB_TRX WHERE trx_state = 'LOCK WAIT' OR trx_state = 'RUNNING'; -- 查看锁等待关系(定位谁在等谁) SELECT r.trx_id waiting_trx_id, r.trx_mysql_thread_id waiting_thread, r.trx_query waiting_query, b.trx_id blocking_trx_id, b.trx_mysql_thread_id blocking_thread, b.trx_query blocking_query FROM information_schema.INNODB_LOCK_WAITS w INNER JOIN information_schema.INNODB_TRX b ON b.trx_id = w.blocking_trx_id INNER JOIN information_schema.INNODB_TRX r ON r.trx_id = w.requesting_trx_id; -- 统计book_copy表中各状态数量(验证是否出现负库存或非法状态) SELECT status, COUNT(*) FROM book_copy GROUP BY status;

4.3 压力测试结果解读与优化阈值

理想结果应满足:

指标合格阈值不合格表现
错误率≤ 0.5%JMeter报告中Error% > 1%,说明事务回滚频繁
90%响应时间≤ 800ms大量请求超时,可能因锁等待过长
book_copy.status分布available数量 ≥ 0,无null或非法值出现status='borrowed'但无对应borrow_record,表明事务未完整提交

若发现LOCK WAIT事务堆积,需检查:

  • 是否遗漏FOR UPDATE(导致间隙锁缺失);
  • borrow_record表索引是否缺失(idx_copy_status缺失会导致全表扫描加锁);
  • MySQL的innodb_lock_wait_timeout是否过短(默认50秒,毕设可调至10秒快速暴露问题)。

5. 毕设代码交付清单与答辩话术:如何用3分钟讲清技术选型依据与数据一致性保障

5.1 必须打包的6类文件及其答辩解释要点

毕设压缩包命名library-system-java-mysql-2024,内部结构需包含:

文件路径作用答辩话术要点
/docs/ER-Diagram.png实体关系图“采用三范式设计,Book与BookCopy分离解决一书多册并发问题,BorrowRecord作为事实表承载业务流水”
/sql/init_db.sql数据库初始化脚本“所有表均定义NOT NULL、UNIQUE、ENUM约束,外键使用RESTRICT防止级联误删”
/src/main/java/com/example/library/dao/DAO层代码“每个DAO方法对应单一SQL,避免复杂逻辑,便于单元测试”
/src/main/java/com/example/library/service/BorrowService.java核心业务类“借书操作使用JDBC手动事务,FOR UPDATE加锁+条件更新双重保障,确保100并发下零超借”
/test/java/BorrowServiceTest.javaJUnit测试用例“覆盖正常借阅、重复借阅、无效副本ID三种场景,断言数据库状态变更”
/README.md部署说明“仅需JDK8+MySQL5.7,双击run.bat启动,无需额外中间件,符合本科毕设技术栈要求”

5.2 面对“为什么不用Spring Boot?”的标准化应答

评审老师若质疑技术选型,切忌说“不会用”或“太复杂”。应聚焦教育目标与可控性:

“Spring Boot虽能简化配置,但其自动事务管理(@Transactional)会掩盖底层锁机制原理。本次毕设要求深入理解ACID实现,因此选择JDBC原生API——通过显式setAutoCommit(false)、commit()、rollback(),配合FOR UPDATE和条件更新,让学生亲手构建事务边界。这比调用一个注解更能体现对数据库并发控制的本质掌握。”

5.3 演示环节的3个必做动作与话术钩子

答辩演示时,按顺序执行以下操作,每步配一句技术点说明:

  1. 打开MySQL Workbench,执行SELECT status, COUNT(*) FROM book_copy GROUP BY status;
    → “当前系统有200本可借图书,0本异常状态,数据完整性基线正常。”
  2. 在GUI界面点击‘借阅’按钮,输入有效用户和副本ID
    → “触发BorrowService的原子操作,后台执行SELECT FOR UPDATE锁定行,再UPDATE状态,最后INSERT记录。”
  3. 故意输入已借出的副本ID,观察弹窗提示
    → “系统捕获‘副本不可借阅’异常,前端友好提示,而非抛出500错误,体现健壮性设计。”

注意:演示前务必清空borrow_record表并重置book_copy.status
使用UPDATE book_copy SET status = 'available' WHERE id BETWEEN 5001 AND 5200;确保演示环境纯净。临时数据污染是答辩最大风险点。

系统上线后,管理员可通过SELECT u.username, COUNT(br.id) as borrow_count FROM user u LEFT JOIN borrow_record br ON u.id = br.user_id WHERE br.return_date IS NULL GROUP BY u.id ORDER BY borrow_count DESC LIMIT 5;快速定位借阅最多的学生,这是评审老师常问的“你系统能解决什么实际问题”的直接答案。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询