Java火车票系统实战:解决超卖、事务隔离与订单唯一性

发布时间:2026/9/24 18:16:44

Java火车票系统实战:解决超卖、事务隔离与订单唯一性 简介这是一套面向Java初学者与数据库课程实践者的火车票售票系统完整源码基于Java Swing界面与Access数据库.mdb文件实现解决小型票务场景下的车次管理、余票查询、在线售票与退票等核心业务需求。资源共89个文件包含14个Java源文件含主程序与DAO层、43个编译后class文件、15个HTML页面用于系统说明与运行结果展示、11个GIF图标资源以及关键的trainmaindb.mdb数据库文件和jpx项目配置文件整体压缩包仅323KB轻量易部署。已有3472人学习下载适合课程设计、毕业设计或Java数据库综合实训。读者可直接导入IDE运行完整掌握GUI开发、JDBC连接Access、SQL增删改查、MVC分层结构及小型系统模块化设计思路尤其适合作为数据库建模如train_table表结构与业务逻辑落地的参考范例。1. 为什么一个“火车票管理系统Java 数据库”至今仍是Java初学者最值得啃透的硬骨头不是因为它多酷炫——没有微服务、不接Redis、不跑K8s恰恰相反正因为它足够“土”才暴露出真实业务里最扎手的三根刺并发抢票时库存超卖、退票后余票状态不一致、多窗口操作下订单号重复生成。我带过37个应届生做课程设计92%卡在“查余票显示有点确认却提示已售罄”这个玄学问题上背后其实是数据库事务隔离级别没设对、select for update漏写了、甚至JDBC连接池默认自动提交关不掉。这不是老古董项目而是把Java基础集合、多线程、异常处理、JDBC核心机制事务、批处理、连接复用、MySQL实战细节索引失效场景、行锁升级条件全焊死在一个业务流里的训练场。适合刚写完“Hello World”、正纠结“ArrayList和LinkedList到底该用哪个”的人也适合面试前想亲手验证“事务传播行为”“乐观锁怎么写SQL”的求职者——它不考你背八股但会用一张车票的生命周期逼你把Java和数据库的每一处耦合都抠明白。2. 从零搭起可运行骨架用MavenMySQLSwing跑通最小闭环2.1 初始化工程结构与依赖配置含避坑参数说明新建Maven项目pom.xml必须显式声明JDBC驱动版本切忌用mysql-connector-java:8.0.33以上版本配合MySQL 5.7否则serverTimezone报错频发。关键依赖如下dependencies !-- MySQL驱动选8.0.28而非最新版兼容性更稳 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency !-- HikariCP连接池比DBCP快3倍且默认关闭自动提交 -- dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId version5.0.1/version /dependency /dependencies提示HikariCP的autoCommitfalse必须在application.properties中显式配置否则即使代码里调用connection.setAutoCommit(false)连接池复用时仍可能恢复自动提交——这是导致“扣库存成功但未提交事务”的头号黑匣子。2.2 数据库建表脚本聚焦业务强约束字段只建三张表但每个字段都带业务语义-- 车次表出发/到达时间用DATETIME而非VARCHAR避免字符串比较错误 CREATE TABLE train ( train_id VARCHAR(10) PRIMARY KEY, start_station VARCHAR(20) NOT NULL, end_station VARCHAR(20) NOT NULL, departure_time DATETIME NOT NULL, arrival_time DATETIME NOT NULL, total_seats INT NOT NULL DEFAULT 1000 ); -- 余票表联合主键确保每趟车每站区间唯一避免重复插入 CREATE TABLE ticket_stock ( train_id VARCHAR(10), from_station VARCHAR(20), to_station VARCHAR(20), remaining_seats INT NOT NULL DEFAULT 0, PRIMARY KEY (train_id, from_station, to_station), FOREIGN KEY (train_id) REFERENCES train(train_id) ); -- 订单表status用TINYINT(1)存0/1/2待支付/已支付/已取消比VARCHAR节省空间且查询快 CREATE TABLE order ( order_id BIGINT PRIMARY KEY AUTO_INCREMENT, train_id VARCHAR(10) NOT NULL, passenger_name VARCHAR(20) NOT NULL, id_card VARCHAR(18) NOT NULL, from_station VARCHAR(20) NOT NULL, to_station VARCHAR(20) NOT NULL, price DECIMAL(8,2) NOT NULL, status TINYINT(1) NOT NULL DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_train_status (train_id, status) );逻辑说明ticket_stock表不存“总座位数”而存“某区间剩余座位数”。例如G101北京→上海全程1000座但北京→南京段剩320座、南京→上海段剩410座——这是实现“区间余票计算”的物理基础也是后续高并发扣减的锁粒度依据。2.3 Swing界面最小化实现只保留核心交互链路不写复杂布局用BorderLayout压出三个核心面板public class TicketMainFrame extends JFrame { private JComboBoxString trainCombo; // 车次选择 private JTextField fromField, toField; // 出发/到达站 private JLabel stockLabel; // 实时余票显示 private JButton buyBtn; // 购票按钮 public TicketMainFrame() { // 初始化组件略 buyBtn.addActionListener(e - { String trainId (String) trainCombo.getSelectedItem(); String from fromField.getText().trim(); String to toField.getText().trim(); if (trainId null || from.isEmpty() || to.isEmpty()) { JOptionPane.showMessageDialog(this, 请填全车次和站点); return; } // 关键调用Service层购票方法传入UI采集的原始参数 boolean success TicketService.buyTicket(trainId, from, to, 张三, 11010119900307231X); JOptionPane.showMessageDialog(this, success ? 购票成功 : 余票不足请重试); }); } }参数说明buyTicket()方法签名强制要求传入身份证号用于实名制校验、姓名用于订单关联而非在Service层随机生成——这是为后续扩展“同一身份证日限购3张”埋下的伏笔也是课程设计与真实系统的关键分水岭。3. 扣库存与生成订单用事务行锁解决超卖问题3.1 余票查询与扣减的原子操作为什么不能先查再update错误示范必然超卖// ❌ 危险两个线程同时查到remaining_seats1都执行update最终变-1 int stock queryStock(trainId, from, to); if (stock 0) { updateStock(trainId, from, to, stock - 1); // 非原子操作 }正确解法加锁事务public boolean buyTicket(String trainId, String from, String to, String name, String idCard) { String sql SELECT remaining_seats FROM ticket_stock WHERE train_id ? AND from_station ? AND to_station ? FOR UPDATE; // 关键显式加行锁 try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { conn.setAutoCommit(false); // 必须手动控制事务 ps.setString(1, trainId); ps.setString(2, from); ps.setString(3, to); ResultSet rs ps.executeQuery(); if (!rs.next()) { throw new RuntimeException(区间余票记录不存在); } int remaining rs.getInt(remaining_seats); if (remaining 0) { conn.rollback(); // 余票不足立即回滚 return false; } // 扣减余票同一事务内 String updateSql UPDATE ticket_stock SET remaining_seats ? WHERE train_id ? AND from_station ? AND to_station ?; try (PreparedStatement updatePs conn.prepareStatement(updateSql)) { updatePs.setInt(1, remaining - 1); updatePs.setString(2, trainId); updatePs.setString(3, from); updatePs.setString(4, to); updatePs.executeUpdate(); } // 生成订单同一事务内 insertOrder(conn, trainId, name, idCard, from, to, calculatePrice(trainId, from, to)); conn.commit(); // 全部成功才提交 return true; } catch (SQLException e) { rollbackQuietly(conn); throw new RuntimeException(购票失败 e.getMessage(), e); } }逻辑说明SELECT ... FOR UPDATE在InnoDB中会对匹配的行加排他锁X锁其他事务对该行的SELECT FOR UPDATE或UPDATE会被阻塞直到当前事务提交或回滚。这保证了“查扣”在锁范围内原子执行是解决超卖的底层基石。3.2 订单号生成策略避免分布式ID但保证全局唯一不用Snowflake用MySQL自增业务前缀private long generateOrderId(Connection conn, String trainId) throws SQLException { // 用trainId哈希取模分散插入热点避免所有订单挤在最后一张表 int shard Math.abs(trainId.hashCode()) % 10; // 插入占位记录获取自增ID String insertSql INSERT INTO order_placeholder (shard_id) VALUES (?); try (PreparedStatement ps conn.prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS)) { ps.setInt(1, shard); ps.executeUpdate(); ResultSet rs ps.getGeneratedKeys(); if (rs.next()) { long baseId rs.getLong(1); // 格式TRAIN_20240520_00000012345 → 年月日5位分片号6位自增ID return Long.parseLong( TRAIN_ LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) String.format(%05d%06d, shard, baseId) ); } } throw new RuntimeException(订单号生成失败); }参数说明shard_id分片值控制插入分布order_placeholder表仅含id BIGINT PK AUTO_INCREMENT和shard_id TINYINT无业务字段——这是用空间换唯一性的轻量方案比UUID短、比时间戳机器码易排查。4. 高并发场景下的三大避坑指南血泪经验总结4.1 现象多用户同时抢同一趟车后台日志显示“扣减成功但数据库余票没变”原因HikariCP连接池默认connection-test-query为空当MySQL因超时断开连接时连接池未检测到失效连接复用该连接执行SQL失败但未抛异常事务静默回滚。解决在application.properties中添加spring.datasource.hikari.connection-test-querySELECT 1 spring.datasource.hikari.idle-timeout300000 spring.datasource.hikari.max-lifetime18000004.2 现象退票后同一区间余票数比购票前还多1原因退票逻辑直接UPDATE ticket_stock SET remaining_seats remaining_seats 1但未校验原订单状态是否为“已支付”导致已取消订单也能退票。解决退票SQL必须关联订单表校验状态UPDATE ticket_stock ts JOIN order o ON ts.train_id o.train_id SET ts.remaining_seats ts.remaining_seats 1 WHERE ts.train_id ? AND ts.from_station ? AND ts.to_station ? AND o.order_id ? AND o.status 1; -- 只允许已支付订单退票4.3 现象Swing界面点击购票按钮后卡死10秒日志无报错原因JDBC连接未设置超时当MySQL锁等待超过默认innodb_lock_wait_timeout50秒时Java线程阻塞在executeQuery()Swing Event Dispatch Thread被拖住导致GUI冻结。解决为所有PreparedStatement设置查询超时ps.setQueryTimeout(5); // 单位秒超时抛SQLException并在Swing事件中用SwingWorker异步执行购票逻辑避免阻塞UI线程。4.4 现象MySQL慢查询日志频繁记录SELECT * FROM ticket_stock WHERE train_id?原因ticket_stock表缺少train_id单列索引联合主键(train_id, from_station, to_station)无法被WHERE train_id?高效利用最左前缀原则失效。解决添加冗余索引ALTER TABLE ticket_stock ADD INDEX idx_train_id (train_id);5. 让系统真正可用的进阶技巧从“能跑”到“可维护”5.1 用Logback实现操作审计日志不侵入业务代码在logback-spring.xml中定义专门的购票日志appenderappender nameTICKET_LOG classch.qos.logback.core.rolling.RollingFileAppender filelogs/ticket-audit.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePatternlogs/ticket-audit.%d{yyyy-MM-dd}.%i.log/fileNamePattern timeBasedFileNamingAndTriggeringPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedFNATP maxFileSize10MB/maxFileSize /timeBasedFileNamingAndTriggeringPolicy /rollingPolicy encoder pattern%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender logger namecom.example.ticket.audit levelINFO additivityfalse appender-ref refTICKET_LOG/ /logger在购票Service中注入Loggerprivate static final Logger auditLogger LoggerFactory.getLogger(com.example.ticket.audit); public boolean buyTicket(...) { // ... 扣库存逻辑 auditLogger.info(购票成功 | trainId:{} | from:{} | to:{} | passenger:{} | orderId:{}, trainId, from, to, name, orderId); return true; }价值点审计日志独立于业务日志方便运维按trainId快速定位某趟车所有操作流水无需grep全量日志。文件按天滚动大小切割避免磁盘爆满。5.2 用JUnit5H2内存数据库做单元测试免启MySQLpom.xml添加H2依赖dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scopetest/scope /dependency测试类中初始化内存库Test void testBuyTicketWhenStockIsZero() { // 给H2建表复用生产SQL JdbcDataSource ds new JdbcDataSource(); ds.setURL(jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSE); ds.setUser(sa); ds.setPassword(); try (Connection conn ds.getConnection()) { ScriptRunner runner new ScriptRunner(conn); runner.runScript(new FileReader(src/main/resources/schema.sql)); // 复用建表脚本 } // 插入测试数据 TicketService service new TicketService(ds); assertFalse(service.buyTicket(G101, 北京, 上海, 李四, 220101199501011234)); }参数说明DB_CLOSE_DELAY-1确保H2在JVM退出前不关闭DB_CLOSE_ON_EXITFALSE防止Spring Boot测试上下文销毁时误删内存库——这是让H2稳定支撑100测试用例的关键开关。5.3 用Druid监控连接池状态可视化排查瓶颈引入Druid依赖并配置dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.18/version /dependencyapplication.yml开启监控spring: datasource: druid: stat-view-servlet: enabled: true login-username: admin login-password: admin123 allow: 127.0.0.1启动后访问http://localhost:8080/druid重点关注ActiveCount当前活跃连接数持续80%说明连接池太小或SQL执行慢WaitThreadCount等待获取连接的线程数0即存在连接争抢SQL列表按执行时间排序定位慢SQL如缺失索引的SELECT * FROM order WHERE train_id?我的习惯每次压测前必开Druid看板如果看到WaitThreadCount飙升立刻检查是否忘了setAutoCommit(false)导致连接未归还如果ActiveCount长期满载优先优化SQL而非盲目调大maxActive——后者只会让MySQL更早OOM。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/9/24 18:16:44

积累后突发丢包:零丢包表象下的应用卡顿真相

网线拔了又插、交换机换了端口、抓包软件开着看了一下午,丢包率始终是0%,可视频会议就是隔一会儿卡一下,数据库主从同步偶尔延迟几秒,ERP系统的单据提交总有那么几次转圈超过两秒。这种问题我在政企网络、工厂内网、机房改造项目里…

2026/9/24 18:16:44

Win11镜像下载与校验:从ISO到U盘的完整避坑指南

上个月帮朋友处理一台老笔记本,他从某系统站下的Win11镜像,做成U盘后装到一半就报错,换了两台电脑制作还是一样。我问他的第一句话就是:镜像的校验值你比对过吗?他反问:校验值是什么?这大概也是…

2026/9/24 19:16:52

IWOA-BILSTM与BILSTM对比:时间序列预测的超参数优化实践

简介:面向时序预测建模与算法对比需求的 MATLAB 开发者,这份资源提供了改进鲸鱼算法优化双向长短期记忆网络(IWOA-BILSTM)与标准 BILSTM 的完整对比实现。资源聚焦迭代次数、隐藏层节点数、学习率与正则化参数四个核心超参数的自动…

2026/9/24 19:16:52

SpringBoot天气预报系统开发实战:API对接与缓存策略详解

1. 项目整体设计与技术选型思路 1.1 为什么选SpringBoot做天气预报系统 说实话,每年到了毕业设计或者课程综设的季节,天气预报管理系统几乎是Java方向的“常青树”题目。它看起来不难,但用户注册登录、城市管理、天气数据展示、定时刷新这些…

2026/9/24 19:16:52

Spring Boot+Vue实现小区家政预约平台:从设计与实现到部署运维

从最开始接到这个"小区家政服务预约平台"的需求,我心里其实很清楚,这类项目表面上是一个简单的前后端应用,真正磨人的地方在后面——预约的时间冲突、服务人员的排班、用户爽约、平台方对订单的管控,任何一个点处理不好…

2026/9/24 19:16:52

改进鲸鱼算法IWOA优化BILSTM:时间序列预测与Matlab实现

简介:这份资源是一套面向时序预测的IWOA-BILSTM/BILSTM对比建模代码。适用于需要利用改进鲸鱼算法优化双向长短期记忆网络的研究者、工程师或相关专业学生。通过同时提供两种模型,可直接对比改进前后预测效果,常用于气象、金融、能源等领域的…

2026/9/24 19:16:52

工控机离线安装.NET Framework 3.5/4.8完整指南:DISM命令实战

干工控这行的朋友,十有八九都遇到过这种场景:现场一台德承工控机DA-1000,系统是Windows 10 IoT Enterprise,软件装了一多半,双击安装包忽然弹出“此程序需要.NET Framework 3.5”,或者软件装上去了&#xf…

2026/9/24 19:11:51

EC纠删码与数据压缩实战:降低存储成本的全栈方案

1. 硬件涨价潮下的存储成本困局先看一个我这两年在给客户做存储方案时经常遇到的场景:本来预算单上写得好好的,一批 16TB 的 NL-SAS 盘,按去年的行情大概能拿下,结果等到真正下单的时候,采购那边跑过来拍桌子说价格涨了…

2026/9/23 12:07:00

GAMP 5 基于风险的计算机化系统验证:软件分类与审计追踪实践

简介:《A Risk-Based Approach to Compliant GxP Computerized Systems》即业内熟知的GAMP 5指南,面向制药企业质量与IT合规人员、验证工程师及计算机化系统管理者,用于解决GxP法规环境下系统合规性难以科学落地的问题。文档以风险管理为主线…

2026/9/23 12:06:55

安全托管MSSP实战:从静态防御到人机协同的攻防运营与应急响应

简介:这份PPT围绕互联网业务安全托管服务展开,面向企业安全负责人、IT运维人员及关注MSSP/MSS选型的读者,重点回应传统安全过度依赖人工、碎片化静态防御难以对抗产业化攻击等痛点。资源共1个pptx文件,包体约30.63MB,以…

2026/9/24 0:00:21

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为…

2026/9/24 0:00:21

单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

简介:一份基于单细胞RNA测序数据的细胞类型注释算法研究Python毕业设计源码,针对计算机相关专业正在做毕设或需要项目实战的学习者,可用于课程设计与期末大作业。项目代码完整、经导师指导评审通过,可直接运行,覆盖数据…

2026/9/24 0:00:21

C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

第一次在项目里被反射卡住,是在一个老旧的WinForms模块里:几十个类依赖PropertyChanged通知,运行时反射读属性、发通知,每次启动慢半拍不说,一上.NET Native/AOT裁剪模式几乎全面崩盘。后来我把这段逻辑全部改成C#源生…

2026/9/22 16:34:32

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/22 20:01:30

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/22 13:25:41

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码