数据库课设:车站售票系统的并发控制与存储过程实战

发布时间:2026/10/9 19:01:51
数据库课设:车站售票系统的并发控制与存储过程实战 简介车站售票管理系统是广东工业大学数据库课程设计的个人方案适合广工及其他高校正在完成同类课设的学生参考。系统以车票购买、退票为业务主线覆盖车次查询、时刻表查询、售票情况统计等日常操作同时包含数据安全管理、备份与恢复、操作员管理和权限设置等维护功能能够展现数据库应用系统从需求分析到模块落地的完整过程。压缩包共有100个文件以21个Java源文件、72个编译后的class文件为主搭配1个SQL脚本用于建库初始化另有项目配置、Word安装说明和txt说明等整个压缩包约4.65MB。资源内附源码及安装说明书便于对照学习Java界面层与数据库层的交互逻辑也可以直接参考表结构和功能划分来完成自己的课设报告与演示。目前已有1155人浏览学习适合需要快速上手数据库课程设计、理解购票退票与后台维护全流程的读者。1. 车站售票管理系统课设为什么你写的SQL查不出“合理”的数据数据库课程设计里车站售票管理系统几乎是出场率最高的题目。这个题目看着简单——无非是车次表、用户表、订单表再加几个增删改查。但真正动手做的人大多会在答辩演示时翻车要么余票成了负数要么两个人同时买最后一张票都显示成功要么退票之后座位“凭空消失”。某位导师在验收时问过一个很直接的问题“你这个系统敢拿去给车站用吗”大多数人答不上来。这份资源的价值正好在这里它不是把“车站售票管理系统”做成一个花架子Demo而是把业务规则拆成主键、外键、约束、事务和存储过程让数据在极端操作下也站得住。适合正在做数据库课设、但又不想只交一个CRUD页面的在校生也适合想快速理解“表结构设计如何兜住业务规则”的开发者。先说结论课设拿高分的关键往往不是你SQL写得多花哨而是你把“一票不能卖两次”这件事用数据库本身的机制守住了。2. 设计选型从ER图到关系模式余票字段为什么必须“冗余”2.1 技术栈选型MySQL Python 还是 MySQL Java多数人纠结的第一个问题是“我用什么写界面”这道题最常见的组合是Java Swing或Java Web其次是Python的Tkinter或Flask。我的建议是除非你们学校强制指定语言否则优先选自己最熟的那一套数据库课设的评分重点不在界面美观度而在“后端SQL写得好不好、表设计合理不合理”。但有一点要提醒如果你选JavaJDBC那一套可能需要多写很多样板代码如果你选PythonPyMySQL的连接和游标操作会轻便不少尤其适合在答辩现场临时改数据演示。课程设计报告里经常会问“为什么选这个技术栈”这时候至少要写出两层理由数据层选MySQL而非SQLite是因为课设要求体现“并发控制”“事务隔离”等数据库特性SQLite在锁和事务粒度上解释起来很勉强访问层选带连接池的访问方式比如Java的HikariCP或Python的DBUtils而不是每来一次请求就重连一次数据库这样才能体现对“数据库连接是稀缺资源”的理解。老实说很多同学的课设报告在第一页就露怯了直接贴一句“本系统采用B/S架构”就没了。其实这里最好画一张架构图哪怕是用Word画个方框图把“客户端 → 业务逻辑 → 数据库”三层的数据流向标清楚尤其标注出“事务在哪一层开启、提交或回滚”。这会让导师一眼看出你不是只会写页面。2.2 ER模型如何转关系模式余票字段的“冗余设计”资源里自带一份完整的ER图PDF版核心实体是管理员、用户、车次、订单、乘车人。实体之间的关系主要有两个用户与订单一个用户可下多张订单一张订单归属一名用户车次与订单一个车次可对应多张订单一张订单只对应一趟车次到关系模式这一步最关键的决策是车次表里要不要存“余票数量”这个字段按照数据库理论第三范式3NF余票数应该由“座位总数 - 已售张数”算出来不应该冗余存储。理论课上这么讲没问题但实际做课设你会发现每个车次的座位可能分“一等座”“二等座”“站票”多个等级总座位数存在不同表里每次查询余票都要SUM所有订单里已售的票数车次一多查询就明显变慢。更麻烦的是如果有人中途退票余票数要立刻恢复如果全靠实时计算数据一致性依赖的链条会很长。常见的解决方案是“冗余余票字段”并在每次订票/退票时同步更新。这个做法打破了3NF但它换来了“查余票只走单行读”的性能收益。资源包里的关系模式说明文档也专门对这个决策做了解释文档里写了一句很精辟的话“余票字段不是热点数据的缓存它是车次表的一个业务状态列必须由事务来维护。”最终关系模式大致如下以资源中的roadmap表为例这里只列出核心字段用户表用户ID主键、用户名、密码MD5摘要不是明文、电话车次表车次号、发车日期、发车时间、到达时间、起点站、终点站、票价、总座位数、余票数订单表订单ID主键、用户ID外键、车次号外键、乘车日期、购买张数、下单时间、订单状态已支付/已取消/已退票乘车人表乘车人ID、姓名、证件号、订单ID外键这几张表的设计需要能回答一个问题“如果用户买3张票订单表存一条记录还是三条”订单主表和乘车人子表拆分的原因主要有三点一是每个乘车人的证件号需要单独校验二是退票时可能只退某一个人的票三是避免一个字段里塞多个身份证号这种明显违反第一范式的设计。资源里的关系模式文档已经把这些范式分析写成了“可以直接抄进课设报告”的版本这是这个资源对赶时间的同学最友好的地方。3. 建库落地五张核心表与索引/外键的取舍3.1 建表脚本主键、唯一键、和“不要乱用外键”资源里带了一个完整的建库脚本create_db.sql里面依次创建数据库、建表、插入演示数据。我摘出最核心的车次表建表语句看几个容易被忽略的细节。CREATE TABLE train ( train_id VARCHAR(10) NOT NULL COMMENT 车次编号如 G1001, travel_date DATE NOT NULL COMMENT 发车日期用于区分同车次不同日期的排班, departure_station VARCHAR(20) NOT NULL COMMENT 始发站, arrival_station VARCHAR(20) NOT NULL COMMENT 终点站, departure_time TIME NOT NULL COMMENT 发车时刻, arrival_time TIME NOT NULL COMMENT 到达时刻, total_seats INT NOT NULL COMMENT 总座位数, remaining_seats INT NOT NULL COMMENT 当前余票数业务状态字段, PRIMARY KEY (train_id, travel_date), KEY idx_depart_time (departure_time), CONSTRAINT chk_seats CHECK (remaining_seats 0 AND remaining_seats total_seats) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段建表语句里有三个容易被课设答辩追问的设计点复合主键(train_id, travel_date)因为同一条线路每天都可能发车必须用“车次号日期”才能唯一定位一趟具体的车。只用train_id做主键会导致同一天的班次只能有一条记录CHECK约束MySQL 8.0.16之前CHECK约束是被解析但不会真正生效的如果你用的是5.7版本这个约束等于是个摆设。余票不为负的校验最终要在应用层或存储过程里做不能只依赖CHECK约束utf8mb4字符集站点名称里可能出现生僻字或emoji比如“XX站新”utf8mb4才能完整存储如果顺手写了utf8会埋一个隐患。此时你会发现我前面说“外键要慎用”但这个建表脚本里并没有在订单表上直接声明外键。需要解释一下MySQL的InnoDB引擎支持外键但在课设这个并发量下外键带来的维护成本通常大于收益。很多同学遇到“删不掉车次因为有订单引用它”这种问题本质就是外键约束在起作用。资源里的方案是在应用层保证逻辑一致性表结构里不写FOREIGN KEY语法只在需要加速查询的地方建普通索引。这样既避免了外键锁竞争也简化了删除操作答辩时能讲清楚“为什么不用外键”反而是加分项。3.2 索引设计哪些字段值得建索引哪些建了也没用资源里的index_notes.txt列了4条索引建议核心观点是索引不是越多越好而是“查询怎么走索引就怎么建”。车次查询的典型SQL是按起点站和终点站筛选SELECT train_id, departure_time, arrival_time, remaining_seats FROM train WHERE departure_station 北京南 AND arrival_station 上海虹桥 AND travel_date 2024-12-01 ORDER BY departure_time;这条SQL的WHERE条件里有三个字段如果在三列上分别建三个单列索引MySQL通常只会选其中一个索引来用优化器会评估区分度另外两个索引等于浪费空间。正确的做法是建一个联合索引(departure_station, arrival_station, travel_date)查询时按索引从左到右匹配三列都能用上。写进课设报告时可以用一条EXPLAIN语句展示key_len和rows的变化这是导师非常认可的实测论证。反过来有些字段建索引是纯属浪费订单状态字段取值范围只有三五个值区分度太差优化器大概率不走索引乘车人姓名查询场景极少课设阶段没有必要为它建索引。我刚做完这个系统时也踩过“每列都建索引”的坑查了下数据字典同一个字段既出现在联合索引里又单独建了索引白白占了双份空间。后来按查询频率重新梳理总共只保留了5个索引体感查询速度差距不大但写报告时“索引设计”这一章就有了真实的思考过程。4. 购票退票核心SQL事务边界、行锁与余票一致性4.1 购票事务先锁行、再判断、后更新车站售票系统最容易出问题的场景是“两人同时买最后一张票”。如果没有做并发控制两个事务同时读到余票数1各自判断“余票大于0”然后都执行扣减最终余票变成-1超卖就发生了。解决这个问题核心手段是给余票扣减加上“行锁”。资源包里给出了标准解法核心是先做一次带FOR UPDATE的查询把车次这一行锁住然后再判断和更新。完整事务如下START TRANSACTION; -- 锁定该车次的行防止其他事务并发修改 SELECT remaining_seats INTO seats FROM train WHERE train_id G1001 AND travel_date 2024-12-01 FOR UPDATE; -- 业务判断余票是否足够 IF seats 1 THEN ROLLBACK; SELECT 余票不足 AS result; ELSE UPDATE train SET remaining_seats remaining_seats - 1 WHERE train_id G1001 AND travel_date 2024-12-01; INSERT INTO orders (user_id, train_id, travel_date, ticket_count, order_status, create_time) VALUES (1001, G1001, 2024-12-01, 1, 已支付, NOW()); COMMIT; SELECT 购票成功 AS result; END IF;这里的FOR UPDATE是InnoDB提供的悲观锁。它的效果是事务A锁住这一行之后事务B执行同样的SELECT ... FOR UPDATE会被阻塞一直到A提交B才能进入判断。换句话说超卖被数据库的锁机制拦住了而不是靠应用层的“运气”。用这段代码去复盘答辩串词时可以这么讲“我故意让两个线程同时提交购票请求用数据库的锁机制保证只有一个事务能扣减成功另一个会阻塞或者超时重试。”这个实验资源里附带的压测脚本可以复现导师容易认可。这里额外的注意点是IF语法在MySQL存储过程和函数里才能直接用如果是在应用层写SQL一般不用IF而是先SELECT ... FOR UPDATE查出来在后端代码里做条件判断再决定执行UPDATE还是ROLLBACK。资源里的sale_procedure.sql是完整的存储过程版本应用层版本在示例代码目录里也有两种写法都给了方便不同技术栈的同学对照。4.2 退票回滚先改状态还是先改余票退票的逻辑比购票多一层“订单状态判断”。如果一个订单已经退过票再点一次退票就要被拦截。为了体现这个约束常见做法是退票前先查订单的状态字段只有“已支付”才允许退票。START TRANSACTION; -- 检查订单状态 SELECT order_status FROM orders WHERE order_id 20241201001 FOR UPDATE; -- 状态不符合则回滚 -- 状态符合则把余票加回去同时更新订单状态 UPDATE train SET remaining_seats remaining_seats 1 WHERE train_id G1001 AND travel_date 2024-12-01; UPDATE orders SET order_status 已退票, refund_time NOW() WHERE order_id 20241201001; COMMIT;我在课设演示时踩过这个坑没有FOR UPDATE直接改的后果是两个窗口同时退同一张订单的票余票被加了两次。听起来这事概率很小但导师在验收时就是会故意连点两次退票按钮来测系统的防御能力。用了FOR UPDATE锁住订单行后第二个事务会等待第一个事务提交然后再读到订单状态已经不是“已支付”直接拒绝退款。这就是“同一张票不可能被退两次”的落地方案。资源里的refund_procedure.sql文件里还有一版带“订单原价退还金额”的写法把票款的回退也做成一个单独的函数这样在课设报告里可以多写一个“金额一致性”的测试点表面上看是一个很小的函数但报告里能写的内容就多了一页。5. 存储过程与触发器把业务规则收进数据库5.1 存储过程的意义为什么说“把逻辑放在数据库里”很多同学的课设是“所有逻辑都在Java或Python代码里数据库只负责存数据”。这种做法本身没错但课程设计评分标准里往往有一条“系统是否充分利用数据库特性”。如果你在项目里只用到了最基本的SELECT/INSERT/UPDATE/DELETE答辩时老师可能会追问“触发器用了没存储过程用了没”为了这个印象分至少要做一个存储过程和两个触发器。资源里的sale_procedure.sql是一个完整的“购票存储过程”参数设计如下DELIMITER $$ CREATE PROCEDURE sp_buy_ticket( IN p_user_id INT, IN p_train_id VARCHAR(10), IN p_travel_date DATE, IN p_ticket_count INT, OUT p_result VARCHAR(20) ) BEGIN DECLARE v_seats INT DEFAULT 0; START TRANSACTION; -- 锁定车次行防止并发超卖 SELECT remaining_seats INTO v_seats FROM train WHERE train_id p_train_id AND travel_date p_travel_date FOR UPDATE; IF v_seats p_ticket_count THEN SET p_result 余票不足; ROLLBACK; ELSE UPDATE train SET remaining_seats remaining_seats - p_ticket_count WHERE train_id p_train_id AND travel_date p_travel_date; INSERT INTO orders (user_id, train_id, travel_date, ticket_count, order_status, create_time) VALUES (p_user_id, p_train_id, p_travel_date, p_ticket_count, 已支付, NOW()); SET p_result 成功; COMMIT; END IF; END$$ DELIMITER ;调用方式CALL sp_buy_ticket(1001, G1001, 2024-12-01, 2, result); SELECT result;这里有两个参数设计的细节值得放进报告p_ticket_count允许一次买多张而不是每张票单独开一个事务。这样能让“买3张票只扣一次余票”变成一个原子操作也避免了三张票分别扣减时中途失败导致的数据不一致OUT参数p_result用来返回业务结果而不是靠异常来传递错误。存储过程里用控制流语句IF/THEN体现“业务逻辑由数据库保证”这是导师非常喜欢的“存储过程业务封装”范例。5.2 触发器自动维护数据避免前后台数据不一致触发器在课设系统里最常被用到的场景是“订单状态变更后自动记录日志”和“余票下限保护”。资源里提供了一个被称为“防呆设计”的触发器当余票被更新为负数时自动把值修正回0并插入一条警告日志。严格来说这个设计有点“亡羊补牢”因为真正拦超卖靠的是事务和锁但触发器可以作为最后一道保险丝。CREATE TRIGGER trg_prevent_negative_seats BEFORE UPDATE ON train FOR EACH ROW BEGIN IF NEW.remaining_seats 0 THEN SET NEW.remaining_seats 0; INSERT INTO warn_log (train_id, travel_date, warn_time, warn_info) VALUES (NEW.train_id, NEW.travel_date, NOW(), 余票被强制修正为0请检查并发逻辑); END IF; END;写进课设报告的时候不要只写“我建了个触发器”要解释清楚“在什么操作、什么条件下触发、对数据做了什么修正”。如果把BEFORE UPDATE写在报告里并指出它触发时机是“行更新前”导师就会知道你是真正理解了触发器而不是套模板。这个触发器的适用边界也要在答辩时说清楚它不能代替事务也不能作为超卖防范的兜底方案它的真正作用是留痕把不该发生的并发异常记录到日志表里。把这个边界讲清楚反而比你吹嘘“触发器完全解决了超卖”更可信。6. 避坑与验收四条经验记录与一个答辩前的自测习惯6.1 高频踩坑记录踩坑一乱码。课程演示的时候明明代码里写入的是“北京南”数据库里存的确是“鍖椾含鍗”。原因是建库时没指定utf8mb4连接串也没加characterEncodingutf-8Java或charsetutf8mb4Python。解决方法是建库时带上CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci并且在连接参数里显式声明字符集。踩坑二MySQL 5.7 下CHECK约束不生效。在5.7版本里建表写了CHECK (remaining_seats 0)余票照样能变成负数。解决方法是把校验逻辑放到存储过程里或者用触发器在BEFORE UPDATE时做拦截。做项目前先SELECT VERSION()确认一下自己的MySQL版本。踩坑三多个窗口同时退同一张订单。两笔退票事务都先SELECT到订单状态“已支付”然后各自恢复余票结果余票多加了。原因是读取订单状态时没加FOR UPDATE属于典型的“读写并发”问题。解决方法是退票和购票一样先锁行再判断再修改。踩坑四外键导致“车次删不掉”。比如想删掉一趟广州南到深圳北的列车但因为订单表里有订单引用了这趟车删除失败。原因是在建订单表时直接写了FOREIGN KEY业务上不允许删。解决的思路有两种如果业务就是不能物理删除那就给车次表加“是否停运”状态字段用逻辑删代替物理删如果确实要物理删除就在应用层先删订单再删车次不要依赖外键级联。资源包采用的做法是后者把“先删子表再删主表”的次序写进了业务代码注释里。6.2 答辩前强制走一遍的“查余票 购票 退票”链路课设答辩那天最容易出现的情况是页面能开、能登录但老师顺手查一下余票发现查询结果和购票记录对不上。所以检查要例行化我现在每次做完一个系统会照着这个顺序跑一遍先在浏览器连续刷新余票查询5次确认同一个车次的余票数不变然后用两个不同的账号同时买同一车次的最后一张票确认只有一个成功接着买一张票后退掉确认余票1、订单状态变为已退票、退款金额字段不为空最后清空订单确认没有脏数据残留。这几步走完再截图作为测试报告附件顺手放进课设文档里答辩时的硬件/系统/部署问题基本不会翻了。还要提一个“有时候你觉得是玄学”的事在本地MySQL命令行下验证通过的SQL放到Java或Python里执行报语法错误。这通常不是SQL的问题而是JDBC或PyMySQL不允许一次执行多条语句或者存储过程的DELIMITER没有还原。资源包里给出的版本已经处理好了DELIMITER还原但如果你从其他地方复制代码一定要检查存储过程定义结束后是否恢复成DELIMITER ;。那次踩坑之后我养成了一个习惯凡是涉及“状态变化”的代码不管是在存储过程还是应用层第一件事先问自己“这条语句在并发下会被执行几次”然后强制在关键读取路径上补FOR UPDATE或唯一约束。从那以后类似的并发问题几乎再没有在答辩现场出现过。希望这份资源的整理过程和踩坑记录也能帮你把课设做得让人敢拿去“真用”。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询