尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Java的电影院购票系统:从SQL设计到事务并发实战解析

基于Java的电影院购票系统:从SQL设计到事务并发实战解析 简介基于 Java 的电影院购票系统源码与数据库脚本整合包面向 Java 学习者、课程设计及毕业设计人员。项目采用 MVC 分层设计逻辑上分离模型与视图以降低耦合既可采用 SSM 或 SpringBoot 等主流框架进行重构也可基于 Servlet 自定义封装帮助理解 JavaWeb 开发全流程。资源压缩包共 970 个文件约 18.43MB包含 jsp/servlet 页面、Java 类源码、数据库 SQL 脚本以及大量前端静态资源js/css/gif/jpg 等并附带 jar 依赖、XML 配置文件等基本构成可直接运行的完整工程。文件类型覆盖 Java、JSP、HTML、XML、properties 及多种图片格式便于对照学习界面布局、业务逻辑与持久层映射。资源已吸引 2792 人学习下载。通过源码可以研究购票系统中影片管理、场次安排、选座购票、订单生成等典型模块的实现思路数据库脚本提供初始表结构与示例数据减少环境搭建成本。整体适合用于项目实训、毕业设计二次开发也可作为熟悉 MVC 架构与主流 Java 框架整合的参考案例。1. 基于 Java 的电影院购票系统源码和 SQL 脚本到底在讲什么标题里的“基于java”不是空话。这类毕业设计与练手项目最常见的组合是 Java Swing/JavaFX 做客户端、MySQL 存数据、JDBC 连库最终把源码工程加一份 cinema.sql 打成 rar 包分发。它要解决的并不是分布式架构或高并发中间件而是最典型的信息系统闭环用户能注册登录、查看电影排片、选座下单、生成票管理员能维护影片和场次。能搜到并下载这个压缩包的人多数不是缺思路而是卡在“解压后怎么让代码和数据库对上”“为什么表结构和实体类对不上”“两个人同时选最后一个座位为什么会超卖”这些落地问题上。接下来按我整理这类项目时的顺序展开先立表再拆码再谈事务最后说排错和兜底。2. 数据库 SQL 先行场次、座位、订单与票的状态约束拿到 rar 先别急着看 Java 代码先打开 sql 文件读表结构。电影院购票最忌讳一张表存所有字段常见做法拆成影片、场次、座位、订单、票五到六张表。表的数量不是越多越好但要能回答一个问题一个用户买一张票数据从哪些表产生、哪些字段变化。2.1 一张票房表拆成 film、session、seat、ticket 的原因如果把“某部电影某天某厅某座卖了没”全部塞进一张表字段会越加越多最后连座位状态和订单号都混在一起。常规设计是让每张表只盯一个业务对象film 存影片基础信息session 存“什么时间在几号厅放哪部片”seat 存某个场次下每个座位的状态ticket 记录某张订单买了哪个座的票。下面是一份能直接执行的 MySQL 建表脚本CREATE DATABASE IF NOT EXISTS cinema DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE cinema; DROP TABLE IF EXISTS t_ticket; DROP TABLE IF EXISTS t_orders; DROP TABLE IF EXISTS t_seat; DROP TABLE IF EXISTS t_session; DROP TABLE IF EXISTS t_film; DROP TABLE IF EXISTS t_user; CREATE TABLE t_film ( film_id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, duration INT NOT NULL COMMENT 片长分钟, price DECIMAL(10,2) NOT NULL ) ENGINEInnoDB; CREATE TABLE t_session ( session_id INT PRIMARY KEY AUTO_INCREMENT, film_id INT NOT NULL, hall_no VARCHAR(20) NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, CONSTRAINT fk_sess_film FOREIGN KEY (film_id) REFERENCES t_film(film_id) ) ENGINEInnoDB; CREATE TABLE t_seat ( seat_id INT PRIMARY KEY AUTO_INCREMENT, session_id INT NOT NULL, row_no INT NOT NULL, col_no INT NOT NULL, seat_status TINYINT NOT NULL DEFAULT 0 COMMENT 0可用 1锁定 2已售, UNIQUE KEY uk_session_seat (session_id, row_no, col_no), CONSTRAINT fk_seat_sess FOREIGN KEY (session_id) REFERENCES t_session(session_id) ) ENGINEInnoDB; CREATE TABLE t_orders ( order_id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id INT NOT NULL, session_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB; CREATE TABLE t_ticket ( ticket_id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, session_id INT NOT NULL, seat_id INT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已出票, CONSTRAINT fk_ticket_order FOREIGN KEY (order_id) REFERENCES t_orders(order_id) ) ENGINEInnoDB;参数说明t_seat 的UNIQUE KEY uk_session_seat (session_id, row_no, col_no)是数据库层面保证“同一场次同一行同列只有一个座位记录”这一行会在第四章作为兜底条件再次出现。ticket 表没有在 session_id seat_id 上建唯一索引这是很多演示项目的通病后面会用一条 ALTER 补上。ENGINE 统一用 InnoDB因为后续购票依赖行锁和事务MyISAM 不适合这类写多读多且要回滚的场景。2.2 座位状态维护在 seat 表还是由 ticket 推导两份常见设计一份是 t_seat.seat_status 直接标记座位是否可用另一份是不维护状态字段每次查“某场次的已售座位”都去 t_ticket 里 count。演示项目绝大多数选前者因为代码直白选中座位时执行一条 UPDATE 把 0 改成 1受影响行数为 0 说明座位刚被别人抢走。缺点是要时刻保证 seat_status 与订单、票一致。后者更“规范”但查询可用座位要写子查询或关联初学者在这上面容易绕晕。我的建议是课设和内部项目用状态字段等真的要考虑并发准确性时再叠加唯一约束兜底而不是推翻表结构。2.3 执行 SQL 脚本的固定动作字符集、删表顺序、重复执行解压 rar 后第一件事是把 sql 导进本地 MySQL。命令行导入时建议写成mysql -uroot -p --default-character-setutf8mb4 cinema.sql加上--default-character-setutf8mb4是为了避免 Windows 命令行默认 gbk 导致中文片名变成乱码。脚本开头的SET NAMES utf8mb4也要保留它告诉服务端当前客户端发送来的字符集。另一个关键是 DROP 顺序先删 ticket、orders再删 seat、session最后删 film 和 user否则外键约束会报删除失败。脚本里用DROP TABLE IF EXISTS是为了让你可以反复执行对“源码数据库sql”这种交付物来说可重复执行比一次性建表更能减少使用者的挫败感。3. 拆开 Java 源码实体、DAO、Service 与界面怎么分工SQL 落地后再看 Java 源码就不会一头雾水。Swing 项目最常见的分层是 entity、dao、service、ui、util 五个包。理解分层的意义不只是为了应付答辩而是排查问题时有明确方向SQL 报错去 dao 找业务逻辑不对去 service 找按钮没反应去 ui 找。3.1 解压 rar 后的标准包结构先对号入座cinema/ ├── sql/ │ └── cinema.sql ├── src/ │ ├── com/cinema/entity/ │ │ ├── Film.java │ │ ├── Session.java │ │ ├── Seat.java │ │ ├── Order.java │ │ └── Ticket.java │ ├── com/cinema/dao/ │ │ ├── FilmDao.java │ │ ├── SessionDao.java │ │ ├── SeatDao.java │ │ └── OrderDao.java │ ├── com/cinema/service/ │ │ └── BookingService.java │ ├── com/cinema/ui/ │ │ ├── LoginFrame.java │ │ ├── MainFrame.java │ │ └── BookingDialog.java │ └── com/cinema/util/ │ └── DBUtil.java ├── lib/ │ └── mysql-connector-j-8.x.jar └── db.properties各层职责在下表里分得很清楚层类名示例职责常见错误entityFilm.java与 t_film 字段一一对应日期用 String 而不是 java.util.DatedaoSeatDao.java只写 SQL不写 if/else拼 SQL 字符串导致注入serviceBookingService.java事务边界和业务校验在 dao 里开事务uiMainFrame.java按钮、表格、弹窗在 UI 线程里做数据库查询utilDBUtil.java获取连接、关资源连接不关闭导致 too many connectionsdao 里只做数据访问service 里只做逻辑。比如“选座前检查余额”是 service 的事“把座位状态改成已售”是 dao 的事。很多源码包把这两层混在一起后续把 Swing 换成 JavaFX 或控制台界面时改动成本会非常大。3.2 DBUtil 的三种写法推荐用 Properties 配置连接数据库的代码不能散落在每个 dao 里。最常见做法是写一个 DBUtil启动时读取 db.properties。下面这个例子不依赖任何框架所有 Java 基础阶段的项目都能直接套用package com.cinema.util; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; public class DBUtil { private static String url; private static String user; private static String password; static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); url props.getProperty(url); user props.getProperty(user); password props.getProperty(password); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } }对应 db.properties 内容drivercom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/cinema?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai userroot password123456逻辑说明静态代码块在类加载时执行一次读取配置并注册驱动。getResourceAsStream(db.properties)要求文件在 classpath 下IDE 里通常放 src 根目录命令行编译时需要手动把它复制到输出目录。URL 里的serverTimezoneAsia/Shanghai是 MySQL 8 的硬性要求缺了会报时区错误characterEncodingutf8配合数据库 utf8mb4 才能保证中文不乱码。提示MySQL 5.7 可以用com.mysql.jdbc.DriverMySQL 8 必须换成com.mysql.cj.jdbc.Driver。如果 rar 里带的是旧版驱动类名连不上库时第一个检查点就是这里。3.3 从“查出排片”到“选座下单”的界面联动Swing 的典型套路是 JTable 显示数据选中行后把主键存在内存变量里点击按钮再触发下一步。例如场次列表加载// ui/MainFrame.java 片段 DefaultTableModel model (DefaultTableModel) sessionTable.getModel(); model.setRowCount(0); // 清空旧数据避免重复加载 for (Session s : sessionDao.findByFilm(currentFilmId)) { model.addRow(new Object[]{ s.getSessionId(), s.getStartTime().toString(), s.getHallNo(), ¥ s.getPrice() }); }参数说明setRowCount(0)是刷新 JTable 的常用手段不调用它会导致每次切换电影后表格里出现两批场次。getSessionId()作为第一列虽然能看见但它并不直接显示给用户而是点击“购票”按钮时从sessionTable.getValueAt(selectedRow, 0)取出来传给下一个窗口。这里要留意一个坑Swing 是单线程模型数据库查询耗时较长时不能让界面卡死在事件线程里至少要用一个线程池SwingWorker去做查询不少源码包里直接在主线程查库点按钮后会白屏几秒这是 Java 基础里常被面试官追问的线程问题。4. 购票事务与并发两个用户同时选最后一张票时会发生什么这是整份源码里最有含金量的地方也是面试时爱从项目里往外挖的问题。数据库 SQL 设计得再好如果 Java 代码忘了开事务就会产生超卖。影院票务和普通商品秒杀不一样的是座位是强一致资源同一个场次的同一个座位只能属于一个用户。4.1 “先查询再更新”为什么必然出错很多初级版本是这样写的查询座位状态判断是 0然后 UPDATE 成 1。两个线程同时查看到同一行是 0两个都执行 UPDATE最后都提示购票成功。解决思路不是把判断放到 Java 的同步块里因为应用只能控制自己的进程另一个节点连的是同一套 MySQL。正确做法是让数据库在 UPDATE 时做原子判断利用受影响行数。4.2 一次购票要同时处理三张表座位、订单、票下面的方法把锁座位、建订单、插入票放在同一个数据库事务里任何一步失败都回滚public void bookSeat(int sessionId, int seatId, int userId) throws Exception { Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); PreparedStatement ps1 conn.prepareStatement( UPDATE t_seat SET seat_status 1 WHERE seat_id ? AND session_id ? AND seat_status 0); ps1.setInt(1, seatId); ps1.setInt(2, sessionId); int rows ps1.executeUpdate(); if (rows 0) { throw new RuntimeException(座位不可售); } PreparedStatement ps2 conn.prepareStatement( INSERT INTO t_orders(order_no, user_id, session_id, total_amount, status) VALUES(?, ?, ?, ?, 1)); // 设置 order_no 为时间戳随机数或从序列生成器获取 PreparedStatement ps3 conn.prepareStatement( INSERT INTO t_ticket(order_id, session_id, seat_id, status) VALUES(?, ?, ?, 1)); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }逻辑说明关键的“原子判断”在 PS1 上。WHERE seat_status 0让数据库在行锁范围内检查状态两条并发请求同时执行时只有一条的executeUpdate()返回 1另一条返回 0 并触发抛出异常。setAutoCommit(false)之后到commit()之前这个连接上所有 SQL 同生共死。finally里的setAutoCommit(true)是恢复连接默认状态避免连接被归还连接池后影响下一次使用。参数说明事务隔离级别设置了READ_COMMITTED比 MySQL 默认的REPEATABLE_READ更宽松本身不带间隙锁对座位这一行的监控更简单。要不要升级成SERIALIZABLE完全没必要因为座位锁的核心已经由带条件的 UPDATE 承担级别只是控制读一致性。4.3 悲观锁、乐观锁和唯一约束在这个场景下谁更合适方案实现优点缺点悲观锁UPDATE ... WHERE seat_status0实现简单失败判断直观长事务会阻塞同一场次的其他座位写入乐观锁表加 version 列UPDATE ... WHERE version?不阻塞读冲突后需要重试代码多一层循环唯一约束兜底对 session_id seat_id 建唯一索引数据库终极防线报错信息不够友好需要翻译成“座位被抢”悲观锁适合座位这样的强竞争短事务通常几毫秒内就提交不会酿成大量阻塞。乐观锁适合读多写少、冲突概率低的场景如果日常看电影上座率只有 30%用它也没问题。看懂这里再看 MyBatis 的Version注解或相关源码解析本质都是 version 列与更新行数配合。而不管选哪种最后都应该加唯一约束否则任何业务层的漏判都会直接造成重复出票。第五章会补上这条 ALTER 语句。5. 部署、排错与从演示品变成可靠交付物r ar 解压后能不能跑起来七成问题集中在数据库驱动、字符集和 classpath 上。这一章按操作顺序过一遍然后给出一个值得写进注释里的兜底技巧。5.1 让 sql 和 Java 源码跑通的最小命令序列# 导入数据库 mysql -uroot -p --default-character-setutf8mb4 sql/cinema.sql # 编译源码假设 lib 下已有 mysql 驱动 javac -encoding UTF-8 -cp lib/* -d out \ src/com/cinema/entity/*.java \ src/com/cinema/dao/*.java \ src/com/cinema/service/*.java \ src/com/cinema/ui/*.java \ src/com/cinema/util/*.java # 把配置文件放到输出目录 cp src/db.properties out/ # 运行 java -cp lib/*:out com.cinema.ui.MainFrame-cp lib/*会引入 lib 下所有 jar注意双引号不可省否则通配符被 shell 展开变成不存在的路径。Windows 下java -cp的分隔符是分号而不是冒号把最后一句改成java -cp lib/*;out com.cinema.ui.MainFrame。对大多数下载者来说用 IDEA 或 Eclipse 打开源码目录会更省事但这种 IDE 自动处理 classpath 的方式会掩盖对编译过程的理解。5.2 三个高频报错与对应的 Java 环境变量排查报错现象常见原因处理方式ClassNotFoundException: com.mysql.cj.jdbc.Driver驱动 jar 没引入或驱动类名旧lib 下确认有驱动检查 db.properties 的 driverUnknown database cinemasql 脚本没执行或执行时报错中断重跑脚本再SHOW DATABASES;确认数据库存在Public Key Retrieval is not allowedMySQL 8 认证方式导致的连接串问题URL 追加allowPublicKeyRetrievaltrueuseSSLfalse如果命令行里java和javac均提示找不到命令回到 Java 环境变量配置去检查 PATH 是否包含 JDK 的 bin 目录这是 Java 基础排错的第一步。如果javac能运行但java报Could not find or load main class多半是-d out之后的包路径和-cp设置不一致比如直接写了com/cinema/ui/MainFrame而不是点分全限定名com.cinema.ui.MainFrame。5.3 最后一招给 t_ticket 补唯一索引并把 SQL 异常翻译成用户提示哪怕业务层已经写了带条件的 UPDATE也推荐在数据库上多设一道防线ALTER TABLE t_ticket ADD UNIQUE KEY uk_ticket_session_seat (session_id, seat_id);这一段索引能拦截一切漏网之鱼。此时 Java 侧要改动捕获逻辑不能把数据库异常直接抛到界面上try { conn.setAutoCommit(false); // 更新座位、插入订单、插入票 conn.commit(); } catch (SQLIntegrityConstraintViolationException e) { conn.rollback(); return 手慢了该座位刚刚被选走请重新选择; } catch (SQLException e) { conn.rollback(); throw new RuntimeException(购票失败请稍后重试, e); }逻辑说明SQLIntegrityConstraintViolationException是 JDBC 层对约束冲突的封装比蛮力判断e.getMessage().contains(Duplicate)可靠得多。MySQL 原生的错误码是 1062这条异常正是包裹了它。注意一个细节事务内某条 SQL 抛出异常后MySQL 会标记该事务为需要回滚的状态所以rollback()必须放在 catch 里执行不能在 finally 里省略。做完这一步即使将来有人绕过 Service 层直接调用 dao也没办法制造出同一场次同一座位的两张票。这个唯一索引比任何 Java 同步块都稳。本文还有配套的精品资源点击获取
返回列表