news 2026/10/13 8:39:35

数据库课设实战:火车票售票管理系统的表设计与并发控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数据库课设实战:火车票售票管理系统的表设计与并发控制

简介:数据库课程设计实验报告《火车票售票管理系统方案》是面向计算机、软件工程专业学生的完整参考文档,适合正在完成数据库课程设计、需要撰写实验报告或学习系统设计流程的读者。报告以Eclipse+MySQL为开发平台,系统阐述火车票售票管理系统的开发全过程,从需求分析、数据库规划、ER图与数据字典、关系表与索引设计,到功能模块、事务设计、安全机制及系统测试均有详细说明,并覆盖车次管理、售票、退票、查询、异常处理等典型业务场景。压缩包内含1个doc文件,共721KB,为完整实验报告正文,无需额外环境即可阅读。该资源已有74人学习,可作为课程设计选题参考、数据库设计思路借鉴或实验报告撰写模板,具有较高实用价值。

1. 火车票售票管理系统:数据库课设到底在考什么

数据库课程设计实验报告火车票售票管理系统方案,这个名字对每个修过数据库课的人来说都不陌生。它看起来只要交一份Word报告和几条SQL,但真到答辩时,老师问的第一个问题往往不是“你建了几张表”,而是“同时有两个人买最后一张票怎么办”。这背后考的是把业务规则转成表结构、把表结构转成增删改查、再把并发和事务串联起来的完整链路。这篇文章就按这个链路,给出一个可以直接跑的MySQL方案,以及每一环最容易翻车的地方。

2. 需求分析与ER设计:先定业务规则,再谈画ER图

2.1 需求边界:别一上来就画表,先把一句话业务规则定下来

很多同学的课设报告第一页就贴一张ER图,第二页开始建表,看起来很标准,其实缺少一个前提:需求边界没定。没有业务规则,ER图画得再漂亮,代码阶段也会被各种“如果”打回来。

火车票售票系统最核心的业务规则只有几句话:一个车次在某个日期有一批可售座位;用户买一张票,可售座位减一;退一张票,可售座位加一;同一个车次同一天只能有唯一一个余票记录。这条规则直接决定了你不能把车次信息、日期和余票塞进同一张表。如果直接把日期字段写进车次表,那么同一趟列车每天发车就会产生多个重复车次号,主键、外键和查询逻辑都会乱。常见做法是拆出两张表:车次基本信息表,以及“某车次在某天运行及余票”的表。

实体关系也随之清晰:用户、车次、运行日期、订单。用户与订单是一对多,一个用户可下多张单;车次与运行日期是一对多,一趟车可以有多个日期的运行计划;运行日期与订单是一对多,一个日期的车次会产生多张订单。到这里,ER图就可以落笔了。不需要为了凑实体硬加一张“座位表”,按余票数控制的售票系统,座位表是冗余;只有做选座功能才需要拆座位表。课程设计阶段,把余票作为一个受保护的计数字段更常见,也更简单。

2.2 从ER到关系模式:四张表覆盖一个完整购票闭环

把上面的ER图转成关系模式,我一般会保留四张表。user表存用户,train表存车次基础信息,schedule表存车次在具体日期的总座位数和剩余座位数,orders表存订单流水。这样设计最大的好处是,余票的维护聚焦在schedule这一张表上,订单不再存车次、日期这些冗余信息,而是通过schedule_id间接关联train。

关系模式如下。user:user_id, username, password_hash, real_name, id_card, phone, create_time。train:train_id, train_no, origin_station, dest_station, depart_time, arrive_time, seat_type, ticket_price, total_seats, status。schedule:id, train_id, depart_date, total_seats, remaining_seats,其中(train_id, depart_date)唯一。orders:order_id, user_id, schedule_id, passenger_name, id_card, seat_no, price, status, create_time, pay_time。

注意schedule同时有total_seats和remaining_seats,看起来有点冗余,但这是有意的。total_seats用来校验“不能卖出超过总座位数”,remaining_seats是可卖的余量。如果你只看remaining_seats大于0,而忘记校验它不能超过total_seats,测试时倒不会出问题,因为初始值一致,但一旦数据被手工改错,就会出现一张车次卖了比座位还多票的脏数据。关系模式里加上这个约束,报告里也能多写一条“完整性约束设计”,答辩时有用。

