简介这份学生选课管理信息系统课程设计报告面向计算机相关专业学生及需要完成数据库课程设计的开发者围绕学生入校注册、课程信息维护、教师任课安排、选课记录入库及成绩补录等核心业务展开帮助读者理解简化系统数据库SCDB的设计思路与实现流程。资源以rar压缩包形式提供包内文件总数暂未明确主要包含可执行程序及相关配套文件整体约19.03MB适合直接运行体验或作为课程设计参考。目前已有1509人学习下载说明其在同类课设资料中具有一定参考价值。读者可从中获取五个数据表的结构设计、字段说明及业务逻辑组织方式并对照选课、任课、成绩管理等模块梳理数据库建模与功能实现的完整脉络为撰写报告或搭建类似系统提供可借鉴的模板与思路。1. 选课系统课程设计为什么你的系统一上线就被并发抢课冲垮每年到了选课季总有一批课程设计在答辩现场翻车。演示时用三五个账号点几下流程丝滑老师随口问一句“如果 500 个人同时抢同一门课呢”系统立刻露馅——超卖、卡死、数据对不上。学生选课管理信息系统课程设计报告本质不是让你画几张 ER 图交差而是逼你把“并发扣减名额”这个真实工程问题想明白。它适合正在做数据库课设、Java Web 课设的本科生也适合想补一补事务与锁基础的初级开发者。这篇笔记按我实际带课设的思路走先定数据模型和选课状态机再落到建表、事务、索引、压测最后讲几个只有真跑过才会遇到的坑。你照着做至少能扛住答辩老师那几轮追问。2. 先把数据模型和选课状态机定死别急着写界面很多人一上来就打开前端画登录页结果做到一半发现“退课再选”“名额回滚”“重复选课”这些逻辑没地方放只能到处打补丁。选课系统的复杂度全在状态流转上界面反而是最不值钱的部分。先把实体和状态定清楚后面写代码就是填空题。2.1 五张核心表学生、课程、教学班、选课记录、操作日志课程设计里最常见的错误是把“课程”和“教学班”混成一张表。一门“数据结构”可能由三个老师各开一个班名额、时间、地点都不同。拆开之后选课实际选的是教学班不是课程。下面是我一般会用的最小表结构MySQL 8 语法。-- 学生表学号做主键避免用自增 id 暴露业务含义 CREATE TABLE student ( stu_no VARCHAR(12) PRIMARY KEY COMMENT 学号, name VARCHAR(32) NOT NULL, major VARCHAR(64) NOT NULL, grade SMALLINT NOT NULL COMMENT 入学年份 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表只描述课程本身不含名额 CREATE TABLE course ( course_id VARCHAR(16) PRIMARY KEY COMMENT 课程代码, course_name VARCHAR(64) NOT NULL, credit DECIMAL(3,1) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 教学班表名额、教师、时间都挂在这里 CREATE TABLE class_section ( section_id BIGINT PRIMARY KEY AUTO_INCREMENT, course_id VARCHAR(16) NOT NULL, teacher VARCHAR(32) NOT NULL, capacity INT NOT NULL COMMENT 总名额, selected INT NOT NULL DEFAULT 0 COMMENT 已选人数, term VARCHAR(16) NOT NULL COMMENT 学期如2024-2025-1, UNIQUE KEY uk_course_term (course_id, teacher, term) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选课记录表状态字段是灵魂 CREATE TABLE enroll_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(12) NOT NULL, section_id BIGINT NOT NULL, status TINYINT NOT NULL COMMENT 1已选 2已退 3候补, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_section (stu_no, section_id), KEY idx_section_status (section_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明class_section.selected是冗余计数字段用它是为了避免每次选课都去enroll_record里COUNT(*)那在并发下既慢又容易读脏。enroll_record上的唯一键uk_stu_section是防重复选课的最后一道防线别指望应用层判断能挡住并发。参数上capacity和selected都用 INT别用 TINYINT一个班几百人是常态。status用数字而不是字符串索引更小。2.2 选课状态机已选、已退、候补三态怎么流转状态机不画清楚退课逻辑一定写乱。我的做法是选课成功写status1退课不删记录改成status2名额满了进候补写status3。为什么退课不物理删除因为要留痕也方便“退课后名额回滚”用同一行更新避免删了再插导致唯一键冲突。当前状态触发动作目标状态名额变化无记录选课且有名额已选(1)selected 1无记录选课且无名额候补(3)不变已选(1)退课已退(2)selected -1已退(2)重新选课已选(1)selected 1候补(3)有人退课已选(1)不变名额已释放这张表建议直接抄进课设报告答辩时老师一看就知道你想过边界。注意“已退(2) 重新选课”这一行更新时要把status从 2 改回 1而不是插新行否则唯一键会拦你。2.3 用 CHECK 约束和唯一键把脏数据挡在库外应用层代码再严谨也架不住有人直接改库或者并发写。把能下沉到数据库的约束全下沉。selected capacity这种约束 MySQL 8 支持 CHECK虽然它不参与并发控制但能挡住明显越界。ALTER TABLE class_section ADD CONSTRAINT chk_selected CHECK (selected 0 AND selected capacity);逻辑说明CHECK 在插入和更新时校验selected超过capacity会直接报错。但它不能替代事务里的行锁因为两个并发事务可能同时读到selectedcapacity-1然后都加一。真正的并发安全靠下一章的UPDATE ... WHERE selected capacity原子操作。参数上约束名chk_selected要唯一方便排错时定位。3. 选课核心逻辑一条 UPDATE 语句解决超卖超卖是选课系统最经典的翻车点。演示时两个人同时点名额只剩 1结果两个人都选上了。根因是“先查后改”不是原子操作。解决办法不是加 synchronized那在单机多线程下勉强能用一上多实例就废。正确姿势是把判断和扣减压进一条 SQL。3.1 原子扣减名额UPDATE 带 WHERE 条件的写法-- 选课第一步原子扣减名额只有 selected capacity 才会命中 UPDATE class_section SET selected selected 1 WHERE section_id ? AND selected capacity; -- 应用层判断 affectedRows -- 如果 affectedRows 1说明抢到名额继续写选课记录 -- 如果 affectedRows 0说明名额已满走候补逻辑逻辑说明这条 UPDATE 在 InnoDB 里会对命中行加排他锁selected capacity的判断和selected 1的写入在同一个锁范围内完成天然原子。参数section_id是教学班主键走主键索引锁粒度最小。关键点应用层必须检查affectedRows返回 1 才算抢到返回 0 绝不能想当然认为成功。我见过有人不检查返回值直接往下写记录结果名额扣了但记录没写或者反过来。3.2 事务边界扣名额和写记录必须同一个事务扣名额和写选课记录是两步必须包在一个事务里。否则扣了名额但写记录失败名额就凭空少了。// 伪代码展示事务边界 Transactional(rollbackFor Exception.class) public EnrollResult enroll(String stuNo, Long sectionId) { // 1. 原子扣减名额 int affected sectionMapper.tryOccupy(sectionId); if (affected 0) { // 名额满写候补记录 enrollMapper.insertOrUpdate(stuNo, sectionId, 3); return EnrollResult.waitlist(); } // 2. 写选课记录唯一键冲突说明重复选课 try { enrollMapper.insertOrUpdate(stuNo, sectionId, 1); } catch (DuplicateKeyException e) { // 重复选课必须回滚名额扣减 throw new BizException(已选过该课程); } return EnrollResult.success(); }逻辑说明Transactional保证两步要么都成要么都回滚。insertOrUpdate用INSERT ... ON DUPLICATE KEY UPDATE status1实现这样“已退重选”也能覆盖。参数rollbackFor Exception.class确保受检异常也回滚默认只回滚运行时异常这是个高频坑。注意唯一键冲突抛异常后事务回滚名额扣减自动撤销不用手写补偿。3.3 退课与名额回滚别用 DELETE用状态更新退课逻辑很多人写成DELETE FROM enroll_record然后selected - 1。问题有两个一是丢历史二是并发下可能把别人的名额减掉。-- 退课先改状态再回滚名额同一事务 UPDATE enroll_record SET status 2 WHERE stu_no ? AND section_id ? AND status 1; -- 只有上面 affectedRows 1 才执行下面这句 UPDATE class_section SET selected selected - 1 WHERE section_id ? AND selected 0;逻辑说明第一条 UPDATE 带status 1条件保证只有“已选”状态才能退重复退课 affectedRows 为 0不会误减名额。第二条带selected 0兜底防止减成负数。参数上stu_no section_id走唯一键索引定位快。退课后如果有人候补可以再触发一次候补转正但那是进阶逻辑课设里可以先手动处理。3.4 用 JMeter 压一把50 并发抢 10 个名额看结果写完不压测等于没写。用 JMeter 或 wrk 模拟并发看最终selected是否等于capacity有没有超卖。# 用 wrk 简单压测选课接口50 个并发连接持续 10 秒 wrk -t4 -c50 -d10s --latency \ -s enroll.lua \ http://localhost:8080/api/enroll逻辑说明-t4是 4 个线程-c50是 50 个并发连接-d10s跑 10 秒--latency输出延迟分布。enroll.lua里随机生成学号避免唯一键冲突干扰。压完去库里查SELECT selected, capacity FROM class_section WHERE section_id ?如果selected超过capacity说明你的原子扣减没生效回去检查 SQL 是不是写成了先查后改。参数上并发数要大于名额数才有意义名额 10 就压 50 并发。4. 索引、分页和查询优化选课列表别拖垮数据库选课系统不只是写读也很多。学生要查“我选了什么”老师要查“这个班谁选了”教务要查“某学期开课情况”。查询写不好列表页一拉就卡。这一章讲几个课设里最容易被忽略的索引和分页问题。4.1 选课记录表的联合索引怎么建才走得到enroll_record上最常见的查询是“按学生查已选”和“按教学班查名单”。这两个查询方向不同索引也要分开建。-- 按学生查stu_no status ALTER TABLE enroll_record ADD KEY idx_stu_status (stu_no, status); -- 按教学班查section_id status ALTER TABLE enroll_record ADD KEY idx_section_status (section_id, status);逻辑说明联合索引遵循最左前缀idx_stu_status能命中WHERE stu_no ? AND status 1也能命中只带stu_no的查询。idx_section_status同理。注意别建(status, stu_no)这种顺序因为status区分度低放前面会导致索引选择性差。参数上status只有三个值区分度低所以必须放在stu_no或section_id后面。4.2 深分页的坑LIMIT 10000, 20 为什么越翻越慢教务查全校选课记录翻到第 500 页时查询突然变慢。原因是LIMIT 10000, 20会先扫描前 10000 行再丢弃越翻越慢。-- 慢扫描 10000 行丢弃 SELECT * FROM enroll_record WHERE status 1 ORDER BY id LIMIT 10000, 20; -- 快用上次最大 id 做游标 SELECT * FROM enroll_record WHERE status 1 AND id ? ORDER BY id LIMIT 20;逻辑说明游标分页记住上一页最后一条的id下一页从id 上次最大 id开始走主键索引扫描行数固定。参数?是上一页返回的最后一条记录 id。课设里如果数据量不大深分页问题不明显但答辩时能说出这个优化点加分不少。注意游标分页不支持跳页适合“下一页”场景。4.3 统计已选人数COUNT 和冗余字段怎么选“这个班选了多少人”有两种查法实时COUNT(*)或读class_section.selected。前者准但慢后者快但可能不一致。方案优点缺点适用场景COUNT(*) 实时统计绝对准确并发下慢大表全扫对账、报表读 selected 冗余字段快O(1)需保证事务内同步更新列表页、选课判断我的做法是选课判断和列表页读selected对账任务每天跑一次COUNT(*)和selected比对不一致就告警。课设里可以只做前者但报告里写上对账思路显得你想过一致性。5. 避坑与排查选课系统上线前必须过的五道坎这一章是我带课设时学生踩得最多的五个坑每个都按“现象 → 原因 → 解决”写。你照着排查能省下大量调试时间。5.1 现象名额扣了但选课记录没写数据对不上原因扣名额和写记录没在同一个事务里或者写记录抛异常后没回滚。常见于把两步写在两个 Service 方法里外层没加事务。解决确认Transactional加在 public 方法上且同类内部调用不生效Spring AOP 代理问题。把两步放进同一个方法或者用编程式事务TransactionTemplate包起来。排查时看日志里有没有“扣减成功”但“插入失败”的间隔。5.2 现象两个人同时选最后一个名额都显示成功原因用了“先 SELECT 查名额再 UPDATE 扣减”的写法两个事务都读到selected capacity - 1都认为有名额。解决改成UPDATE ... WHERE selected capacity原子扣减检查affectedRows。这是超卖的根因没有例外。排查时在压测后查selected是否大于capacity。5.3 现象退课后重新选同一门课报唯一键冲突原因退课用了DELETE物理删除或者退课改状态但重选时用了INSERT而不是UPDATE。唯一键uk_stu_section拦住了重复插入。解决退课改status 2不删行重选用INSERT ... ON DUPLICATE KEY UPDATE status 1。这样同一行状态流转不触发唯一键冲突。排查时看enroll_record里同一stu_no section_id是不是有多行。5.4 现象选课接口响应越来越慢最后超时原因enroll_record表没建索引或者事务里做了耗时操作比如发通知、写日志到远程。长事务持有行锁并发一高就排队。解决给stu_no、section_id建索引事务里只做数据库操作通知和日志放到事务提交后异步做。排查时用SHOW ENGINE INNODB STATUS看有没有锁等待或者开慢查询日志。5.5 现象候补转正后名额没释放或者释放了但没人转正原因候补逻辑没和退课联动。退课时只减了selected没去查候补队列或者查了候补但转正时又扣了一次名额。解决退课事务里减完selected后查status 3的候补记录按时间排序取第一条把status改成 1。注意此时名额已经释放不要再扣selected。排查时看候补记录的status和selected是否匹配。6. 进阶技巧用乐观锁和缓存把选课系统再提一档前面讲的原子扣减是悲观锁思路靠数据库行锁保证安全。它简单可靠但热点教学班的行锁会成为瓶颈——几百人抢一个班全堵在那一行上。如果课设想拿高分可以再往前一步乐观锁 本地缓存。这一章讲怎么在不引入复杂中间件的前提下把并发能力再提一档。6.1 乐观锁版本号用 version 字段替代行锁乐观锁的思路是读的时候记下版本号更新时带上版本号条件版本号对不上就重试。-- 加 version 字段 ALTER TABLE class_section ADD COLUMN version INT NOT NULL DEFAULT 0; -- 乐观锁扣减带上读到的 version UPDATE class_section SET selected selected 1, version version 1 WHERE section_id ? AND selected capacity AND version ?;逻辑说明version ?是读到的旧版本号如果期间有别人更新过version变了这条 UPDATE 影响行数为 0应用层重试。参数?是查询时拿到的版本号。乐观锁适合冲突不激烈的场景热点班冲突激烈时重试次数会飙升反而不如悲观锁。所以我的建议是普通班用乐观锁热点班用悲观锁或者热点班单独限流。6.2 本地缓存名额用 Caffeine 挡住读请求选课列表页每次刷新都查库没必要。用本地缓存把教学班名额缓存几秒挡住大部分读请求。// 用 Caffeine 缓存教学班名额写入后 3 秒过期 CacheLong, Integer capacityCache Caffeine.newBuilder() .expireAfterWrite(Duration.ofSeconds(3)) .maximumSize(1000) .build(); public int getRemaining(Long sectionId) { return capacityCache.get(sectionId, id - { ClassSection s sectionMapper.selectById(id); return s.getCapacity() - s.getSelected(); }); }逻辑说明expireAfterWrite(3)表示写入 3 秒后过期选课高峰期最多脏 3 秒可接受。maximumSize(1000)防止缓存无限增长。注意缓存只用于展示真正的选课扣减必须走数据库不能信缓存。参数上过期时间根据业务容忍度调课设里 3 到 5 秒都行。6.3 用对账任务兜底每天跑一次 COUNT 和 selected 比对再好的并发控制也可能有漏网之鱼对账是最后一道保险。-- 对账查询找出 selected 和实际记录数不一致的教学班 SELECT s.section_id, s.selected, COUNT(r.id) AS real_count FROM class_section s LEFT JOIN enroll_record r ON r.section_id s.section_id AND r.status 1 GROUP BY s.section_id, s.selected HAVING s.selected COUNT(r.id);逻辑说明LEFT JOIN保证没有选课记录的教学班也能查出来HAVING过滤出不一致的行。参数上status 1只统计已选候补和已退不算。这个查询建议每天凌晨跑一次结果写日志或告警。课设里可以做成一个定时任务答辩时演示“故意改脏数据对账任务能发现”很加分。我自己的习惯是任何涉及名额、库存、余额的系统先写对账 SQL再写业务逻辑。因为业务逻辑可能写错但对账能告诉你错在哪。选课系统课程设计看着简单真把并发、事务、索引、对账都走一遍你对数据库的理解会上一个台阶。希望帮到你。本文还有配套的精品资源点击获取