ARTICLE DETAIL

资讯详情

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

数据库课程设计机票预订系统:从ER模型到事务并发控制的完整设计指南

数据库课程设计机票预订系统:从ER模型到事务并发控制的完整设计指南 简介《数据库课程设计——机票预订系统.doc》是一份以机票预订业务为背景的Oracle数据库课程设计完整报告面向计算机相关专业学生以及需要综合练习ER建模和PL/SQL开发的读者。文档从需求分析入手明确航班、机票、客户等核心数据管理任务并绘制系统E-R图识别航空企业、飞机、航班、机舱、机票、乘客、业务员等实体及其联系再转换为关系模型。实现层面介绍了表空间分配、数据表创建、参数化视图设计以及存储过程、函数、触发器的编写思路同时还规划了角色权限和数据备份策略覆盖数据库设计从建模到安全管理的主要环节。资源包仅含1个doc格式文件大小约1.2MB目录结构完整配有SQL语句示例便于按步骤复现。目前已有84人学习适合正在完成数据库课程设计或希望系统梳理Oracle数据库设计流程的读者。1. 数据库课程设计机票预订系统别把课设做成“增删改查堆砌”每年数据库课程设计季总有一批人抱着“机票预订系统”这个题目最后交上去的却是一个只有两张表、四个按钮的“玩具”。 数据库课程设计机票预订系统这个题目之所以经典是因为它天然覆盖了关系数据库的绝大部分核心考点多对多关系、事务一致性、并发控制、复杂查询。但大多数人的翻车点不在功能写不出来而在数据库设计一开始就走错了方向——表拆得太碎或太粗外键乱挂事务边界模糊最后作业演示时一并发就死锁。这篇笔记不会给你一份已经写好的 .doc 文档而是把这个课设题目的完整落地路径拆开讲透从 ER 模型怎么画到建表约束怎么写到下单退票的 SQL 在事务里怎么编排再到并发压测时你大概率会踩的坑。不求文档华丽但求每个步骤你能照着复现并且经得起老师追问“为什么这样设计”。适合谁看正在做数据库课设的学生、需要快速交付一个演示系统的初学者以及想把自己的课设从“能跑”提升到“能讲出设计理由”的开发者。后面所有 SQL 和设计思路按照 MySQL 8.x Navicat 的环境来写理论和表结构换到 SQL Server 也通用。2. 机票预订系统的需求拆解从业务规则倒推表结构别先画表再想需求2.1 先把“订票”当成一个完整业务流程而不是一条 INSERT很多人的第一个错误是打开工具就开始建表一个用户表、一个航班表、一个订单表完事。但“机票预订”在教学上真正要考察的是你能否把业务流程翻译成数据流。一条完整的订票链路是这样的用户登录 → 查询航班 → 选择舱位 → 生成订单状态为待支付 → 扣减库存 → 支付成功 → 出票。这里涉及两个关键动作写订单和扣库存它们必须是一个原子操作否则就会出现“订单生成了但票没扣掉”或者“票扣了但订单失败”的脏数据。这就是课程设计打分的重要分水岭功能齐全只是及格事务边界正确才是良好到优秀。基于这个链路我们反向推导出核心实体用户、航班、舱位、订单。注意舱位不是航班的一个字符串字段而是一个独立实体。因为同一个航班有头等舱、经济舱等不同库存和价格如果把舱位塞在航班表里一个航班要多条记录反而把航班本身的属性起降时间、航班号搞得冗余。2.2 ER 模型设计三张表还是五张表关键看“座位”怎么建模这里给出一个推荐的五表模型用户表 users、航班表 flights、舱位表 cabins、订单表 orders、订单明细表 order_items。其中 cabins 承载 (flight_id, cabin_class, price, stock) 的组合orders 记录用户与订单状态order_items 记录订单里具体买了哪个航班的哪个舱位。为什么拆订单和订单明细因为一个订单理论上可能包含多张机票——虽然课设里通常一人一票但拆开更规范还能应对“同行人一起下单”的扩展追问。关系建模上要注意用户和订单是一对多订单和订单明细是一对多航班和舱位是一对多订单明细和舱位是多对一。用户与航班之间没有直接关系是通过订单明细间接关联的——这个细节在考试问答里几乎是必问的“你的系统里用户怎么关联到航班”。如果你回答“通过订单中间表”老师会点头如果你说“用户表和航班表直接加了外键”那就要扣分了。2.3 用可视化工具先画 ER 图再建表文档截图材料也有了实测有效的工作流是先用 draw.io 或 Navicat 的模型工具把 ER 图拖出来图上标注主键、外键和联系类型然后截图存证。这张图最终可以直接贴进 .doc 的设计文档里比你最后用文字描述表结构直观得多。这里给一个建表前的实体属性清单照着在工具里建模就行usersuser_id 主键username 唯一password_hashphonecreated_atflightsflight_id 主键flight_no 唯一departure_cityarrival_citydeparture_timearrival_timeairlinecabinscabin_id 主键flight_id 外键cabin_class枚举经济舱/公务舱/头等舱pricestockordersorder_id 主键user_id 外键order_no 唯一status待支付/已支付/已取消/已退票created_atpaid_atorder_itemsitem_id 主键order_id 外键cabin_id 外键passenger_nameid_cardseat_no这个模型对应的建表语句在下一章给出。重要的是你在画图阶段就要想清楚一个业务问题退票是删除订单记录还是把状态改成“已退票”这个决策直接决定你的订单表要不要保留历史数据也会在后面的避坑章节展开。3. 建表与约束落地用 SQL 把设计固化下来主外键和唯一键一个都不能少3.1 完整建表脚本含主键、外键、唯一约束、检查约束和默认值打开 Navicat 或命令行按顺序执行下面的脚本。顺序有讲究先建被引用的表父表再建引用表子表否则外键会报错找不到参照表。-- 用户表 CREATE TABLE users ( user_id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, password_hash VARCHAR(128) NOT NULL, phone VARCHAR(20), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uk_username UNIQUE (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 航班表 CREATE TABLE flights ( flight_id INT AUTO_INCREMENT PRIMARY KEY, flight_no VARCHAR(20) NOT NULL, departure_city VARCHAR(50) NOT NULL, arrival_city VARCHAR(50) NOT NULL, departure_time DATETIME NOT NULL, arrival_time DATETIME NOT NULL, airline VARCHAR(50), CONSTRAINT uk_flight_no UNIQUE (flight_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 舱位表同一航班不同舱位价格与库存分开管理 CREATE TABLE cabins ( cabin_id INT AUTO_INCREMENT PRIMARY KEY, flight_id INT NOT NULL, cabin_class ENUM(economy, business, first) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, CONSTRAINT fk_cabins_flight FOREIGN KEY (flight_id) REFERENCES flights (flight_id) ON DELETE CASCADE, CONSTRAINT uk_flight_class UNIQUE (flight_id, cabin_class), CONSTRAINT chk_stock_nonnegative CHECK (stock 0) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表 CREATE TABLE orders ( order_id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL, user_id INT NOT NULL, status ENUM(pending, paid, cancelled, refunded) NOT NULL DEFAULT pending, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, paid_at DATETIME, CONSTRAINT uk_order_no UNIQUE (order_no), CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE RESTRICT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细表 CREATE TABLE order_items ( item_id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, cabin_id INT NOT NULL, passenger_name VARCHAR(50) NOT NULL, id_card VARCHAR(20) NOT NULL, seat_no VARCHAR(5), CONSTRAINT fk_items_order FOREIGN KEY (order_id) REFERENCES orders (order_id) ON DELETE CASCADE, CONSTRAINT fk_items_cabin FOREIGN KEY (cabin_id) REFERENCES cabins (cabin_id) ON DELETE RESTRICT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;执行完后用SHOW CREATE TABLE orders;检查索引和外键是否生效。逻辑说明users 和 flights 是独立实体没有外键依赖cabins 通过 flight_id 关联航班并且用 (flight_id, cabin_class) 联合唯一键约束“同一航班不能有两个经济舱记录”这是防止数据冗余的关键orders 的 user_id 外键用 RESTRICT目的是不允许直接删除有订单的用户这在课设答辩时可以解释为“保护历史数据”。3.2 参数说明字符集、引擎、约束策略的选择理由上面脚本里几个参数值得注意。ENGINE 必须用 InnoDB因为 MyISAM 不支持事务和外键而机票预订的核心是事务这个坑每年都有人踩。CHARSET 用 utf8mb4 而不是 utf8否则存中文没问题但遇到特殊符号比如 emoji 昵称会报错。DECIMAL(10,2) 是价格字段的标准做法不要用 FLOAT 或 DOUBLE——二进制浮点数在金额计算时会产生精度误差10.2 存进去可能变成 10.199999。这个知识点在答辩时主动讲出来很加分。ON DELETE 策略的选用逻辑flights 被 cabins 引用删除航班时级联删除舱位是合理的航班都没了舱位没有存在意义orders 被 order_items 引用删除订单时级联删除明细也是合理的但 users 和 cabins 都用 RESTRICT防止误删用户和舱位导致订单明细失去参照。这套设计在数据库原理里叫“引用完整性”是课程设计的核心考点。3.3 用连接池和统一字符集避免乱码与连接数打满如果往系统里加代码比如 Java JDBC注意连接池参数。常见做法是配置 Druid 或 HikariCP连接池的 initialSize 设 5maxActive 设 20maxWait 设 6000 毫秒。很多人忽略 characterEncoding导致插入中文变成问号。JDBC URL 里显式加characterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai后两个参数分别解决 SSL 握手警告和时区差八小时的问题。// Druid 连接池最小配置示例 DruidDataSource ds new DruidDataSource(); ds.setUrl(jdbc:mysql://localhost:3306/airline?characterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai); ds.setUsername(root); ds.setPassword(your_password); ds.setInitialSize(5); ds.setMaxActive(20); ds.setMaxWait(6000);这里要注意 maxWait 是获取连接的最大等待毫秒数超过就抛异常避免并发时线程无限等下去。连接池不能设置得越大越好maxActive 设太大比如 100反而会让 MySQL 的线程数暴涨导致上下文切换开销压垮数据库。课设规模 20 足够。4. 核心业务 SQL 编排航班查询、下单扣库存、退票回补的写法与参数4.1 多表联查查询航班余票一个 JOIN 把三张表串起来业务里最高频的操作是“查询某天从 A 到 B 的航班余票”。这个查询要返回航班号、起降时间、各舱位价格和余票数。SQL 里要同时关联 flights 和 cabins并过滤掉库存为 0 的舱位。SELECT f.flight_no, f.departure_city, f.arrival_city, DATE_FORMAT(f.departure_time, %Y-%m-%d %H:%i) AS dep_time, DATE_FORMAT(f.arrival_time, %Y-%m-%d %H:%i) AS arr_time, c.cabin_class, c.price, c.stock FROM flights f JOIN cabins c ON f.flight_id c.flight_id WHERE f.departure_city 北京 AND f.arrival_city 上海 AND DATE(f.departure_time) 2025-06-01 AND c.stock 0 ORDER BY f.departure_time, c.price;逻辑说明JOIN 把航班和舱位展平为一行一个舱位的宽表WHERE 里的 DATE(f.departure_time) 精确匹配日期c.stock 0 直接过滤无票舱位。这个查询是课设里最常用的展示型 SQL务必滚瓜烂熟。ORDER BY 先按起飞时间、再按价格排序符合用户浏览习惯。4.2 下单扣库存一个事务里完成订单写入和库存扣减这是整个系统最核心的 SQL 片段也是事务的经典应用场景。思路是插入订单 → 拿到自增 order_id → 插入订单明细 → 扣减舱位库存 → 更新订单状态为已支付。整个流程包在事务里任何一步失败则全部回滚。-- 伪代码在 Java/后端代码里控制事务 START TRANSACTION; -- 1. 插入订单order_no 由业务代码生成时间戳随机数 INSERT INTO orders (order_no, user_id, status) VALUES (202506011230001234, 1, pending); -- 2. 拿刚才的 order_id插入明细 INSERT INTO order_items (order_id, cabin_id, passenger_name, id_card) VALUES (LAST_INSERT_ID(), 3, 张三, 110101199001011234); -- 3. 扣库存先检查再更新这是防超卖的关键 UPDATE cabins SET stock stock - 1 WHERE cabin_id 3 AND stock 0; -- 4. 影响行数为 0 说明库存不足回滚 IF ROW_COUNT() 0 THEN ROLLBACK; END IF; -- 5. 更新订单状态 UPDATE orders SET status paid, paid_at NOW() WHERE order_id LAST_INSERT_ID(); COMMIT;这段逻辑里最关键的是第三步 UPDATE 的写法SET stock stock - 1 WHERE cabin_id 3 AND stock 0。这不是随意写的而是利用数据库行锁实现“原子扣减”——在同一时刻两个并发请求都读到 stock1 时第一个请求执行 UPDATE 后行锁释放第二个请求再执行时 stock 已经是 0stock 0条件不满足影响行数为 0事务回滚。这就从根上避免了超卖。很多教程里先 SELECT 查余票再用 UPDATE 无条件扣减并发一高就会把库存扣成负数。4.3 退票回补与状态流转保留历史还是物理删除这里给一个推荐方案退票的典型 SQL 是反向操作把 cabin 的 stock 1再把订单状态改成 refunded。但有一个设计上的分叉口物理删除订单记录还是保留记录只改状态推荐保留记录、只改状态原因有三一是保留用户的购票历史方便在系统里查看“我的订单”时区分已退票和已支付二是避免删除订单时级联删除明细导致审计信息丢失三是这个表不断增长反而能用来做“退票率”之类的分析题素材。-- 退票事务内完成回补库存和状态修改 START TRANSACTION; UPDATE cabins c JOIN order_items oi ON c.cabin_id oi.cabin_id SET c.stock c.stock 1 WHERE oi.order_id 1001 AND oi.cabin_id 3; UPDATE orders SET status refunded WHERE order_id 1001 AND status paid; -- 如果订单不是 paid 状态说明重复退票影响行数为 0回滚 IF ROW_COUNT() 0 THEN ROLLBACK; END IF; COMMIT;注意第二个 UPDATE 的 WHERE 条件里带了status paid这是防重复退票的屏障。如果用户连续点击两次退票按钮第一次把状态改成 refunded第二次执行时 status 已经不匹配影响行数为 0直接回滚。这种写法比先 SELECT 查状态再决定是否更新要安全得多省去了代码层的加锁判断。5. 避坑指南数据库课程设计里最容易翻车的 5 个经典问题5.1 死锁两个事务互相等对方释放锁重试机制比改 SQL 更有效现象并发测试时两个请求同时下单且操作了两个不同的舱位数据库报 Deadlock found when trying to get lock。原因事务 A 先锁了 cabin_id1 再锁 cabin_id2事务 B 先锁了 cabin_id2 再锁 cabin_id1两边都在等对方释放。解决一是让所有事务按相同顺序访问资源——比如强制 cabin_id 升序处理二是在代码层捕获死锁异常并重试。课程设计里最容易出现的死锁场景是同一个订单同时插入多条明细多人同行明细插表顺序不一致就会互相锁。解决方案是应用层先对 cabin_id 排序再逐个插入从源头消除循环等待。5.2 外键导致无法删除航班数据上课演示时当场报错现象删除航班记录时 MySQL 报 Cannot delete or update a parent row。原因这就是前面建表时设了 ON DELETE RESTRICT 的后果——订单明细表里还引用着该航班的舱位。解决不要直接在 UI 上提供“删除航班”按钮而是提供“取消航班”功能。具体做法是给 flights 表加一个is_active TINYINT DEFAULT 1字段取消航班时 UPDATE 成 0查询时默认过滤is_active 1。这样既保留了引用完整性又实现了“逻辑删除”。这个设计决策在答辩时可以主动讲出来远比当场改表结构要体面。5.3 中文乱码表建对了但数据全是问号问题出在连接串现象插入“北京”变成了“”“张三”变成乱码。原因MySQL 服务器端字符集是 utf8mb4但 JDBC 连接串没指定 characterEncoding驱动用了默认的 latin1。解决连接 URL 显式加characterEncodingutf8同时确认建库语句是CREATE DATABASE airline DEFAULT CHARSET utf8mb4;。还有一个隐藏坑如果用了 Navicat 的查询窗口手动插入中文正常但程序里插入乱码那 100% 是连接串的问题而不是表的问题。5.4 并发下单超卖SELECT 查库存然后再 UPDATE是典型错误写法现象用 JMeter 模拟 50 个并发用户同时购买最后一个座位结果订单生成了 5 个库存只有 1。原因代码写成SELECT stock FROM cabins WHERE cabin_id 3判断 stock 0然后UPDATE cabins SET stock stock - 1 WHERE cabin_id 3。两个操作分开执行中间没有锁保护并发时全部通过 SELECT 检查然后一起执行 UPDATE库存直接扣成负数。解决把检查与扣减合并到一条 UPDATE就是前面写的UPDATE ... WHERE cabin_id 3 AND stock 0用影响行数判断是否成功。5.5 数据库连接没有关闭演示十分钟后系统假死现象程序跑几分钟后就报 Connection is not available, request timed out。原因代码里获取了连接但没在 finally 中关闭连接池的连接被耗尽。解决使用 try-with-resources 自动关闭连接并在 finally 块里归还连接。检查标准是业务代码中每出现一次 getConnection必须有对应的 close。try (Connection conn ds.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, Beijing); ResultSet rs ps.executeQuery(); // 处理结果 } catch (SQLException e) { // 记日志不要吞掉异常 }这段代码里 try-with-resources 保证了 conn 和 ps 在语句块结束后自动关闭不需要手写 finally。但注意 ResultSet 不需要显式关闭Statement 关闭时它自动关闭。很多人漏掉 PreparedStatement 的关闭同样会让连接池慢慢耗尽——Connection 虽然归还了但 Statement 持有的服务器端游标没有释放。6. 把课设文档做厚索引优化、事务隔离级别和 ER 图是答辩加分项6.1 为高频查询设计联合索引用 EXPLAIN 验证如果你的文档里能写出一段索引优化分析答辩老师会直接对你改观。在 flights 表上(departure_city, arrival_city, departure_time) 是最值得建的联合索引。因为查询总是按“出发地 目的地 日期”来过滤。orders 表上(user_id, status) 联合索引可以加速“查某用户的所有订单并按状态过滤”的查询。ALTER TABLE flights ADD INDEX idx_route_time (departure_city, arrival_city, departure_time); ALTER TABLE orders ADD INDEX idx_user_status (user_id, status);建好索引后用EXPLAIN SELECT ...验证type列从 ALL 变成了 refkey列显示用了索引名。把这个前后对比放进文档里是实打实的调优证据。但注意索引不是越多越好。每张表超过 5 个索引后INSERT 和 UPDATE 的代价会明显上升因为写入时要同步维护索引树。课设里三四张核心表各 23 个索引就足够了。6.2 事务隔离级别为什么默认的 REPEATABLE READ 在订票场景够用MySQL InnoDB 默认隔离级别是 REPEATABLE READ。在这个级别下同一事务内多次 SELECT 会看到一致的快照不会出现不可重复读。对订票系统来说这个级别天然适合——下单过程中先查用户信息再查航班不会因为别的事务提交而读到不一致的数据。但如果要做实时库存校验快照读可能读到旧值。解决方式是使用当前读即每次都执行 SELECT ... FOR UPDATE 锁定行或者依赖我们前面用 UPDATE 的条件更新直接操作最新数据。这里不推荐把隔离级别调成 READ COMMITTED 或 SERIALIZABLE。前者可能让一个事务里的两次查询结果不一致后者会把所有读操作升级为锁并发能力急剧下降。课设答辩时能说清楚“为什么用默认隔离级别、它在什么场景下会有问题、怎么规避”这已经是超出教材水平的回答。6.3 设计文档的图表组织ER 图、用例图、状态图三层数据库课程设计机票预订系统.doc 这份文档要想拿高分结构上建议这样组织开篇放数据库设计说明ER 图 关系模式重点解释每个表的主外键及联系中间放核心业务逻辑下单购票、退票、航班动态查询再把表的 DDL 和权限分配放进附录。ER 图用 2.3 节的工具画完导出关系模式写成标准的三行式表名(属性主键用下划线标识外键用波浪线标识)。状态图可以只画订单状态流转pending → paid → refunded / cancelled这张图不仅展示了业务思考深度还能解释为什么订单表里要加 status 字段。6.4 一个验证脚本并发下单的压测方法自己先跑一遍再演示最后给一个快速自测脚本。开两个终端同时执行同一张舱位的购票事务验证是否只有一个成功# 终端 1 mysql -uroot -p airline -e START TRANSACTION; UPDATE cabins SET stock stock - 1 WHERE cabin_id 3 AND stock 0; SELECT ROW_COUNT(); COMMIT; # 终端 2和终端 1 同时执行 mysql -uroot -p airline -e START TRANSACTION; UPDATE cabins SET stock stock - 1 WHERE cabin_id 3 AND stock 0; SELECT ROW_COUNT(); COMMIT;如果两个终端执行之间几乎没有间隔你会看到其中一个 ROW_COUNT() 返回 1另一个返回 0。返回 1 的表示成功扣减返回 0 的说明库存不够或行锁等待超时。这就是事务隔离和条件更新共同作用的结果。我自己做课设的时候第一次用这种双终端验证方法才发现之前写的“先查后改”代码在并发下有多脆弱——那个坑让系统丢了 5 张票的数据。如果这份笔记只留一个印象给你那就是库存操作永远是 UPDATE 加条件不是 SELECT 加判断。希望帮到你。本文还有配套的精品资源点击获取
返回列表