订单表没有把票价冗余进来?票价放在train表,订单通过schedule关联train拿到价格。这里有两种做法:一种开票时把票价冗余进订单,好处是车次调价后历史订单不受影响,坏处是报告里要被追问“为什么违反第三范式”;另一种不冗余,每次查询时关联train,好处是范式漂亮,坏处是历史订单价格随车次改价变化。课程设计建议用冗余,因为火车票定价会变,订单快照是业务要求。老师如果揪着范式,你就回答“这是出于业务快照考虑的受控冗余”,比硬背概念好。这也是实践里最常见的权衡。

3. 关系模式与建表SQL:把ER图变成能跑的MySQL DDL

3.1 建表前两个决策:引擎、字符集和你逃不掉的外键

到了写DDL这一步,动手建库前先定两件事。第一,引擎一律用InnoDB。你会在很多老资料里看到MyISAM,它查得快但根本不支持事务和行级锁。火车票系统的余票扣减必须依赖事务,这一步选错,后面所有并发控制都用不上。MySQL 8.x默认引擎就是InnoDB,不必额外指定,但建议在建表时显式写上ENGINE=InnoDB,让人一眼就能看到。

第二,字符集用utf8mb4而不是utf8。MySQL的utf8是utf8mb3,存不了emoji和部分生僻字。乘客姓名、身份证号里出现生僻字或字符,用utf8会报“Incorrect string value”。建库时写DEFAULT CHARSET=utf8mb4,排序规则一般用utf8mb4_unicode_ci或utf8mb4_0900_ai_ci,课程设计随便选一个,关键是字符集统一。最后,外键不要省。虽然有些工程师在业务库中刻意不建外键,靠应用层保证完整性,但课程设计要求体现数据库设计,外键是完整性约束的直观证据。外键列要记得加索引,否则关联查询和级联更新时性能会很难看。课设宁可让老师看到外键约束,也不要让他觉得表与表之间关系全靠脑子记。

3.2 核心DDL:四张表的完整可抄脚本

下面这段SQL可以直接在MySQL 8.x里执行。先建库,再建表,顺序不能乱,因为orders依赖schedule,schedule依赖train。代码里的注释对应课程设计报告里需要的“表结构说明”。

CREATE DATABASE IF NOT EXISTS train_ticket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE train_ticket; CREATE TABLE `user` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `username` VARCHAR(50) NOT NULL UNIQUE, `password_hash` CHAR(64) NOT NULL COMMENT 'SHA-256后的密文,别存明文', `real_name` VARCHAR(50) NOT NULL, `id_card` CHAR(18) NOT NULL UNIQUE COMMENT '身份证号是唯一键,防重复注册', `phone` VARCHAR(20) NOT NULL, `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

user表里password_hash用CHAR(64)而不是VARCHAR,是给SHA-256定长密文留的位置。课程设计里很多人用VARCHAR(255),结果报告里说不清为什么可变长,定长就是为了一份“当时存的就是固定摘要”的自信。id_card设为UNIQUE,这是“一个实名用户只能注册一次”的约束落点。

CREATE TABLE `train` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `train_no` VARCHAR(20) NOT NULL UNIQUE COMMENT '车次号,如G123', `origin_station` VARCHAR(50) NOT NULL, `dest_station` VARCHAR(50) NOT NULL, `depart_time` TIME NOT NULL, `arrive_time` TIME NOT NULL, `seat_type` VARCHAR(20) NOT NULL DEFAULT '二等座', `ticket_price` DECIMAL(10,2) NOT NULL, `total_seats` INT NOT NULL DEFAULT 200, `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1-运行;0-停运' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车次基本信息';

车次表中depart_time和arrive_time用TIME,日期用schedule表存,这是之前需求分析里“车次与日期分离”的落地。票价用DECIMAL(10,2)而不是FLOAT,课程设计报告里可以多写一句“浮点型不适合存金额”。status字段给停运车次预留,查余票时记得过滤。

