资讯动态

Java毕业设计:JDBC事务与MySQL并发控制实战

发布时间:2026/9/10 5:51:15 来源:尧图企业网站定制
简介这是一套基于Java SwingJDBCMySQL的毕业设计选题管理系统完整源码与配套文档面向高校计算机相关专业学生、毕业设计开发者及需要快速搭建桌面管理系统的初学者。项目实现了学生选题、教师命题、管理员审核、题目管理、查询统计等核心功能覆盖用户登录与角色权限控制、数据库增删改查及基础安全处理适合用于课程设计、毕业设计或Java桌面应用开发练习。压缩包共18个文件包括12个Java源文件、1个SQL数据库脚本、1个数据库表设计文档doc/md、1个依赖jar包及项目结构文件等整体大小约4.61MB结构清晰便于直接导入开发工具查看和运行。已有1531人学习或下载。资料内含可运行的源码、数据库初始化脚本及表结构说明文档能帮助读者理解Swing界面编写、JDBC数据库交互和业务分层设计思路也可作为选题或答辩演示的参考项目。1. 一个毕设题目里最难的不是Swing而是JDBC的状态边界“java SwingJDBCmysql实现毕业设计选题管理系统”这个标题三重技术名词叠在一起新手照着网上的教程敲完界面能弹窗、查询能查出几条数据真正拿去答辩时被问倒的往往是同一个问题一个题目同时被两个学生选为什么两个人都能选上原因是把JDBC当成了普通的数据访问工具没把它当成事务边界。这套系统不复杂四张表、两种角色、一条选题审批流程但它集中了桌面应用最常见的三类问题JDBC连接怎么不泄漏、Swing界面怎么不被慢查询卡死、并发下怎么保持数据一致。这篇文章按表结构、连接层、界面层、业务层的顺序写所有代码都能直接放进一个Java工程里跑通。适合做毕设的学生也适合想回头补一层JDBC功底的开发。2. 表结构与MySQL 8.0连接层先定约束再写JDBC做选题系统我最常建议的顺序是先建表。JDBC的应用层SQL非常依赖表的约束比如唯一键和状态字段在表结构里设计对了后面抢题、撤回、审核都只是下一条UPDATE设计错了就得在Java代码里塞一堆if判断去掩盖数据问题。2.1 四张核心表的字段设计与状态约定一个选题管理系统最少要有学生、教师、题目、选题记录四张表。题目表不直接存学生ID而是通过selection记录学生和题目的多对多关系同时用状态字段控制一条选题记录的流程节点。建表SQL如下。CREATE DATABASE choose_topic DEFAULT CHARACTER SET utf8mb4; USE choose_topic; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 内部ID, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL, major VARCHAR(50) COMMENT 专业, password VARCHAR(64) NOT NULL DEFAULT 123456 ) ENGINEInnoDB; CREATE TABLE teacher ( id INT PRIMARY KEY AUTO_INCREMENT, teacher_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, title VARCHAR(20) COMMENT 职称 ) ENGINEInnoDB; CREATE TABLE topic ( id INT PRIMARY KEY AUTO_INCREMENT, teacher_id INT NOT NULL, title VARCHAR(200) NOT NULL, description TEXT, max_students INT NOT NULL DEFAULT 1 COMMENT 选题人数上限, selected_count INT NOT NULL DEFAULT 0 COMMENT 当前通过人数, status TINYINT NOT NULL DEFAULT 0 COMMENT 0可选 1已满 2关闭, CONSTRAINT fk_topic_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(id) ) ENGINEInnoDB; CREATE TABLE selection ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, topic_id INT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审核 1通过 2拒绝 3撤回, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 申请时间, review_time DATETIME COMMENT 审核时间, CONSTRAINT fk_sel_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_sel_topic FOREIGN KEY (topic_id) REFERENCES topic(id), UNIQUE KEY uk_student_topic (student_id, topic_id) ) ENGINEInnoDB;这段建表SQL里有几个地方会影响后续JDBC代码的写法。selection表加了一个联合唯一键同一个学生不能对同一题目重复申请这是后面处理重复提交的兜底。topic.status字段用TINYINT注释写清楚取值避免在Java里用字符串比较状态。外键加上之后删除教师或学生会被数据库挡住所以业务上要么先处理关联数据要么做假删除。状态字段统一用数字而不是中文最好把它们写成一个常量类。下表是后面所有SQL的公共约定。字段/取值含义在代码里的影响topic.status 0可选selected_count max_students可作为抢题UPDATE的WHERE条件topic.status 1已满由UPDATE子句自动触发不手动改selection.status 0待审核学生可撤回selection.status 1审核通过学生不可再选其他题selection.status 2拒绝学生可重新选题selection.status 3已撤回保留历史记录不物理删除先做表就是给业务画状态机的草稿。状态机画清楚了后面每一条业务SQL都能找到明确的更新方向。2.2 MySQL 8.0 驱动和连接参数怎么不踩坑网上搜“MySQL安装教程”能搜到一大堆但装上之后JDBC连不上的人更多。MySQL 8.0之后官方驱动从mysql-connector-java改名为mysql-connector-jMaven坐标如下。dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency没用Maven的话直接把jar包放进项目lib目录并引入classpath。Class.forName(com.mysql.cj.jdbc.Driver)在8.0驱动里仍然可用但DriverManager的SPI机制出现以后这行已经不是必须的写上也没有副作用。真正导致No suitable driver found for jdbc:mysql的原因绝大多数是两点URL没有以jdbc:mysql://开头或者驱动jar没进到运行时的classpath。第二个高频问题是时区8.0版本不传serverTimezone会直接报错。常见的开发环境连接URL是jdbc:mysql://localhost:3306/choose_topic?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue参数建议值说明useUnicodetrue配合characterEncoding避免中文乱码characterEncodingutf8数据库建库用utf8mb4客户端encoding用utf8serverTimezoneAsia/Shanghai8.0驱动不设会报时区错误useSSLfalse本地开发避免证书校验allowPublicKeyRetrievaltrue解决caching_sha2_password认证取公钥失败allowPublicKeyRetrieval容易被忽略。MySQL 8.0默认认证插件是caching_sha2_password非SSL连接下第一次认证需要拿服务器公钥不开启这一项会抛出Public Key Retrieval is not allowed。开发环境直接加true生产环境应该配置SSL而不是靠这个参数绕过。2.3 用单例管理连接用try-with-resources保证关闭Swing桌面程序不像后端服务有那么多并发但JDBC连接也不能每次新建底层Socket。常见的做法是写一个单例的ConnectionManager整个进程维护一个连接排错也简单。public class ConnectionManager { private static final String URL jdbc:mysql://localhost:3306/choose_topic ?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse; private static final String USER root; private static final String PASSWORD your_password; private static Connection conn; public static synchronized Connection getConnection() throws SQLException { if (conn null || conn.isClosed()) { conn DriverManager.getConnection(URL, USER, PASSWORD); } return conn; } }单例连接有个隐患一个用户登录后多个窗口共用同一Connection另一个窗口执行查询时可能碰到ResultSet already closed。所以更稳妥的做法是让类只负责连接参数每个DAO方法里单独取连接用try-with-resources归还。下面是一个查询全部题目的DAO写法。public ListTopic findAllTopics() { String sql SELECT id, title, teacher_id, max_students, selected_count, status FROM topic ORDER BY id DESC; ListTopic topics new ArrayList(); try (Connection conn DriverManager.getConnection(URL, USER, PASSWORD); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Topic topic new Topic(); topic.setId(rs.getInt(id)); topic.setTitle(rs.getString(title)); topic.setMaxStudents(rs.getInt(max_students)); topics.add(topic); } } catch (SQLException e) { e.printStackTrace(); } return topics; }try-with-resources会在return之前按逆序关闭rs、ps、connclose方法内部由连接驱动决定是断开还是归还池子。如果沿用老代码里的finally块嵌套关闭一旦前一个close抛异常后面的资源就不会关闭。JDBC的PreparedStatement也要用参数占位符不要拿字符串拼接SQL这里不展开讲注入攻击单是学号里的引号就能让你SQL报错。3. Swing界面层与JDBC数据渲染别把SQL写在按钮事件里登录窗口能弹出来、按钮点了能查库很多毕设就到此为止了。真正的问题在后面数据查出来怎么填到JTable里为什么窗口一执行查询就变成白色无响应这两件事都跟Swing的单线程模型有关。3.1 用SwingWorker避免登录按钮把窗口卡死先看一个反例直接在JButton的ActionListener里执行DriverManager.getConnection和PreparedStatement.executeQuery。这样写本机能跑但查询超过一两秒EDT会被数据库操作占住窗口立刻开始转圈。Swing的规则是所有UI组件只能在事件分发线程上操作耗时任务发到后台线程再切回EDT更新界面。我使用的方案是SwingWorker。下面是学生登录校验的最小实现。loginButton.addActionListener(e - { String no usernameField.getText().trim(); String pwd new String(passwordField.getPassword()); loginButton.setEnabled(false); SwingWorkerBoolean, Void worker new SwingWorker() { Override protected Boolean doInBackground() { String sql SELECT id FROM student WHERE student_no ? AND password ?; try (Connection conn ConnectionManager.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, no); ps.setString(2, encryptPassword(pwd)); try (ResultSet rs ps.executeQuery()) { return rs.next(); } } catch (SQLException ex) { ex.printStackTrace(); return false; } } Override protected void done() { loginButton.setEnabled(true); try { if (get()) { JOptionPane.showMessageDialog(LoginFrame.this, 登录成功); } else { JOptionPane.showMessageDialog(LoginFrame.this, 账号或密码错误); } } catch (InterruptedException | ExecutionException ex) { ex.printStackTrace(); } } }; worker.execute(); });三个细节值得注意。第一密码在doInBackground里做摘要后再传给SQL不要在前台把明文拼进SQL。第二登录成功后的窗口跳转必须放在done()里因为它是UI操作只能由EDT执行。第三登录期间把按钮setEnabled(false)防止双击触发两条重复SQL这在后面抢题场景是同样的道理。连接获取也可以单独封一层UserDaoSwingWorker里只调用dao.login(no, pwd)让界面代码看不到任何数据库细节。3.2 题目列表ResultSet别直接塞给JTable主界面一般用JTabbedPane放“全部题目”“我的选题”“个人信息”几个Tab。题目列表用JTable展示但JTable接受的是TableModel不是ResultSet。中间需要一步转换DAO层把ResultSet转成List对象再交给自定义TableModel显示。这样做的理由是TableModel需要随机访问每行每列的值而ResultSet只能一路next遍历在getValueAt里调用rs.getString会打乱游标数据渲染就错乱了。public class TopicTableModel extends AbstractTableModel { private final String[] columns {题目编号, 题目名称, 所属教师, 可选人数, 状态}; private final ListTopicWithTeacher rows; public TopicTableModel(ListTopicWithTeacher rows) { this.rows rows; } Override public int getRowCount() { return rows.size(); } Override public int getColumnCount() { return columns.length; } Override public String getColumnName(int column) { return columns[column]; } Override public boolean isCellEditable(int rowIndex, int columnIndex) { return false; } Override public Object getValueAt(int rowIndex, int columnIndex) { TopicWithTeacher t rows.get(rowIndex); return switch (columnIndex) { case 0 - t.getId(); case 1 - t.getTitle(); case 2 - t.getTeacherName(); case 3 - t.getSelectedCount() / t.getMaxStudents(); case 4 - t.getStatus() 0 ? 可选 : 已满; default - ; }; } }这组数据来自一次连表查询在DAO里执行SELECT t.id, t.title, t.max_students, t.selected_count, t.status, tc.name AS teacher_name FROM topic t LEFT JOIN teacher tc ON t.teacher_id tc.id ORDER BY t.id DESC;DAO层把每行封装成TopicWithTeacher对象return List。Swing拿到List后直接table.setModel(new TopicTableModel(list))。这层转换看着多写了几行但后期要把桌面系统改造成Web时List和DAO能原样挪走只有表格模型留在Swing包里。3.3 主界面的角色切换怎么组织登录时区分学生、教师、管理员最简单的做法是用账号前缀S开头是学生T开头是教师管理员直接比对固定账号。登录成功拿到角色后用一个顶层CardLayout容器作为主窗体的根面板把三个业务面板都加进去再调用show方法切换。不要给每个角色单独做一个JFrame。窗口弹一层套一层用户关闭某个子窗口时程序可能连带退出焦点管理也会出问题。Swing组件这个系统里的合适用途JTabbedPane学生端主界面放“全部题目”“我的选题”JTable AbstractTableModel题目列表、学生列表、审核列表JComboBox按专业、按教师筛选题目JTextField / JPasswordField登录、新增题目、搜索学号JTextArea描述题目详情、填写审核意见CardLayout登录后按角色切换主面板组件选型原则是“能显示表格的用JTable能选择的用下拉框需要多行文字的用JTextArea”不要在Swing里做花哨的自绘界面。所以我在TopicTableModel里把isCellEditable直接返回false用户不能在表格里双击改数据只能通过“选择这个题目”按钮进入业务操作。接下来进入第四章这一步不能只在内存里判断必须让数据库参与进来。4. 选题流程的事务与并发不让两个学生抢到同一题选题系统的“业务”不是增删改查而是状态更新。学生点选题生成一条待审核记录教师点通过记录变成已通过。最容易被问住的场景是两个学生在不同窗口同时点“选题”数据库还没反应过来两条申请都插进去了。这道题如果再抽象一点就是Java面试题里常说的超卖问题换了个业务场景。4.1 选题状态流与“检查先行”的误区正常流程是学生查看题目状态为可选点击申请插入一条status0的selection记录。教师端列出status0的记录审核通过时把selection.status改成1同时更新topic状态。很多代码写成这样先查再插String checkSql SELECT status, selected_count, max_students FROM topic WHERE id ?; // status 0 且 selected_count max_students然后执行插入 String insertSql INSERT INTO selection(student_id, topic_id, status) VALUES (?, ?, 0);这个“先查再插”在单线程下没问题但两个进程同时执行checkSql时会看到同一个旧值都进入insert。MySQL默认RR隔离级别下两条事务可以同时读到同一条记录检查先行只能拦单用户操作不能拦多窗口并发。网上把这个叫超卖本质是业务SQL没有把“检查”和“占用”放进同一个原子操作。4.2 用一条UPDATE抢状态用影响行数判断是否成功正确的做法是把检查与占用合成一条UPDATE通过affected rows判断有没有抢到。这组系统按毕设最常见的“一个题目只能被一个学生通过选题”规则SQL可以写成UPDATE topic SET selected_count selected_count 1, status CASE WHEN selected_count 1 max_students THEN 1 ELSE status END WHERE id ? AND status 0 AND selected_count 0;注意WHERE里用selected_count 0而不是selected_count max_students。默认一个题目只能选一个人已选人数是0时两个学生都能执行这条UPDATE但InnoDB行锁会让后一个执行的人等前一个提交等它真正执行时selected_count已经变成1匹配不到行UPDATE返回0。如果写selected_count max_students两个事务先后执行后第二个仍可能匹配到“1小于2”数据就会出错。UPDATE返回1后再把插入申请记录放进同一个事务commit后两个操作一起生效。完整方法如下。public synchronized boolean chooseTopic(int studentId, int topicId) throws SQLException { String updateTopicSql UPDATE topic SET selected_count selected_count 1, status CASE WHEN selected_count 1 max_students THEN 1 ELSE status END WHERE id ? AND status 0 AND selected_count 0; String insertSelectionSql INSERT INTO selection(student_id, topic_id, status, create_time) VALUES (?, ?, 0, NOW()); try (Connection conn ConnectionManager.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps1 conn.prepareStatement(updateTopicSql)) { ps1.setInt(1, topicId); int affected ps1.executeUpdate(); if (affected ! 1) { conn.rollback(); return false; } try (PreparedStatement ps2 conn.prepareStatement(insertSelectionSql)) { ps2.setInt(1, studentId); ps2.setInt(2, topicId); ps2.executeUpdate(); } conn.commit(); return true; } catch (SQLException ex) { conn.rollback(); throw ex; } } }关键参数说明setAutoCommit(false)表示事务开始直到commit才生效后面任何一条SQL异常rollback会把前面的UPDATE也撤回。如果连接用的是2.3节的进程级单例这段代码要改造成传入同一个Connection否则自动提交一开事务边界就断了。synchronized在这里补充了进程内两个按钮同时点击的情况但真正的并发控制还是靠UPDATE行数判断多实例部署时synchronized拦不住跨进程竞争。如果题目支持多人组队就把WHERE里的selected_count 0换成selected_count max_students同时在Java里循环插入多条selection记录并利用联合唯一键保证同一个学生不会重复加入同一题。4.3 教师审核时要注意的连带更新教师点“通过”的本质是更新selection.status但这个操作不只影响当前这行。审核通过的同时该学生的其他待审核记录要自动变成拒绝否则学生可以同时挂着多个待审核申请等其中一个通过了另一个还留在待审列表里。这里用同一个事务执行两条UPDATE。UPDATE selection SET status 2, review_time NOW() WHERE student_id ? AND status 0 AND id ! ?; UPDATE selection SET status 1, review_time NOW() WHERE id ? AND status 0;顺序上先拒绝其他申请再通过当前申请可以防止同一个学生同时被审核两条记录。题目名额在4.2节的抢题阶段已经占用教师审核只负责状态流转不再次增加计数器避免审核时出现题目已满但申请仍然通过的冲突。4.4 JDBC异常对照表把这套系统里最容易出现的连接报错整理成一张表方便照着排查。异常现象原因处理ClassNotFoundException: com.mysql.cj.jdbc.Driverjar包没进classpath检查项目lib目录与IDE的module依赖No suitable driver found for jdbc:mysqlURL没有jdbc:前缀或驱动版本过旧确认URL以jdbc:mysql://开头Access denied for user账号密码或权限错误先用mysql -u root -p验证Public Key Retrieval is not allowedcaching_sha2_password拿不到公钥URL加allowPublicKeyRetrievaltrueUnknown character set: utf8连接字符集和库不一致统一用utf8mb4URL写characterEncodingutf8Duplicate entry for key uk_student_topic重复插入联合唯一键捕获SQLIntegrityConstraintViolationException给提示5. 用general_log和日志开关验证抢题SQL是不是按预期执行代码写完还需要证明“两个学生同时点选题”时数据库收到的SQL顺序是对的。靠UI点按钮看不出内部状态我一般会开MySQL的general_log把JDBC实际发给数据库的每一条SQL都记录下来再对照时间戳判断事务顺序。5.1 开启MySQL general_log在MySQL 8.0里执行下面的SQLSET global general_log ON; SET global general_log_file /tmp/mysql_general.log;Windows下可以将路径改成C:/mysql_general.log注意MySQL进程要有对应目录的写权限。开启后执行一次选题操作打开日志文件会看到类似这样的顺序2025-06-01T10:00:00.123456Z 123 Query UPDATE topic SET selected_count selected_count 1 ... 2025-06-01T10:00:00.223456Z 123 Query INSERT INTO selection(student_id, topic_id, status, create_time) VALUES ... 2025-06-01T10:00:00.323456Z 123 Query COMMIT如果UPDATE和INSERT之间没有COMMIT说明事务生效如果INSERT出现在UPDATE之前说明连接开了自动提交回头去检查代码里的setAutoCommit(false)。用完记得把general_log关掉生产环境开它会让磁盘IO和日志体积快速增长。5.2 在DAO里留一个无需改SQL的日志开关可以写一个小工具类包装PreparedStatement在执行前打印SQL和参数比每个DAO里加System.out.println干净。public class SqlLogUtil { public static PreparedStatement prepare(Connection conn, String sql, Object... params) throws SQLException { if (System.getProperty(jdbc.sql.log) ! null) { System.out.println([SQL] sql); System.out.println([PARAMS] Arrays.toString(params)); } PreparedStatement ps conn.prepareStatement(sql); for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } return ps; } }启动JVM时加上-Djdbc.sql.logtrue日志才输出不加就不输出。调用方式是把DAO里的conn.prepareStatement(sql)替换成SqlLogUtil.prepare(conn, sql, 参数们)。用setObject统一设置参数能避免int和String混用时报Parameter index out of range。5.3 给桌面系统留一条可迁移的路径到这一步界面层是Swing数据访问层是裸JDBC两者已经通过DAO接口和VO对象隔开了。如果哪天要改成Spring Boot需要换掉的只是DAO实现JFrame和JTable会全部丢弃但表结构、业务规则、事务边界不用重做。你在这套系统里调通的SQL、状态字段取值、抢题的UPDATE写法换框架之后照样是这些。所以我把建表语句放在第二章把事务放在第四章就为了一句话会写Swing的人查文档就会能把表结构和UPDATE操作讲清楚的人才是真的会做选题系统。验证的时候开两个窗口同时点选题再去看general_log里两条UPDATE的间隔时间比用断点单步调试直观得多。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价