简介:这份《数据库系统课程设计报告——学生选课管理信息系统》面向数据库课程设计、课程大作业与答辩准备的学生,可作为从需求分析到系统实现的完整范文模板。内容围绕学生选课业务展开,覆盖可行性分析与功能需求、组织结构和模块划分、业务流与数据流分析、数据字典,以及实体属性联系、CDM概念模型与PDM逻辑模型设计;物理实现部分给出SQL Server各表设计、完整性约束建表代码,以及视图、索引、存储过程和触发器的创建与测试,并延伸至学生信息、课程信息、登录和成绩管理等功能模块调试。资源包共1个docx文档,约3.96MB,目录完整、章节清晰,便于按课程设计流程对照参考和改写。已有852人学习,适合需要快速搭建报告框架、补齐数据库设计与SQL实现细节的读者。
1. 从一张手写选课表说起:数据库系统课程设计要交付的四样东西
题目发下来那天,很多人的第一反应是打开编辑器先建三张表,结果做到一半发现选课记录和课程容量对不上,报告里 E-R 图和实际表结构又是两套。学生选课管理信息系统这个题目之所以年年被选,不是因为它简单,而是因为它把数据库课程里几乎所有硬骨头都塞进了一个场景:多对多联系、复合主键、外键级联策略、事务与并发、视图与统计查询。它要交付的其实是四样东西——一份自洽的需求与概念设计、一套能跑起来的表结构与约束、一段扛得住并发选课的事务逻辑、一份能自证正确性的测试记录。这份数据库系统课程设计报告写得好不好,八成取决于后面三样有没有真跑过,而不是 E-R 图画得漂不漂亮。下面按我自己带课设的习惯,从设计推到实现,再推到验收。
2. 需求到 E-R 图:选课系统的实体、联系和基数怎么定
2.1 需求里必须先问出来的六件事
课程设计的需求分析最容易写成散文,一段“系统应支持学生选课、退课、查询成绩”就结束了,等到建表时才发现每个动词背后都缺参数。我的做法是先把动词全部换成带主语、宾语、约束条件的句子,能问出答案的才算需求。选课这个动作至少要回答六件事:谁选(在校且在籍的学生,休学状态能不能选)、选什么(是课程还是某个学期开的教学班)、什么时候能选(选课窗口的起止时间)、选几个(学分上限、门数上限)、选不上怎么办(容量满了是排队还是直接拒绝)、选完之后谁能改(退课截止时间、成绩录入后还能不能退)。
这六个问题里,最容易漏的是“教学班”这个概念。课程是抽象的“数据结构,4 学分”,教学班才是“2024 秋季,某教师,周三第 3-4 节,容量 60 人”。学生选的是教学班,不是课程。如果 E-R 图里把学生直接连到课程上,后面容量、教师、教室、时间冲突全部无处安放,这是课设里最常见的结构性返工。
还有两个隐藏需求值得写进报告,能明显拉开差距:一是时间冲突检测,同一学期同一时间段只能选一个教学班;二是先修课校验,选高级课程前要检查是否已修并通过先修课程。这两条不一定都要实现,但在需求章节里点出来并说明取舍理由,评审时会被认为是真想过业务。
2.2 E-R 图:三个实体、两个多对多,别把选课画成属性
概念设计阶段建议老老实实画一张 Chen 记法的 E-R 图,实体用矩形,联系用菱形,基数标在连线上。选课系统的骨架其实很小:
| 实体 | 主要属性 | 说明 |
|---|---|---|
| 学生 | 学号、姓名、性别、出生日期、入学年份、学籍状态 | 学号做主标识 |
| 教师 | 工号、姓名、职称、所属院系 | 工号做主标识 |
| 课程 | 课程号、课程名、学分、学时、课程性质 | 课程号做主标识 |
| 教学班 | 班级号、学期、上课时间、教室、容量、已选人数 | 依赖课程与学期存在 |
| 院系 | 院系号、院系名 | 学生、教师、课程都归属它 |
联系有四个:学生与院系是多对一,教师与院系是多对一,课程与院系是多对一,教师与教学班是一对多(一个教师可以开多个班,一个班只由一个教师主讲),课程与教学班是一对多,学生与教学班是多对多——这个多对多联系“选课”必须单独成表,并且带自己的属性:选课时间、选课状态、成绩、成绩录入时间。
教学班要不要画成弱实体,是个可以展开讲两句的点。它的存在依赖于课程和学期,标识可以用“课程号 + 学期 + 班序号”这个组合,去掉课程它就失去意义,符合弱实体的定义。课设里把它画成弱实体、用双线矩形加双线菱形连到课程,概念设计的分数会好看一些,逻辑设计时也自然导出复合唯一约束。
另一个常被忽略的是时间属性。选课不是一次性动作,它有生命周期:已选、已退、已录成绩。状态字段必须进表,否则退课只能物理删除,成绩录完就查不到历史,报告里的“数据可追溯”就成了空话。
2.3 关系模式转换与 3NF 检查
E-R 图转关系模式有一套固定规则:每个强实体转一张表,一对多联系把“一”端的主键放到“多”端做外键,多对多联系单独转表、两端主键做联合主键、联系自身的属性一起带过去。转完得到七张表:
院系(dep_id, dep_name) 专业(major_id, major_name, dep_id) 学生(sno, sname, gender, birth_date, major_id, enroll_year, status) 教师(tno, tname, title, dep_id) 课程(cno, cname, credit, hours, course_type, dep_id) 教学班(tc_id, cno, tno, term, class_no, capacity, enrolled, weekday, section, classroom) 选课(sno, tc_id, status, select_time, score, score_time)接着做范式检查,重点看三张表。学生表里如果写了“院系名”,就产生传递依赖(学号→专业→院系),违反 3NF,所以要拆出专业表和院系表。选课表里如果写了“学分”,也是传递依赖(学号+班级号→课程→学分),学分应该留在课程表,查询时 JOIN 出来。教学班表里的“课程名”“教师名”同理,只留外键。
这里有个必须提前说明的例外:教学班表的enrolled(已选人数)是刻意冗余的。严格按范式,人数应该由选课表 COUNT 出来,但每次选课都去 COUNT 一遍全表,在高并发下既慢又难加锁。保留这个计数字段属于有意识的反范式,代价是要在事务里保证它和明细表同步——这一点在第 4 章会展开,报告里也要写清楚,否则评委会认为你没做范式分析。
还有一个字段设计上的抉择:成绩放选课表还是单独建成绩表。一门课只有一次总评成绩,放选课表最简单;如果课程有平时、期中、期末多次成绩,就该拆出成绩明细表。课设规模下前者够用,但在报告里说明“若扩展为过程性考核则拆表”,是个加分项。
3. 建表与约束:把选课规则写进 DDL 而不是写在报告里
3.1 字段类型和主键的选型
建表前先把类型定死,后面改类型是最烦的返工。学号用CHAR(12)而不是INT,因为学号可能有前导零、也不会参与算术运算;课程号CHAR(8);学期用CHAR(11)存成2024-2025-1这种格式,比两个整数字段更好读,也方便排序和范围查询。性别用CHAR(1)加 CHECK 限定为 M/F,别用中文“男/女”占三个字节还容易踩字符集问题。学分用DECIMAL(3,1),因为确实存在 0.5、1.5 学分的课,用 INT 会丢精度。
主键策略上,学生、教师、课程、院系用业务主键(学号、工号等),这是课设里最清晰的做法,也方便演示时肉眼核对。教学班和选课记录用自增代理主键更省事:教学班加一个tc_id BIGINT AUTO_INCREMENT,同时保留(cno, term, class_no)的唯一约束;选课表用(sno, tc_id)联合主键,天然防重复选课,不需要额外加 ID。
提示:如果数据库是 MySQL,
utf8mb4字符集和utf8mb4_0900_ai_ci排序规则要一开始就定好,中途改字符集要重建表,课设演示前一天干这事非常容易翻车。
3.2 约束是把业务规则写死在数据库里
很多人把“容量不能超”“成绩 0 到 100”只写在 Java 或 Python 的业务层,数据库里毫无防护,一旦有第二条写入路径(比如手工补录、导入脚本)规则就形同虚设。我的习惯是能在 DDL 里表达的规则一律写进 DDL。
CREATE TABLE teaching_class ( tc_id BIGINT AUTO_INCREMENT PRIMARY KEY, cno CHAR(8) NOT NULL, tno CHAR(8) NOT NULL, term CHAR(11) NOT NULL, class_no SMALLINT NOT NULL, capacity SMALLINT NOT NULL, enrolled SMALLINT NOT NULL DEFAULT 0, weekday TINYINT NOT NULL, section TINYINT NOT NULL, classroom VARCHAR(32), status VARCHAR(16) NOT NULL DEFAULT 'OPEN', CONSTRAINT fk_tc_course FOREIGN KEY (cno) REFERENCES course(cno), CONSTRAINT fk_tc_teacher FOREIGN KEY (tno) REFERENCES teacher(tno), CONSTRAINT uk_tc_natural UNIQUE (cno, term, class_no), CONSTRAINT ck_tc_capacity CHECK (capacity BETWEEN 1 AND 500), CONSTRAINT ck_tc_enrolled CHECK (enrolled >= 0 AND enrolled <= capacity), CONSTRAINT ck_tc_term CHECK (term REGEXP '^[0-9]{4}-[0-9]{4}-[12]$') ); CREATE TABLE enrollment ( sno CHAR(12) NOT NULL, tc_id BIGINT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'SELECTED', select_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, score DECIMAL(5,2), score_time DATETIME, PRIMARY KEY (sno, tc_id), CONSTRAINT fk_en_sno FOREIGN KEY (sno) REFERENCES student(sno), CONSTRAINT fk_en_tc FOREIGN KEY (tc_id) REFERENCES teaching_class(tc_id), CONSTRAINT ck_en_status CHECK (status IN ('SELECTED','DROPPED','SCORED')), CONSTRAINT ck_en_score CHECK (score IS NULL OR (score >= 0 AND score <= 100)), CONSTRAINT ck_en_score_time CHECK ((score IS NULL AND score_time IS NULL) OR (score IS NOT NULL AND score_time IS NOT NULL)) );这段 DDL 里有几个点值得在报告里单独解释。uk_tc_natural保证同一门课同一学期不会出现两个班序号相同的教学班,这是弱实体标识在逻辑层的落地。ck_tc_enrolled是容量规则的最后一道防线,正常情况下事务会保证不越界,这条 CHECK 是用来兜住异常路径的。ck_en_score_time这条组合 CHECK 保证成绩和录入时间要么都有、要么都没有,避免出现“有分数没时间”的半截数据。最后一条ck_en_status把选课状态限定为三个值,比用一个布尔字段“是否退课”可读性强得多。
注意 MySQL 在 8.0.16 之前的版本会解析 CHECK 但不执行,如果实验室装的是老版本,这条约束就是摆设。要么升级,要么用触发器补,报告里的测试用例必须实测一次“插入 120 分会不会被拒”,别只贴 DDL 就说约束生效了。
3.3 索引:先看查询再建,别给每列都加
索引不是越多越好,每加一个索引,插入和更新就多一份维护成本。课设里先用EXPLAIN看清几条核心查询的执行计划,再决定建什么。选课系统的读写比例大概是“查询远多于写入”,所以要优先照顾这几条 SQL:按学期列出可选教学班、按学生查课表、按教学班查名单、按学期统计选课人数。
-- 学生课表:按学号 + 学期查 EXPLAIN SELECT tc.tc_id, c.cname, t.tname, tc.weekday, tc.section, tc.classroom FROM enrollment e JOIN teaching_class tc ON tc.tc_id = e.tc_id JOIN course c ON c.cno = tc.cno JOIN teacher t ON t.tno = tc.tno WHERE e.sno = '2021001' AND tc.term = '2024-2025-1' AND e.status = 'SELECTED'; -- 教师名单:按教学班查 EXPLAIN SELECT e.sno, s.sname, e.score FROM enrollment e JOIN student s ON s.sno = e.sno WHERE e.tc_id = 1001;第一条查询的执行路径是“先通过主键(sno, tc_id)定位该学生的选课记录,再回表查教学班”,主键索引已经够用,但tc.term的过滤发生在 JOIN 之后,数据量大时会扫很多无关记录。可以补一个覆盖索引:
CREATE INDEX idx_tc_term_cno ON teaching_class (term, cno, tc_id); CREATE INDEX idx_en_tc_status ON enrollment (tc_id, status, sno);idx_tc_term_cno让“按学期筛教学班”能走索引,顺带把cno带上做覆盖,减少回表。idx_en_tc_status服务第二条查询和“某班未录成绩人数”这类统计。反面例子是给student.sname建索引——课设数据量几百行,姓名查询全表扫描几毫秒,建了纯属占空间,答辩时被问“为什么给姓名建索引”反而不好答。
还有一个容易被忽略的点:外键列在 MySQL 里会自动创建索引,但组合外键的第二列不会。选课表的tc_id是外键,MySQL 会为它建索引,但(tc_id, status)这种组合还是得手动建。报告里列出“索引清单 + 对应查询”,比单纯贴几条 CREATE INDEX 有说服力得多。
4. 选课并发与事务:超选、重复选、计数漂移怎么防
4.1 事务边界画在哪:一次选课是一个原子动作
选课在数据库里涉及两条写操作:往选课表插一条记录,把教学班的enrolled加一。这两步必须同生共死,否则就会出现两种经典脏数据:记录插了但计数没加,容量看起来还有空位,实际已经超了;计数加了但记录没插,白白占了一个名额,学生却查不到自己的课。
所以事务边界非常清晰:从“检查并占用容量”开始,到“写入选课记录”结束,中间任何一步失败都整体回滚。
START TRANSACTION; UPDATE teaching_class SET enrolled = enrolled + 1 WHERE tc_id = 1001 AND status = 'OPEN' AND enrolled < capacity; -- 应用层判断影响行数:若为 0,说明容量已满或班级关闭 -- 为 0 时执行 ROLLBACK 并返回“选课失败:名额已满” INSERT INTO enrollment (sno, tc_id, status, select_time) VALUES ('2021001', 1001, 'SELECTED', NOW()); COMMIT;这段代码的关键在UPDATE ... WHERE enrolled < capacity这一句。它把“读容量、判断、写回”三步压缩成一条原子语句,数据库在执行时会先对tc_id = 1001这一行加排他锁,再在最新值上判断条件,条件不满足就不更新、影响行数为 0。如果在应用层先SELECT capacity, enrolled再在内存里判断,两个并发请求会同时读到enrolled = 59, capacity = 60,都认为还有一个名额,最后双双写入,变成 61 人——这就是课设演示时最经典的超选现场。
顺序也很讲究:先占容量,再插记录。反过来先插记录再更新容量,如果更新失败回滚,记录也跟着回滚,结果一样,但先占容量能让“满员”的失败发生在最前面,少一次无效插入和唯一键冲突。
4.2 防重复选:唯一约束比“先查再插”可靠
重复选课有两个来源:学生手快点了两次、前端重复提交。用“先 SELECT 看有没有、没有再 INSERT”的写法,在并发下依然会插进去两条,因为检查和插入之间有窗口。正确做法是把这条规则交给唯一约束,也就是选课表的联合主键(sno, tc_id),插入重复时数据库抛唯一键冲突,应用层捕获后返回“已选过该课程”。
-- 幂等写入的写法:已存在就更新状态,不存在就插入 INSERT INTO enrollment (sno, tc_id, status, select_time) VALUES ('2021001', 1001, 'SELECTED', NOW()) ON DUPLICATE KEY UPDATE status = IF(status = 'DROPPED', 'SELECTED', status), select_time = IF(status = 'DROPPED', NOW(), select_time);这里用到了 MySQL 的ON DUPLICATE KEY UPDATE做“退课后重新选”的幂等处理:如果这条记录之前被退课(status 为 DROPPED),重新选时把它改回 SELECTED 并刷新时间;如果本来就是 SELECTED,则保持原样,不重复加容量。注意这段语句本身不改enrolled,容量仍然由前面那条条件 UPDATE 负责,两者在同一个事务里,逻辑才闭合。
如果用 PostgreSQL,对应写法是ON CONFLICT (sno, tc_id) DO UPDATE;用 SQL Server 则是先MERGE或者捕获 2627 错误。报告里写清楚目标数据库和对应语法,别混着抄。
4.3 退课、死锁与批量选课的顺序问题
退课是选课的逆操作,事务里先删记录、再减计数,顺序不能反。反过来的话,先减计数成功、删记录失败回滚,计数虽然也回滚了,但在回滚前的窗口里,别的会话可能已经读到偏小的enrolled并趁虚而入,造成实际人数超过容量。
START TRANSACTION; DELETE FROM enrollment WHERE sno = '2021001' AND tc_id = 1001 AND status = 'SELECTED'; -- 影响行数为 1 才执行下面的减计数 UPDATE teaching_class SET enrolled = enrolled - 1 WHERE tc_id = 1001 AND enrolled > 0; COMMIT;死锁是并发章节里必须提一笔的。单个学生选一门课不会死锁,但“批量选课”接口——学生一次性提交 5 门课——就可能出问题:会话 A 锁了 1001 再等 1002,会话 B 锁了 1002 再等 1001,互相等待,数据库检测到后牺牲一个。解法很简单,批量操作前把tc_id排序,所有会话按同一顺序加锁,环路就断了。
# 批量选课前对教学班 ID 排序,避免交叉加锁导致死锁 tc_ids = sorted(request.tc_ids) for tc_id in tc_ids: ok = enroll_one(student_no, tc_id) if not ok: rollback_all() return {"code": "ENROLL_FAILED", "tc_id": tc_id}那段 Python 只是示意业务流程,真正的加锁发生在每条 SQL 内部,重点是“排序”这个动作。选课高峰时同一批学生抢的往往是同一组热门课,顺序一致能把死锁率压到接近零。另一个办法是给整个批量操作设一个较短的锁等待超时,超时即失败重试,但这属于工程兜底,不如排序来得干净。
隔离级别这块,MySQL 默认的可重复读就够用,因为防超选依赖的是UPDATE的当前读加行锁,不是快照读。把隔离级别降到读已提交也能工作,锁范围更小、并发更高,但如果同一事务里要先读后写,读已提交下会出现不可重复读,得自己保证读到的数据不被别人改。课设里我的建议是保持默认,把精力花在条件更新和唯一约束上,别在隔离级别上折腾。
5. 避坑与排查:课程设计里最容易翻车的五个地方
5.1 建表阶段:约束写了但没生效
现象:报告里明明写了“成绩必须在 0 到 100 之间”,演示时手工插一条 120 分的记录,数据库照单全收,评委当场让你解释。
原因有两个。一是 MySQL 8.0.16 之前的版本解析 CHECK 约束但不执行,DDL 建表成功、SHOW CREATE TABLE里也能看到 CHECK,但插入时被静默忽略;二是 CHECK 写在了错误的层级,比如把容量约束写在选课表上,而容量字段在教学班表,约束根本管不到。
解决:先确认数据库版本和 CHECK 支持情况,用一条INSERT实测。实测不生效就改用触发器,或者在应用层加校验并在报告里注明“数据库版本限制,约束由应用层兜底”。约束一定要写在字段所在的表上,跨表规则(比如选课总数不超过 6 门)无法用 CHECK 表达,只能靠触发器或事务内的统计查询。测试用例要成对出现:一条合法插入、一条非法插入,截图保留报错信息,这是报告里最有力的证据。
5.2 建表阶段:外键级联把历史数据删没了
现象:测试时删了一门课,结果这门课下所有教学班、所有学生的选课记录、成绩全部消失,报告里的统计数字瞬间对不上。
原因:建外键时图省事写了ON DELETE CASCADE,从课程到教学班、从教学班到选课一路级联,删除像多米诺骨牌一样传到底。课设数据本来就不多,一删就空了。
解决:业务数据一律用ON DELETE RESTRICT,禁止物理删除有引用的记录,改为状态字段软删除(课程status = 'ARCHIVED',教学班status = 'CLOSED')。只有纯日志、纯临时表才考虑级联。报告里画一张“删除顺序图”,说明要删一门课必须先检查有没有教学班、有没有选课记录,把这个检查写成一段 SQL 放进测试章节,比口头说“做了完整性保护”强得多。
-- 删除课程前的引用检查,任何一项大于 0 就拒绝删除 SELECT (SELECT COUNT(*) FROM teaching_class WHERE cno = 'CS101') AS tc_cnt, (SELECT COUNT(*) FROM enrollment e JOIN teaching_class tc ON tc.tc_id = e.tc_id WHERE tc.cno = 'CS101') AS en_cnt;5.3 并发阶段:计数和明细对不上
现象:压测跑完,教学班显示enrolled = 58,实际SELECT COUNT(*)出来是 60,或者反过来容量 60 的班显示 61 人。
原因通常有三种。一是插入记录和更新计数不在同一个事务里,中间抛异常只回滚了一半;二是异常处理分支写漏了,捕获到唯一键冲突后直接return,忘了回滚已经执行的加计数;三是有人手工改过数据,或者用了ON DUPLICATE KEY UPDATE但没处理好“重复选不应该再加容量”的逻辑。
解决:把选课封装成一个函数,事务开启、占容量、插记录、提交四步固定,任何一步失败统一走回滚。再写一条对账 SQL,定期或在每次压测后跑一遍,把不一致的教学班列出来:
SELECT tc.tc_id, tc.capacity, tc.enrolled, COUNT(e.sno) AS actual FROM teaching_class tc LEFT JOIN enrollment e ON e.tc_id = tc.tc_id AND e.status IN ('SELECTED','SCORED') GROUP BY tc.tc_id, tc.capacity, tc.enrolled HAVING tc.enrolled <> actual OR tc.enrolled > tc.capacity;这条 SQL 返回空集才算过关。报告里把它作为“一致性校验”的固定用例,附上压测前后的两次执行结果。
5.4 并发阶段:压测时偶发死锁,重试就好了但说不清原因
现象:并发选课脚本跑到第 3 轮开始报死锁,日志里一行Deadlock found when trying to get lock,重试一次又成功了。
原因:多个会话以不同顺序锁多行。最典型的是批量选课,A 学生提交 [1001, 1002],B 学生提交 [1002, 1001],两边各持一把锁再等对方那把;其次是长事务里穿插了无关的更新,锁持有时间被拉长,撞车概率上升。
解决:批量操作前对 ID 排序,统一加锁顺序;缩短事务,把参数校验、权限检查这些不涉及写锁的逻辑挪到事务外;给业务加有限次重试(比如 3 次,每次退避几十毫秒),并统计重试率。报告里不要只写“加了重试”,要写清楚根因是加锁顺序不一致,重试只是兜底。
5.5 演示阶段:数据和截图对不上
现象:答辩前一天重新导了一次数据,报告里贴的课表截图、人数统计全部失效,现场演示时页面空空如也。
原因:没有可重复执行的初始化脚本,所有测试数据都是手工敲的,改一次库就乱一次;也没有区分“演示用数据”和“测试用数据”。
解决:写两个脚本,一个schema.sql建表建约束建索引,一个seed.sql灌入固定数据(3 个院系、5 个专业、20 名学生、8 门课、10 个教学班,其中两个班容量设为 30 和 2,方便现场演示“选满”)。再加一个reset.sh,一条命令 drop 库、重建、灌数据,答辩前跑一遍,报告里的截图全部基于这套固定数据生成。这套东西看着笨,但能省掉答辩当天百分之八十的慌乱,属于典型的后悔药。
6. 验收前的一次自测:并发压测与一致性校验怎么做
代码写完不等于课设做完,能拿出“我证明它是对的”的证据才算。我的习惯是写一个几十行的并发脚本,把所有学生同时放进去抢一个容量很小的教学班,跑完立刻对账。
import threading from concurrent.futures import ThreadPoolExecutor import pymysql def enroll(sno, tc_id): conn = pymysql.connect(host="127.0.0.1", user="app", password="***", database="course_sys", autocommit=False) try: with conn.cursor() as cur: cur.execute( "UPDATE teaching_class SET enrolled = enrolled + 1 " "WHERE tc_id = %s AND status = 'OPEN' AND enrolled < capacity", (tc_id,)) if cur.rowcount == 0: # 名额已满,直接回滚 conn.rollback() return "FULL" cur.execute( "INSERT INTO enrollment(sno, tc_id, status) VALUES (%s, %s, 'SELECTED')", (sno, tc_id)) conn.commit() return "OK" except pymysql.err.IntegrityError: conn.rollback() # 重复选课由主键拦下 return "DUP" finally: conn.close() # 30 个学生抢容量为 2 的教学班,预期只有 2 个 OK with ThreadPoolExecutor(max_workers=30) as pool: results = list(pool.map(lambda i: enroll(f"2021{i:03d}", 1001), range(30))) print({k: results.count(k) for k in set(results)})这段脚本的价值不在代码本身,而在于它把“并发”变成了一个可以重复执行的动作。跑完之后依次检查三件事:OK的数量必须等于容量、FULL的数量必须等于其余请求数、对账 SQL 返回空集。三项全过,超选防线才算立住。如果OK超过容量,回去看是不是漏了enrolled < capacity这个条件;如果OK少于容量但FULL很多,大概率是锁等待超时,把连接超时和锁等待时间调大再试。
再做一次反向验证:故意把UPDATE的条件改成只写WHERE tc_id = %s,重跑脚本,观察是否出现超选。这个“故意打坏再修好”的过程写进报告,比单纯贴一段正确的代码更能说明你理解锁和条件更新的作用。
最后提醒一句,报告里的测试章节要把环境写清楚:数据库版本、隔离级别、连接池大小、并发线程数、每个用例的预期和实际结果。评委不一定看你的界面,但一定会翻这一章。我自己踩过的最大的坑是压测脚本用了 autocommit 模式,每条 SQL 自动提交,跑出来全是脏数据,排查了大半天才发现事务根本没生效——教训就是任何并发测试前,先确认autocommit是关的,事务是真的事务。希望帮到你。
本文还有配套的精品资源,点击获取