CREATE TABLE `schedule` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `train_id` INT NOT NULL, `depart_date` DATE NOT NULL, `total_seats` INT NOT NULL, `remaining_seats` INT NOT NULL, UNIQUE KEY `uk_train_date` (`train_id`, `depart_date`), CONSTRAINT `fk_train` FOREIGN KEY (`train_id`) REFERENCES `train` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='某车次在某日期的余票';

schedule的联合唯一键是核心。没有这个唯一约束,同一个车次同一天可能被插入两条记录,余票会各存一份,买票时会出现“同一个座位被卖两次”的边界。total_seats和remaining_seats都不设CHECK,是因为MySQL 8.0.16以前的版本CHECK约束会被解析但不会强制,与其依赖一个可能会失效的约束,不如在业务SQL里用条件更新控制,能跨版本保住正确性。

CREATE TABLE `orders` ( `id` CHAR(36) PRIMARY KEY COMMENT 'UUID,当成订单号', `user_id` INT NOT NULL, `schedule_id` INT NOT NULL, `passenger_name` VARCHAR(50) NOT NULL, `id_card` CHAR(18) NOT NULL, `price` DECIMAL(10,2) NOT NULL, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0-待支付;1-已支付;2-已退票;3-已取消', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `pay_time` DATETIME NULL, CONSTRAINT `fk_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`), CONSTRAINT `fk_schedule` FOREIGN KEY (`schedule_id`) REFERENCES `schedule` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单与乘车人信息';

orders主键用CHAR(36)存UUID,一个看似简单但实用的决定。自增主键虽然简单,但订单号会暴露每天的单量,而且多数据源合并时容易冲突。课程设计从UUID开始练习,后面接触分布式系统时会有肌肉记忆。这里不需要再给orders加order_no字段,主键本身就是订单流水。

提示:如果用自增int做主键,订单表不要再用order_no单独存流水,主键就是流水,否则会多一套生成逻辑。

3.3 约束与索引:让增删改查不翻车的细节

上面已经落了三个关键约束:user.username和user.id_card唯一,schedule的(train_id, depart_date)唯一,orders主键。剩下的索引设计要跟着查询走。课程设计最常演示的查询是“查某天从A站到B站还有余票的车次”,这个查询会先用origin_station和dest_station过滤train,再用schedule过滤日期和余票。schedule表上已经有一个联合唯一键uk_train_date,它既能保证(train_id, depart_date)不重复,也能作为该字段组合的索引使用,不需要额外加。

orders表按user_id查询用户订单是高频,外键约束会为user_id建索引。按status查超时订单做定时清理,需要在(status, create_time)上建联合索引。课程设计阶段索引不用贪多,两个原则足够:外键列必须有索引,WHERE过滤的高频字段优先加。索引建滥了,插入和更新要维护多棵树,反而拖慢带并发压力的售票事务。

写DDL时还有一个小细节:所有表都用单数表名,不要用orders_list这种词。表名统一小写,字段统一下划线风格,报告里不要一会儿user一会儿users。虽然数据库不区分这些,但答辩时老师一眼就能看出你是否有工程习惯。

4. 核心业务SQL:从单表增删改查到存储过程、触发器与事务

4.1 基础增删改查:售票、退票与改签的SQL骨架

有了表,接下来就是把需求里的业务翻译成SQL。最先要能跑的是“查车票”。这个查询需要join train和schedule,用户真正关心的是车次、日期、发到站、时间和余票。参数通过占位符传入,避免拼接字符串。

SELECT t.train_no, t.origin_station, t.dest_station, s.depart_date, t.depart_time, t.arrive_time, t.seat_type, t.ticket_price, s.remaining_seats FROM schedule s JOIN train t ON s.train_id = t.id WHERE t.origin_station = ? AND t.dest_station = ? AND s.depart_date >= CURDATE() AND s.remaining_seats > 0 AND t.status = 1 ORDER BY s.depart_date, t.depart_time;

这里的?是预处理语句占位符,参数依次是出发站、到达站。写死在SQL里的值只适合测试,正式报告和程序里全部要用参数化查询,这一条能同时防注入,也方便程序里复用。查询里日期过滤用CURDATE()把过去的车次直接挡掉,status=1过滤停运,remaining_seats>0过滤已满车次。这四个过滤条件就是“什么是可售车票”的业务定义,缺一个,跑出来的结果就会有人买不到票或买到已停运的票。

售票是系统的主心骨,它的SQL看起来简单,但其实最关键的一点不是插入订单,而是先扣余票。扣余票必须用带条件的UPDATE,并且要等它成功后再插入订单,这两步在同一事务里。下面的逻辑需要放在存储过程或程序里执行,不能直接在MySQL客户端分步敲,因为IF ROW_COUNT()是过程语句。

START TRANSACTION; UPDATE schedule SET remaining_seats = remaining_seats - 1 WHERE id = ? AND remaining_seats > 0; -- MySQL会把上一步影响行数放在ROW_COUNT()里 IF ROW_COUNT() = 0 THEN ROLLBACK; SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'no_remaining_seats'; END IF; INSERT INTO orders (id, user_id, schedule_id, passenger_name, id_card, price) SELECT UUID(), ?, ?, ?, ?, ticket_price FROM train t JOIN schedule s ON s.train_id = t.id WHERE s.id = ?; COMMIT;

这里最值得写进报告的是WHERE条件remaining_seats > 0,它让扣减动作本身具有原子性:无论多少并发同时执行,数据库的行锁都会让这条UPDATE排队执行,后到的事务看到remaining_seats已经是0或负数条件不满足,影响行数为0,直接回滚。后面第5章会专门讲这个细节。

退票则是反向操作。要注意防重复退票,不能直接UPDATE orders SET status=2,而要先以“status=1”为条件,保证只有已支付的订单能被退。

START TRANSACTION; UPDATE orders SET status = 2, pay_time = NULL WHERE id = ? AND status = 1; IF ROW_COUNT() = 1 THEN UPDATE schedule s SET s.remaining_seats = s.remaining_seats + 1 WHERE s.id = (SELECT o.schedule_id FROM orders o WHERE o.id = ?); COMMIT; ELSE ROLLBACK; END IF;

这个UPDATE语句的条件status=1是关键。如果省略它,同一张订单被请求两次,第一次已经改成2,第二次又满足条件,会再加一次余票,造成超卖。把“状态机迁移”写在SQL条件里,用影响行数判定业务是否成功,是数据库做防重复处理的常用手法,比先SELECT再判断严谨得多。

4.2 存储过程封装购票事务:一次把事务边界交代清楚

课程设计要求里有“存储过程”这一项的话,购票流程是最合适的载体。把上面的事务包进存储过程,对外只暴露参数和返回码,程序里不用再拼一堆SQL。下面是一个能直接跑的版本。

DELIMITER $$ CREATE PROCEDURE purchase_ticket( IN p_user_id INT, IN p_schedule_id INT, IN p_passenger_name VARCHAR(50), IN p_id_card CHAR(18), OUT p_result TINYINT ) purchase: BEGIN DECLARE p_price DECIMAL(10,2); DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SET p_result = 0; END; START TRANSACTION; UPDATE schedule SET remaining_seats = remaining_seats - 1 WHERE id = p_schedule_id AND remaining_seats > 0; IF ROW_COUNT() = 0 THEN ROLLBACK; SET p_result = 0; LEAVE purchase; END IF; -- 从车次表取当前票价,不允许客户端传入价格 SELECT t.ticket_price INTO p_price FROM schedule s JOIN train t ON s.train_id = t.id WHERE s.id = p_schedule_id; INSERT INTO orders (id, user_id, schedule_id, passenger_name, id_card, price) VALUES (UUID(), p_user_id, p_schedule_id, p_passenger_name, p_id_card, p_price); COMMIT; SET p_result = 1; END$$ DELIMITER ;

注意price不是入参,而是从train表查出来的,避免前端恶意传一个0元票价。p_result表示1成功、0失败,可以用来提示“余票不足”或“系统异常”。存储过程的优势是把事务边界明确收口,调用方不用知道先扣票再插订单的顺序,也不容易漏掉COMMIT。事务里SELECT价格用的是普通读,因为票价不会并发变化,这里不需要额外加锁。

调用方式:

CALL purchase_ticket(1, 2, '张三', '110101199001011234', @r); SELECT @r;

返回1说明购票成功,0说明失败。注意存储过程里用EXIT HANDLER处理SQLEXCEPTION,只要事务中任何一个语句出错,自动回滚并把结果置0。如果去掉这个handler,一旦中途出错,事务会保持打开状态,连接不释放,后续查询全部卡住。这是很多新手在Navicat里“跑一次就锁住”的常见原因。

4.3 触发器与定时任务:自动扣票与过期订单清理

课程设计一般还会要求触发器。一个常见选择是用触发器在insert订单时自动扣减余票。这个想法容易写,但在并发场景里是危险的。因为触发器里的UPDATE会在insert语句的内部执行,如果外层事务最后回滚,触发器的操作也跟着回滚,看起来是安全的,但出问题时排查路径多一层,而且不容易在代码里控制超卖。我的习惯是主业务不用触发器,而是把扣票逻辑放在事务里显式做,触发器只放到非核心的“补偿”动作上。

下面这个触发器可以作为退票后自动恢复余票的演示,同时防重复恢复:

CREATE TRIGGER trg_orders_refund AFTER UPDATE ON orders FOR EACH ROW UPDATE schedule s SET s.remaining_seats = s.remaining_seats + 1 WHERE s.id = NEW.schedule_id AND NEW.status = 2 AND OLD.status != 2;

触发器的条件NEW.status=2且OLD.status!=2是防重复的关键。如果订单已经是退票状态,再跑一次更新不会满足OLD.status!=2,触发器不会重复加票。适用范围是“订单状态只能从已支付流转到已退票”,如果你允许已退票再改成其他状态,这个触发器就要再加状态机限制。

定时任务可以自动关闭超时未支付订单。MySQL的EVENT配合上面的退票逻辑,能做到超过30分钟未支付自动取消并恢复余票。

SET GLOBAL event_scheduler = ON; CREATE EVENT auto_cancel_unpaid_orders ON SCHEDULE EVERY 1 MINUTE DO BEGIN UPDATE orders SET status = 3 WHERE status = 0 AND create_time < NOW() - INTERVAL 30 MINUTE; END;

这个event做了自动取消,但余票怎么恢复?建议在取消后紧接着写一条update schedule,但要注意只对真正发生状态变化的行做恢复。event里可以先记录受影响行数,再通过JOIN去恢复余票。课程设计只需要展示思路,这里点到为止。要提醒的是,MySQL EVENT默认是关闭的,很多课设环境没权限打开,所以这一条写进报告时注明“生产环境需开启event_scheduler”,不要死磕。

5. 并发购票与数据一致性排查:死锁、超卖和余票负数怎么防

5.1 坑一:多人同时买最后一张票,余票变成负的

现象:压测或手工同时发起两笔购票请求,最后看到schedule表的remaining_seats变成了-1,而且两个订单都创建成功。

原因:后端的购票逻辑是先SELECT remaining_seats,if大于0,再UPDATE减一。两个会话同时SELECT到余票为1,都通过了检查,然后各自执行UPDATE。在MySQL默认的REPEATABLE READ隔离级别下,普通SELECT是快照读,读不到其他事务尚未提交的数据,所以两个事务都认为还有1张票。这一步就是典型的丢失更新。

解决:把“检查余票”和“扣减余票”合并成一条原子UPDATE:

UPDATE schedule SET remaining_seats = remaining_seats - 1 WHERE id = ? AND remaining_seats > 0;

MySQL对这条语句的并发访问是通过行锁串行化的,不可能两个事务同时把1扣成两个0。执行后立刻用ROW_COUNT()判断影响行数,为0就是没抢到票,直接回滚并返回“余票不足”。这个方案不依赖隔离级别,也不需要额外的锁,是课设里最值得写的并发控制点。

5.2 坑二:事务里先查后改导致丢失更新

现象:用存储过程或脚本模拟并发,发现订单数比实际扣减的余票数多,或者出现两张相同乘车人的票占同一张余票。数据库日志里找不到明显的锁等待,数据就是错了。

原因:很多同学写业务是“先SELECT余额,再UPDATE”,这个模式天生有竞态。两个事务同时读到remaining_seats=1,然后各自UPDATE减一,后一个事务在UPDATE上等待,但等待结束后它可能基于旧的快照继续操作,造成覆盖。尤其是在程序里把查到的值传回UPDATE SET,两个事务最终把同一行写成同一个数,两个订单却都建立了。

解决:不要写“SELECT ... FOR UPDATE”再判断,直接写带条件的UPDATE。如果你确实需要先查后改,必须用SELECT ... FOR UPDATE把行锁住,但这样代码多了一步,还容易造成锁等待超时。用原子UPDATE更短,也更不容易死锁。下面这种写法是错误示范,不要在报告里出现:

-- 错误习惯:先查后改造成丢失更新 SELECT remaining_seats INTO v_rem FROM schedule WHERE id = ?; IF v_rem > 0 THEN UPDATE schedule SET remaining_seats = v_rem - 1 WHERE id = ?; END IF;

这个错误直接在UPDATE里用v_rem旧值覆盖,两个事务各自拿到v_rem=1,最终把同一行的remaining_seats写成0,而不是变成-1,但两个订单都建成了,比超卖更隐蔽。

5.3 坑三:误用表锁把售票改成串行,压测一上去就崩

现象:把并发调高后,发现系统吞吐量骤降,所有购票请求排队,一个事务还没结束,后面的全部等待,命令行上全是锁等待超时。

原因:最常见的是建表时用了MyISAM,或者手动执行过LOCK TABLES ... WRITE,也可能在UPDATE schedule时没有走索引,InnoDB升级成表锁。MyISAM根本不支持行锁,任何写操作都会锁整张表;LOCK TABLES WRITE更直接,把整张表锁死;UPDATE语句的WHERE条件没索引也会导致全表扫描,行锁在扫描过程中可能升级成表锁。

解决:全库统一InnoDB,禁止在业务代码里出现LOCK TABLES。检查UPDATE的WHERE列是否被索引覆盖,schedule.id是主键,天然有索引,但如果你的余票扣减条件里除了id还加了depart_date等额外字段,要确保这些字段也走了索引。用EXPLAIN看执行计划:

EXPLAIN UPDATE schedule SET remaining_seats = remaining_seats - 1 WHERE id = 10 AND remaining_seats > 0;

关注type列是否出现ALL或range,key列有没有实际使用索引。出现ALL就要回表扫描,并发一大锁冲突就飙升。课程设计里能写一句“通过EXPLAIN确认行级锁而不是表级锁”,会显得比同龄人更懂性能。

5.4 坑四:死锁导致订单回滚,用户界面报错但订单并未生成

现象:两个购票请求同时成功扣了余票,但一个事务最终回滚,用户看到“网络异常,请重试”,重试后可能又买到同一张票,业务方拿着日志查不到原因,最后在数据库的error log里看到Deadlock found。

原因:死锁的经典场景是锁顺序不一致。比如事务A先更新schedule再插入orders,事务B先插入orders再更新schedule;两个事务各持一把锁等对方,MySQL会检测并回滚代价较小的一方。另一个场景是两条UPDATE语句的WHERE条件范围重叠,比如A更新schedule id=1和2,B更新schedule id=2和1,就会互相等待。

解决:约定所有事务都按同一顺序访问资源:先操作schedule,再操作orders。避免在同一个事务里做外部调用或长时间循环,事务越短,锁持有时间越短,死锁概率越低。另外可以把锁等待超时调短,让失败快速暴露而不是挂起:

SET innodb_lock_wait_timeout = 3; SET innodb_deadlock_detect = ON; -- MySQL 8.x默认ON

查看死锁信息用:

SHOW ENGINE INNODB STATUS\G

打开后看LATEST DETECTED DEADLOCK段,里面明确写了两个事务的SQL和等待的锁。课程设计报告里放这个输出截图,再配合“同一表访问顺序”的说明,就是一篇有深度的实验报告。

6. 实验报告写作与验收验证:一小时跑通全流程的自查清单

到这里方案已经能跑了,但项目标题里还挂着“实验报告”,所以最后一步是把验证过程写进报告,别只交代码。我自己的习惯是先在本地把四个测试场景跑完,再开始写文档:正常购票成功、余票不足时不扣款、退票恢复余票、并发10个线程抢最后一张票但有且仅有一单成功。

验证并发最快的方式是在MySQL客户端开两个会话,同时执行事务。也可以写个简单的并发脚本,但课设不需要太复杂,用下面的SQL模拟即可:

-- 会话A START TRANSACTION; UPDATE schedule SET remaining_seats = 0 WHERE id = 1; -- 不提交,继续在会话B执行下面语句
-- 会话B START TRANSACTION; UPDATE schedule SET remaining_seats = 0 WHERE id = 1; -- 这里会阻塞,观察出现锁等待

如果遇到死锁,就用第5章的SHOW ENGINE INNODB STATUS查看。

报告结构按“需求分析→ER图→关系模式→DDL→核心SQL→并发测试→总结”写,其中并发测试要占一页。去答辩时,老师最关心的是你知不知道每张表为什么存在,以及余票在并发下如何保持正确。把第5章的四个坑用“现象→原因→解决”写进去,那这一页就是你的加分项。

我当年第一次做这个课题时,只写了顺序执行的SQL,被老师一句“同时买最后一张票会怎样”问得哑口无言。后来我养成了一个习惯:动手建表前,先把要防的坑一条条列成测试用例,代码跑完后对着测试用例打勾。数据库课设的分数不在建了多少张表,而在你主动挖了几个坑又填了几个坑。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 8:38:50

Kotlin与Jetpack现代Android开发:核心组件、协程与Compose实战指南

Android开发这几年变化太快&#xff0c;如果你还停留在写findViewById、开AsyncTask的年代&#xff0c;出去面试或者接手新项目&#xff0c;大概率会觉得很吃力。Kotlin彻底转正成了Android的一等语言&#xff0c;Jetpack又把这几年Google在架构、生命周期、UI开发上的最佳实践…

作者头像 李华
网站建设 2026/10/13 8:37:27

PyQt6窗口开发指南:主窗口、事件与托盘管理全解析

开工之前先说明一句。这套教程第一篇讲的是环境搭建&#xff0c;代码该装的装好、跑个空窗口没问题&#xff0c;咱们就接着往里走。本篇只聊一件事&#xff1a;通用窗口。也就是你程序打开后头顶那个带标题栏、可拖拽、可缩放的玩意儿。PyQt6里的通用窗口&#xff0c;说白了就两…

作者头像 李华
网站建设 2026/10/13 8:36:22

Web SCADA电力接线图:HTML5实时可视化与WebSocket数据对接实战

简介&#xff1a;这份资源是一套面向工业自动化前端开发者的 Web SCADA 电力接线系统图实现方案&#xff0c;适用于电力、油田燃气、供水管网等监控场景&#xff0c;帮助开发者理解如何用纯 HTML5 技术替代早期 Flex、Silverlight 等重客户端方案。压缩包共 43 个文件&#xff…

作者头像 李华
网站建设 2026/10/13 8:36:03

开源代码审查实践:分层可见与角色驱动的协作体系

1. 项目概述&#xff1a;这不是代码审查工具&#xff0c;而是一套可落地的开源协作实践体系“open-code-review”这个标题乍看像某个新发布的开源工具&#xff0c;但实际它代表的是一类正在快速演进的工程实践范式——把代码审查&#xff08;Code Review&#xff09;这件事&…

作者头像 李华
网站建设 2026/10/13 8:35:56

MATLAB BP神经网络汽油浓度预测:归一化、时序划分与反推实战

简介&#xff1a;本资源是一份面向人工智能初学者与MATLAB实践者的BP神经网络入门级预测案例&#xff0c;聚焦环境监测与工业过程优化场景&#xff0c;解决汽油浓度这一典型回归预测问题。压缩包共2个文件&#xff08;1个MATLAB数据文件spectra_data.mat用于存储多维环境特征与…

作者头像 李华