ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java Swing挂号系统实战:MySQL并发扣号与GUI线程安全

Java Swing挂号系统实战:MySQL并发扣号与GUI线程安全 简介这是一份面向计算机专业本科生与Java初学者的课程设计级医院挂号管理系统实战项目聚焦GUI桌面应用开发与MySQL数据库综合实践解决医疗场景下挂号、退号、信息查询与统计等核心业务流程。资源包共331个文件含45个Java源码文件如PatientControl.java、Doctor.java、39个编译后class文件、37个界面资源png图、13个gif动效及7个UI布局文件辅以HTML文档与CSS/FXML样式文件完整覆盖从界面构建、数据库交互到权限控制的全链路实现压缩包仅3.43MB轻量易解压学习。已有752人下载学习可直接运行查看登录、挂号、退号、医生/患者双角色查询及日报打印等全部功能代码结构清晰、注释充分特别适合课程设计参考、期末项目复现或Java SwingMySQL集成开发能力提升。1. 为什么一个“简易”挂号系统反而最容易在真实科室翻车你手头这个【基于 JavaMySQL 实现GUI医院简易挂号管理系统】不是教学玩具而是压在门诊护士站桌面的真实压力源早上七点半窗口刚开三分钟内涌进27个老人排队系统卡顿半秒后面就有人拍桌子喊“我挂不上号今天白跑了”。所谓“简易”指的是功能边界清晰——只做患者登记、科室/医生选择、号源查询、挂号生成、当日退号这五件事不碰电子病历、不连医保结算、不对接HIS主系统。但恰恰是这种“轻量级”对Java GUI线程安全、MySQL事务隔离级别、号源并发扣减逻辑的容错要求反而更高——它没冗余层兜底一出问题就是前台直接崩。适合两类人计算机专业课设要交可运行Demo的学生需能本地一键启动、基层社区卫生服务中心想快速替换Excel排班表的IT兼管员需无依赖部署。本文不讲Swing和JavaFX选型辩论不堆MVC分层图只带你用最稳的SwingJDBC直连方案把挂号核心链路跑通、压测过、上线扛住过早高峰流量。2. 用 Swing JDBC 在本地跑通挂号核心链路的最小命令2.1 数据库建模避开“医生-科室”多对多陷阱的3张表设计挂号系统最常翻车的起点是把“医生属于科室”简单建为外键关联。实际中一个医生可能在内科坐诊半天、下午去体检中心而体检中心又不属于传统临床科室。我们用三张表解耦-- 患者表仅存挂号必需字段避免过度收集隐私 CREATE TABLE patient ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) NOT NULL, id_card CHAR(18) UNIQUE, -- 身份证号唯一但允许为空儿童无证 phone CHAR(11), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 科室表纯静态信息无业务逻辑 CREATE TABLE department ( id TINYINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(30) NOT NULL UNIQUE, description VARCHAR(100) ); -- 医生表关键每个医生记录绑定当日出诊状态 CREATE TABLE doctor ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) NOT NULL, title VARCHAR(10), -- 主任医师/主治医师等 dept_id TINYINT NOT NULL, status ENUM(ON_DUTY, OFF_DUTY, LEAVE) DEFAULT ON_DUTY, FOREIGN KEY (dept_id) REFERENCES department(id) ); -- 号源表核心按“医生日期时段”粒度控制非按科室 CREATE TABLE registration_slot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_id INT NOT NULL, date DATE NOT NULL, time_period ENUM(MORNING, AFTERNOON) NOT NULL, total_quota TINYINT NOT NULL DEFAULT 30, used_quota TINYINT NOT NULL DEFAULT 0, available BOOLEAN GENERATED ALWAYS AS (used_quota total_quota) STORED, UNIQUE KEY uk_doctor_date_period (doctor_id, date, time_period), FOREIGN KEY (doctor_id) REFERENCES doctor(id) ); -- 挂号记录表挂号动作的原子化凭证 CREATE TABLE registration ( id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_id BIGINT NOT NULL, slot_id BIGINT NOT NULL, reg_no VARCHAR(20) NOT NULL UNIQUE, -- 生成规则YYMMDD4位流水如2405200001 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, status ENUM(CONFIRMED, CANCELED) DEFAULT CONFIRMED, FOREIGN KEY (patient_id) REFERENCES patient(id), FOREIGN KEY (slot_id) REFERENCES registration_slot(id) );提示registration_slot表的available字段用GENERATED ALWAYS AS计算列避免每次查号源时写WHERE used_quota total_quota——MySQL 5.7 支持该语法查询走索引更快reg_no不用UUID或雪花ID因挂号单需人工核对短数字更易读。2.2 Java GUI 窗体骨架用 GroupLayout 避免绝对定位灾难Swing 的GroupLayout是 SwingBuilder 时代被低估的利器它让组件随窗口缩放自动重排比GridBagLayout少写60%约束代码。挂号主窗体结构如下// RegistrationMainFrame.java public class RegistrationMainFrame extends JFrame { private JComboBoxString deptComboBox; private JComboBoxString doctorComboBox; private JComboBoxString timePeriodComboBox; private JLabel availableLabel; // 显示当前号源余量 private JButton registerButton; private JButton cancelButton; public RegistrationMainFrame() { setTitle(医院简易挂号系统 v1.0); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new BorderLayout()); // 顶部操作区科室-医生-时段三级联动 JPanel topPanel new JPanel(); GroupLayout layout new GroupLayout(topPanel); topPanel.setLayout(layout); layout.setAutoCreateGaps(true); layout.setAutoCreateContainerGaps(true); // 组件声明省略初始化代码 deptComboBox new JComboBox(); doctorComboBox new JComboBox(); timePeriodComboBox new JComboBox(new String[]{上午, 下午}); availableLabel new JLabel(余号--); registerButton new JButton(挂号); cancelButton new JButton(退号); // 水平组科室下拉框 → 医生下拉框 → 时段下拉框 → 余号标签 → 挂号按钮 → 退号按钮 layout.setHorizontalGroup( layout.createSequentialGroup() .addComponent(deptComboBox) .addComponent(doctorComboBox) .addComponent(timePeriodComboBox) .addComponent(availableLabel) .addComponent(registerButton) .addComponent(cancelButton) ); // 垂直组所有组件在同一行对齐 layout.setVerticalGroup( layout.createParallelGroup(GroupLayout.Alignment.CENTER) .addComponent(deptComboBox) .addComponent(doctorComboBox) .addComponent(timePeriodComboBox) .addComponent(availableLabel) .addComponent(registerButton) .addComponent(cancelButton) ); add(topPanel, BorderLayout.NORTH); // 中间患者信息区JTable展示历史挂号 JTable historyTable new JTable(new DefaultTableModel( new Object[][]{}, new String[]{挂号号, 患者姓名, 科室, 医生, 时段, 状态} )); add(new JScrollPane(historyTable), BorderLayout.CENTER); pack(); setLocationRelativeTo(null); } }逻辑说明GroupLayout的setAutoCreateGaps(true)自动添加组件间距避免手动调setBorder()Alignment.CENTER确保按钮文字垂直居中防止不同系统字体渲染导致错位JScrollPane包裹JTable是刚需——挂号记录滚动查看否则窗口拉大后表格内容被裁剪。2.3 挂号核心逻辑用 MySQL 的 SELECT ... FOR UPDATE 实现号源强一致性挂号本质是“查余量→扣减→生成记录”三步原子操作。若用普通UPDATE先扣再查高并发下必然超挂。必须用数据库行锁// RegistrationService.java public class RegistrationService { private final ConnectionPool connectionPool; // 自定义连接池非HikariCP简化课设 public boolean createRegistration(long patientId, long slotId) throws SQLException { String sql SELECT used_quota, total_quota FROM registration_slot WHERE id ? FOR UPDATE ; try (Connection conn connectionPool.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setLong(1, slotId); ResultSet rs ps.executeQuery(); if (!rs.next()) return false; int used rs.getInt(used_quota); int total rs.getInt(total_quota); if (used total) return false; // 余量不足 // 在同一事务内完成扣减和挂号记录插入 conn.setAutoCommit(false); try { // 步骤1更新号源余量 String updateSql UPDATE registration_slot SET used_quota used_quota 1 WHERE id ?; try (PreparedStatement updatePs conn.prepareStatement(updateSql)) { updatePs.setLong(1, slotId); updatePs.executeUpdate(); } // 步骤2生成挂号号格式YYMMDD4位流水 String regNo generateRegNo(conn); // 步骤3插入挂号记录 String insertSql INSERT INTO registration (patient_id, slot_id, reg_no) VALUES (?, ?, ?) ; try (PreparedStatement insertPs conn.prepareStatement(insertSql)) { insertPs.setLong(1, patientId); insertPs.setLong(2, slotId); insertPs.setString(3, regNo); insertPs.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } } } private String generateRegNo(Connection conn) throws SQLException { // 获取当日最大挂号号避免分布式ID生成器复杂度 String today LocalDate.now().format(DateTimeFormatter.ofPattern(yyMMdd)); String sql SELECT MAX(reg_no) FROM registration WHERE reg_no LIKE ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, today %); ResultSet rs ps.executeQuery(); int nextSeq 1; if (rs.next() rs.getString(1) ! null) { String maxNo rs.getString(1); nextSeq Integer.parseInt(maxNo.substring(6)) 1; } return today String.format(%04d, nextSeq); } } }参数说明FOR UPDATE锁住registration_slot行其他事务对该slot_id的相同查询会被阻塞直到本事务提交或回滚generateRegNo()用MAX(reg_no)防止重复虽有性能损耗但课设场景完全够用日挂号量500conn.setAutoCommit(false)是事务控制开关漏写会导致扣减成功但挂号失败时数据不一致。3. 把 MySQL 连接池配成“挂号专用通道”的3个必调参数3.1 连接池不是越大越好根据挂号峰值反推连接数公式挂号系统并发压力集中在早7:30-9:00假设单次挂号操作平均耗时200ms含网络DB窗口开放90分钟5400秒预估最大并发用户数为120人社区医院规模。按阿姆达尔定律估算理论最大TPS 并发数 / 平均响应时间 120 / 0.2s 600 TPS 所需最小连接数 TPS × 平均连接占用时间而挂号操作中连接真正被占用的时间只有执行SQL的50ms其余为GUI渲染、用户输入故最小连接数 ≈ 600 × 0.05 30但必须预留缓冲最终设为maxPoolSize40。若盲目设成100MySQL服务端会因过多空闲连接耗尽内存。3.2 驱动URL加这3个参数解决80%的连接异常MySQL 8.0 JDBC URL 必须显式配置否则在挂号高峰期出现Communications link failure// DatabaseConfig.java public class DatabaseConfig { private static final String URL jdbc:mysql://localhost:3306/hospital_db? useSSLfalse // 关闭SSL课设无需加密 serverTimezoneAsia/Shanghai // 强制时区避免挂号时间存错 allowPublicKeyRetrievaltrue // MySQL 8.0 必加否则驱动报错 connectTimeout3000 // 连接超时3秒挂号不能卡死 socketTimeout10000; // 查询超时10秒防慢SQL拖垮GUI private static final String USERNAME root; private static final String PASSWORD your_password; public static DataSource getDataSource() { HikariConfig config new HikariConfig(); config.setJdbcUrl(URL); config.setUsername(USERNAME); config.setPassword(PASSWORD); config.setMaximumPoolSize(40); config.setMinimumIdle(5); config.setConnectionTimeout(3000); config.setIdleTimeout(600000); config.setMaxLifetime(1800000); return new HikariDataSource(config); } }注意allowPublicKeyRetrievaltrue是MySQL 8.0.13强制要求不加会抛Public Key Retrieval is not allowedsocketTimeout10000比connectionTimeout大确保连接建立后查询也能超时退出避免GUI线程永久阻塞。3.3 为什么挂号系统必须禁用 autoReconnect网上教程常教加autoReconnecttrue解决断连但在挂号场景这是毒药当MySQL服务短暂重启autoReconnect会静默重建连接但旧连接上的未提交事务如已扣号未写记录丢失GUI线程感知不到连接已换继续用旧连接句柄导致SQLException: Connection is closed更糟的是新连接无事务上下文commit()无效号源被扣但挂号失败患者白等。正确做法捕获SQLException后主动提示“系统繁忙请稍后再试”并清空当前挂号表单。代码示例try { if (service.createRegistration(patientId, slotId)) { JOptionPane.showMessageDialog(this, 挂号成功号 regNo); } else { JOptionPane.showMessageDialog(this, 号源已满请选择其他时段); } } catch (SQLException e) { String msg e.getMessage(); if (msg.contains(Communications link failure) || msg.contains(Connection refused)) { JOptionPane.showMessageDialog(this, 系统连接异常请检查MySQL服务是否运行, 连接错误, JOptionPane.ERROR_MESSAGE); // 清空表单防止用户重复提交 clearForm(); } else { JOptionPane.showMessageDialog(this, 挂号失败 msg); } }4. 挂号系统避坑5条血泪经验每一条都让新手少熬两夜4.1 现象挂号号生成重复同一时段出现两个“2405200001”原因generateRegNo()方法未加synchronized或数据库锁多线程并发调用时读到相同MAX(reg_no)解决将号生成逻辑移至数据库端用INSERT ... SELECT原子化INSERT INTO registration (patient_id, slot_id, reg_no) SELECT ?, ?, CONCAT(DATE_FORMAT(NOW(), %y%m%d), LPAD(IFNULL(MAX(SUBSTR(reg_no,7)),0)1,4,0)) FROM registration WHERE reg_no LIKE CONCAT(DATE_FORMAT(NOW(), %y%m%d), %);注此SQL在MySQL中需配合触发器或存储过程完善课设建议改用SELECT ... FOR UPDATE锁定当天最大号记录表4.2 现象切换科室后医生下拉框空白F5刷新才显示原因Swing事件线程EDT被耗时DB查询阻塞UI冻结导致组件未重绘解决所有数据库操作必须异步执行用SwingWorker包装new SwingWorkerVoid, Void() { Override protected Void doInBackground() throws Exception { doctorList doctorDao.findByDeptId(deptId); return null; } Override protected void done() { doctorComboBox.setModel(new DefaultComboBoxModel(doctorList.toArray())); } }.execute();4.3 现象MySQL 8.0 连接报错The server time zone value XXX is unrecognized原因JVM时区与MySQL服务器时区不一致且JDBC未显式指定解决URL中强制serverTimezoneAsia/Shanghai并在MySQL中执行SET GLOBAL time_zone 08:00; SET time_zone 08:00;4.4 现象挂号成功后GUI界面未刷新余号数仍显示“余号30”原因availableLabel.setText()在非EDT线程调用Swing线程安全机制丢弃更新解决所有UI更新必须用SwingUtilities.invokeLater()SwingUtilities.invokeLater(() - { availableLabel.setText(余号 (total - used)); });4.5 现象退号后号源余量未增加导致后续无法挂号原因退号逻辑未开启事务或UPDATE registration_slot SET used_quota used_quota - 1执行后未commit()解决退号方法必须与挂号同级事务控制且检查statusCONFIRMED防止重复退String sql UPDATE registration_slot rs JOIN registration r ON rs.id r.slot_id SET rs.used_quota rs.used_quota - 1 WHERE r.id ? AND r.status CONFIRMED ;5. 验证挂号系统是否“真可用”的3个硬核测试法5.1 用 JMeter 模拟早高峰120并发用户持续压测30分钟挂号系统能否上线不看代码行数只看压测结果。用JMeter配置如下参数值说明Threads (users)120模拟早高峰并发量Ramp-up period60 seconds1分钟内逐步加压避免瞬间冲击Loop countForever持续运行观察稳定性HTTP RequestPOST /register接口地址若改造为Web版或本地Java调用JDBC RequestINSERT INTO registration...直连MySQL执行挂号SQL关键监控指标Error Rate 0.5%超过则说明号源锁竞争或连接池瓶颈90% Line 300ms挂号操作从点击到弹窗成功用户感知不卡顿Active Threads 峰值 ≤ 40验证连接池maxPoolSize40设置合理无连接等待。若压测中出现大量Timeout connecting to database立即检查MySQLmax_connections是否 ≥ 100默认151够用若used_quota更新异常需确认SELECT ... FOR UPDATE是否被其他未提交事务长期持有。5.2 用 MySQL 的 INFORMATION_SCHEMA 查号源锁状态当挂号变慢时别急着重启服务先查数据库是否被锁死-- 查看当前被锁的号源行 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; -- 查看挂号事务是否长时间未提交 SELECT trx_id, trx_state, trx_started, TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) as duration_sec, trx_query FROM information_schema.INNODB_TRX WHERE trx_query LIKE %registration_slot% AND trx_state LOCK WAIT;解读若duration_sec 60说明某挂号事务卡住未提交需KILL对应线程若blocking_query是UPDATE registration_slot...证明号源扣减逻辑有缺陷如未catch异常导致未rollback。5.3 用 Swing 的 RepaintManager 截图诊断GUI卡顿根源Swing界面卡顿90%源于EDT线程被阻塞。启用RepaintManager日志// 在main方法开头加入 RepaintManager.setCurrentManager(new RepaintManager() { Override public void addDirtyRegion(JComponent c, int x, int y, int w, int h) { super.addDirtyRegion(c, x, y, w, h); // 记录脏区域添加辅助定位重绘异常 System.out.printf(Dirty: %s [%d,%d,%d,%d]%n, c.getClass().getSimpleName(), x,y,w,h); } }); // 启动时添加EDT监控 EventQueue.invokeLater(() - { SwingUtilities.invokeLater(() - { // 检查EDT是否被阻塞 Thread edt Thread.currentThread(); System.out.println(EDT alive: edt.isAlive()); }); });典型卡顿信号控制台持续打印Dirty: JButton [...]按钮反复重绘可能是repaint()被循环调用点击挂号按钮后System.out.println(EDT alive)延迟2秒才输出证明DB查询在EDT中执行必须改用SwingWorker。我带过的3届学生课设80%的“系统卡死”问题靠这三招中的第一招JMeter压测就能定位到连接池或SQL问题剩下20%用第二招锁查询揪出事务漏洞最后那点GUI毛刺第三招截图日志一目了然。挂号系统真正的难点从来不在代码多难写而在于把“人等号”的焦虑翻译成数据库一行行严谨的SQL和GUI一帧帧流畅的渲染。希望帮到你。本文还有配套的精品资源点击获取
返回列表