ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java Swing+MySQL模拟12306购票:从JDBC到并发防超卖实战

Java Swing+MySQL模拟12306购票:从JDBC到并发防超卖实战 简介一份基于Java与MySQL开发的仿12306购买火车票入门项目定位为Java数据库编程与图形界面结合的练手Demo。项目实现从车次查询、座位选择、订单生成到支付确认的完整购票流程重点练习JDBC操作MySQL、多线程模拟并发购票、事务处理保证数据一致性等核心技术。压缩包共61个文件其中包含12个Java源码文件、39个class编译产物、3个外部依赖库压缩包另有数据库连接池配置与每日一言模块文件整体体积约为4.73MB目录清晰适合直接导入开发环境。已有665人学习浏览对正在完成课程设计或想理解桌面应用与数据库交互的开发者很有帮助。通过研读该资源可以学会使用连接池优化数据库访问、编写带事务的购票逻辑、设计火车票相关数据表也能体会并发场景下共享数据的处理方式是一份兼具教学与参考价值的实战资料。 最近有朋友问我学完Java基础之后该拿什么项目练手。我翻出自己当初写的那个myticket.zip——一个用Java Swing仿12306窗口购票的Demo后端接MySQL存储车次和余票。今天把这套实现完整拆开聊聊从数据库设计到并发购票的防超卖逻辑都过一遍。这东西适合刚学完Java SE、想理解JDBC和MySQL实操的读者也适合准备Java面试时梳理并发控制知识的人。麻雀虽小五脏俱全该踩的坑一个没落下。1. 项目整体设计与思路拆解1.1 核心需求到底有哪些表面上这是个“模拟买火车票”的小程序真正拆开之后其实有四个独立的知识点图形界面用Swing画一个售票窗口包括查询条件、车次表格、购票按钮和状态提示。数据存储车次信息、余票数量、订单记录不能写死在内存里必须放到MySQL程序重启后数据还在。业务逻辑查车次、看余票、点购票、扣库存、生成订单每一步都要符合现实预期。并发安全这是整个Demo最有价值的地方。多个售票窗口多线程同时抢最后一张票时不能出现一票多卖。这四个点正好对应JavaWeb岗前必学的几大块GUI基础、JDBC操作、SQL设计、线程并发。这也是为什么我一直建议初学者别一上来就学Spring Boot全家桶先用纯Java SE JDBC Swing把底层原理吃透后面上框架时你会感谢自己写过这个Demo。1.2 为什么选Swing加MySQL这个组合我见过不少人纠结技术选型有人用JavaFX有人直接做Web端还有人嫌MySQL太重想用SQLite。我的建议是教学级项目别在UI框架上花太多心思Swing是JDK自带的不需要引额外依赖双击就能跑JavaFX配置繁琐对新手不友好Web端则把重心转移到了前端技术上反而弱化了数据库和并发这两个核心训练点。MySQL就更不用说了国内Java后端岗位几乎默认要求MySQL这个Demo正好让你把建库、建表、JDBC连接、事务这些基础操作练熟。相比SQLiteMySQL在并发控制和事务支持上更接近真实生产环境练出来的经验能直接迁移到工作中。2. 数据库设计三张表撑起全部业务2.1 表结构设计思路先上建表SQL这是整个项目的地基CREATE DATABASE myticket DEFAULT CHARACTER SET utf8mb4; CREATE TABLE train ( train_no VARCHAR(10) PRIMARY KEY, train_name VARCHAR(50) NOT NULL, start_station VARCHAR(30) NOT NULL, end_station VARCHAR(30) NOT NULL, start_time TIME NOT NULL, end_time TIME NOT NULL, price DECIMAL(10,2) NOT NULL, remaining INT NOT NULL DEFAULT 0 ); CREATE TABLE ticket_order ( order_id BIGINT PRIMARY KEY AUTO_INCREMENT, train_no VARCHAR(10) NOT NULL, passenger_name VARCHAR(30) NOT NULL, passenger_id_card VARCHAR(18) NOT NULL, buy_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, status TINYINT NOT NULL DEFAULT 1, CONSTRAINT fk_order_train FOREIGN KEY (train_no) REFERENCES train(train_no) );这里说两个新手容易踩的坑。第一价格字段千万别用FLOAT或者DOUBLE浮点数在MySQL里存储会有精度丢失比如19.9存进去可能变成19.899999。正确做法是DECIMAL(10,2)精确到分。第二数据库字符集一定要用utf8mb4而不是utf8MySQL的utf8是utf8mb3不支持emoji和生僻字后续如果要存用户昵称这种字段直接乱码给你看。2.2 字段设计和冗余取舍train表里我直接把剩余票数remaining放在车次表里没有单独拆一张余票表。有人可能会问真实系统不是按座位类型二等座、一等座、卧铺分别存余票吗没错但Demo阶段最重要的目的是把并发控制讲清楚一张表一个字段最直观。如果你想把项目做得更完整可以加一张seat_type表一个车次对应多条座位类型记录每类座位有自己的余票数。这个扩展放到后面再说。订单表里有个status字段默认1代表有效订单。这个字段现在看着多余但其实是为退票功能预留的。真实系统里订单不会物理删除而是通过状态字段标记取消或已退票保留历史数据方便对账和统计。如果你想让Demo更完善可以把退票功能加上将status改成0同时把对应车次的remaining加回来。2.3 为什么要用外键外键这个事网上争议很大大厂生产环境普遍禁用外键因为会影响写入性能。但Demo阶段我强烈建议加上它能帮你保证数据完整性。比如你插入一个订单train_no对应的车次根本不存在外键约束会直接报错而不是让你莫名其妙地生成一条脏数据。等你工作几年后明白为什么生产环境不用外键了再把这个约束去掉也不迟。学习阶段让数据库帮你兜底是好事。3. 购票核心流程界面、事务与并发控制3.1 窗口界面与操作流Swing窗口结构很常规顶部是查询区出发站、到达站、查询按钮中间是JTable车次列表显示车次号、站名、时间、票价、余票底部是操作区乘客姓名、身份证号输入框购票按钮和状态提示栏。操作流程是输入出发站和到达站点击查询JTable刷新车次列表选中一行车次填入乘客信息点击购票按钮程序执行扣余票和插入订单成功后在状态栏提示“购票成功”失败则提示“余票不足”或“系统异常”。界面本身没什么技术含量但有一个细节值得注意Swing是单线程模型所有UI更新必须在事件分发线程EDT中执行。如果你直接在点击事件里执行数据库查询网络慢的时候整个窗口会卡死用户点哪里都没反应。我当初写第一版就踩了这个坑后来用SwingWorker把耗时操作放到后台线程查询期间界面还能正常刷新体验才正常。3.2 JDBC查询PreparedStatement防注入查询车次的代码是JDBC最标准的写法public ListTrain searchTrain(String start, String end) { String sql SELECT * FROM train WHERE start_station ? AND end_station ?; ListTrain result new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, start); ps.setString(2, end); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Train train new Train(); train.setTrainNo(rs.getString(train_no)); train.setTrainName(rs.getString(train_name)); train.setStartStation(rs.getString(start_station)); train.setEndStation(rs.getString(end_station)); train.setPrice(rs.getBigDecimal(price)); train.setRemaining(rs.getInt(remaining)); result.add(train); } } } catch (SQLException e) { e.printStackTrace(); } return result; }这段代码有两个关键点。第一所有SQL都用PreparedStatement而不是拼接字符串。新手最容易犯的错就是写成SELECT * FROM train WHERE start_station start 一旦用户输入 OR 11 --整个表的数据全被查出来这就是典型的SQL注入。第二用try-with-resources语法Connection、PreparedStatement、ResultSet三个资源自动关闭不用写繁琐的finally块也不容易漏关连接导致数据库连接耗尽。3.3 并发购票与超卖问题的解决方案这是整个Demo含金量最高的部分。想象一个场景G102次列车只剩最后一张票售票窗口A和窗口B同时收到用户请求。如果代码先查余票再判断余票是否大于0最后执行扣减两个窗口可能同时查到remaining1然后都认为自己能买票最后结果就是卖出去两张票——超卖。解决超卖的核心思路就是一句话让“检查余票”和“扣减余票”成为一个不可分割的原子操作。在单机MySQL场景下最直接的办法是条件更新String updateSql UPDATE train SET remaining remaining - 1 WHERE train_no ? AND remaining 0; PreparedStatement ps conn.prepareStatement(updateSql); ps.setString(1, trainNo); int rows ps.executeUpdate(); if (rows 0) { conn.rollback(); // 提示“余票不足” return; }这个方法为什么有效因为UPDATE语句会对命中的行加行锁。两个事务同时执行这条UPDATEInnoDB引擎会让它们排队执行第一个事务更新成功影响行数为1然后继续插入订单并提交第二个事务等第一个提交后才能执行此时remaining已经是0UPDATE条件remaining 0不成立影响行数为0代码检测到这个结果就知道余票没了直接回滚。从根源上杜绝了超卖。用线程池模拟20个窗口并发抢5张票实测下来最终订单数永远是5没有一次超卖。这就是数据库行锁的威力。另外提一句悲观锁方案SELECT ... FOR UPDATE先把行锁住再查余票数量也能解决问题但锁持有时间更长性能不如条件更新。3.4 购票事务扣库存和生订单必须同生共死扣了余票却没生成订单或者生成了订单但票没扣掉这两种情况都是数据不一致。所以购票逻辑必须放在同一个事务里Connection conn DBUtil.getConnection(); conn.setAutoCommit(false); try { // 1. 条件更新扣减余票 // 2. 插入订单记录 conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.close(); }注意conn.setAutoCommit(false)这句必须在try之前执行确保整个流程手动控制提交。如果某个步骤抛出异常catch块里执行conn.rollback()数据库会撤销所有未提交的修改回到事务开始前的状态。这就是事务的原子性理解了这个面试被问“什么是事务ACID”时你就有了活生生的例子。3.5 连接管理Demo阶段也要有良好的连接习惯Demo阶段很多人的写法是每个方法里都调DriverManager.getConnection()用完就关。这个方案勉强能用但一旦并发线程变多频繁创建和销毁连接的开销非常明显。更严重的问题是如果你忘了关连接MySQL最大连接数默认151很快会被耗尽程序报错Too many connections。我给这个Demo封装了一个简单的DBUtil工具类核心就一个方法public static Connection getConnection() throws SQLException { String url jdbc:mysql://localhost:3306/myticket?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; String user root; String password 你的密码; return DriverManager.getConnection(url, user, password); }同时所有使用连接的方法都强制用try-with-resources保证连接一定关闭。生产环境当然要用HikariCP这类连接池但Demo阶段先把“谁开谁关”这个习惯养成更重要。4. 常见问题与排查技巧实录4.1 中文乱码这个Demo最常遇到的问题就是中文乱码。车次站名显示成问号或者一堆乱码绝大多数情况是字符集不统一。检查三个地方建库时用没用DEFAULT CHARACTER SET utf8mb4JDBC连接URL有没有带characterEncodingutf8MySQL服务端默认字符集是否正确。排查方法很简单在命令行执行SHOW VARIABLES LIKE character_set%;确认全链路都是utf8系列即可。4.2 MySQL 8.x驱动类和时区问题很多新手还在用老旧的com.mysql.jdbc.Driver这句话在MySQL 5.x没问题但到了MySQL 8.x已经废弃了。正确写法是com.mysql.cj.jdbc.Driver。另外MySQL 8.x还有一个经典坑不指定时区会报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized看似乱码其实是时区问题。在URL加上serverTimezoneAsia/Shanghai即可解决或者建连接时执行SET time_zone 08:00。4.3 连接耗尽报错现象是程序跑一段时间后突然报错提示获取连接超时。排查过程很简单打开MySQL的SHOW PROCESSLIST;命令能看到一大堆Sleep状态的连接基本就是代码里连接没关。我用SELECT COUNT(*) FROM information_schema.processlist;确认连接数爆表后把代码改成try-with-resources问题立刻消失。那些Sleep连接大多是查询完忘记调close或者是ResultSet没有正常遍历完导致连接无法释放。4.4 死锁问题在并发测试中我曾经遇到过Deadlock found when trying to get lock的报错。原因是两个事务试图以不同顺序更新资源事务A先更新车次表再插入订单表事务B先插入订单表再更新车次表两个操作互相持有对方需要的锁就死锁了。解决办法是统一SQL执行顺序所有线程都先扣余票再插订单死锁概率大大降低。InnoDB检测到死锁会自动回滚其中一个事务程序再写个重试逻辑就能处理。4.5 排查速查表现象可能原因解决方案中文显示为乱码字符集不统一库用utf8mb4URL加characterEncodingutf8ClassNotFoundException驱动类名错误MySQL 8.x使用com.mysql.cj.jdbc.Driver时区解析报错未指定serverTimezoneURL加serverTimezoneAsia/Shanghai余票出现负数扣减未加余票条件UPDATE语句带remaining 0连接耗尽连接未关闭使用try-with-resources界面卡死无响应耗时操作阻塞EDT使用SwingWorker异步执行购票成功但列表没刷新未重新查询数据提交事务后重新执行查询刷新表格5. myticket.zip的项目结构与扩展方向5.1 项目文件目录解压myticket.zip后目录结构是这样的myticket/ ├── sql/ │ └── init.sql -- 建库建表脚本 ├── lib/ │ └── mysql-connector-java-8.0.33.jar ├── src/ │ └── com/myticket/ │ ├── Main.java -- 启动入口 │ ├── db/ │ │ └── DBUtil.java -- 数据库连接工具 │ ├── model/ │ │ ├── Train.java -- 车次实体类 │ │ └── Order.java -- 订单实体类 │ ├── dao/ │ │ ├── TrainDao.java -- 车次查询 │ │ └── OrderDao.java -- 订单插入与事务 │ └── ui/ │ ├── LoginFrame.java -- 登录窗口 │ └── TicketFrame.java -- 购票主窗口 └── README.md这个结构是按MVC思想拆的Model实体类、ViewSwing窗口、ControllerDao层逻辑分离。项目虽小但分层清晰后面理解Spring的三层架构会非常顺滑。5.2 从Demo到能上线的系统还差什么如果想把myticket升级成一个真正可用的购票系统有几个方向可以扩展。第一加用户注册登录功能密码存储用加盐哈希不能明文存库。第二加余票分类型管理把remaining字段拆成二等座余票、一等座余票等每一类用独立的行锁控制。第三加退票改签流程订单状态机从单一值变成多状态流转。第四引入Redis分布式锁替代数据库行锁解决多实例部署下的并发问题。这些扩展每一个都能单独写成一篇博客但核心思想和这个Demo一模一样保证数据一致性和并发安全。5.3 面试时怎么讲这个项目很多人做完了项目但面试时讲不清楚其实就按这个思路讲先说自己用Swing实现图形界面MySQL存储数据JDBC做数据库交互。然后重点讲并发购票这个亮点——遇到超卖问题后如何分析问题、如何设计条件更新SQL、如何用事务保证一致性最后说明压测结果。面试官最想听的就是你“发现问题、分析问题、解决问题”的完整链路这比项目多炫技重要得多。我个人写这个Demo最深的体会是真正的技术难点不在UI而在数据一致性。窗口上画个按钮谁都会但让20个线程同时抢5张票还能保持余票不为负这才是程序员基本功的体现。最后分享一个小技巧购票成功后记得重新执行一次车次查询并刷新JTable不然界面上余票数字纹丝不动用户会以为你的程序没反应。这个细节不贵但体验提升非常明显。本文还有配套的精品资源点击获取
返回列表