ARTICLE DETAIL

资讯详情

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

Java+MySQL高校选课系统实战:表设计、事务防超卖与索引调优

Java+MySQL高校选课系统实战:表设计、事务防超卖与索引调优 简介这是一套面向高校教育管理场景的Web选课系统项目源码基于Java与MySQL开发适合计算机相关专业学生作为课程设计、毕业设计或全栈练手参考。系统划分管理员、学生、教师三类角色覆盖学生信息增删改查、课程录入与教室时间安排、选课退选改选、成绩录入与查询统计以及角色权限分配、数据备份恢复等模块并兼顾界面简洁与操作流程友好。压缩包共82个文件约1.75MB包含22个html页面、9个css样式、5个js脚本及png、jpeg、svg等界面素材另有6个py脚本、4个xml配置、requirements.txt依赖清单与README说明文档整体结构清晰便于按模块阅读与二次开发。目前已有35人学习下载。通过该资源可直观理解选课规则设计、成绩处理流程与前后端交互方式是掌握教育管理类系统开发思路的实用参考。1. 从一张选课冲突表说起JavaMySQL 的高校选课系统到底在解决什么每学期选课季教务老师最怕的不是服务器崩而是同一时间段被塞进两门课的学生名单。手工核对几千条选课记录Excel 卡到转圈改完一轮又冒出新冲突。基于 Java 和 MySQL 开发的高校学生选课管理系统核心就是把这件重复劳动交给数据库约束和事务去扛。它面向管理员、学生、教师三种角色管理员维护学生信息与课程安排学生完成选课退课教师录入成绩。适合有 Java 基础、想找一个能跑通完整业务闭环的 Web 项目练手的开发者也适合需要给小型院系搭一套轻量教务工具的团队。下面按「表怎么设计 → 后端怎么写 → 冲突怎么防 → 坑在哪」的顺序拆开讲。2. 先把三张核心表定下来学生、课程、选课记录2.1 为什么选课记录必须单独建表很多新手会把选课结果直接塞进学生表的一个字段里用逗号拼接课程 ID。这种做法在查询「某门课有多少人选」时就要全表扫描加字符串拆分MySQL 的索引完全用不上。正确做法是拆成三张表student存学生基本信息course存课程与容量scstudent-course存选课关系。sc表里放student_id、course_id、score、select_time一条记录就是一次选课行为。这样设计的好处是选课人数用COUNT(*)配合WHERE course_id ?就能走索引成绩录入只更新sc表的score字段退课就是删一条sc记录。三张表各管各的职责清晰。2.2 建表 SQL 与字段类型选择-- 学生表学号做主键避免用自增 ID 暴露业务含义 CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, password VARCHAR(64) NOT NULL COMMENT 密码哈希, major VARCHAR(50) COMMENT 专业, grade INT COMMENT 年级 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表capacity 控制容量selected 记录已选人数 CREATE TABLE course ( course_id VARCHAR(20) PRIMARY KEY COMMENT 课程号, course_name VARCHAR(100) NOT NULL COMMENT 课程名, teacher_id VARCHAR(20) COMMENT 授课教师工号, credit DECIMAL(3,1) COMMENT 学分, capacity INT DEFAULT 50 COMMENT 容量上限, selected INT DEFAULT 0 COMMENT 已选人数, schedule VARCHAR(50) COMMENT 上课时间段 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选课记录表联合唯一索引防止重复选课 CREATE TABLE sc ( id BIGINT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20) NOT NULL, course_id VARCHAR(20) NOT NULL, score DECIMAL(5,1) DEFAULT NULL COMMENT 成绩NULL 表示未录入, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_course (student_id, course_id), KEY idx_course (course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;student_id用VARCHAR而不是INT因为学号可能带字母或前导零。sc表上的uk_stu_course联合唯一索引是关键它让「同一学生重复选同一门课」在数据库层面直接报错不用在 Java 里先查再插。idx_course索引服务于「查某门课选课名单」和「统计选课人数」这两类高频查询。提示utf8mb4而不是utf8否则学生姓名里的生僻字会变成问号。这个坑我在两个项目里都踩过。3. 用 Spring Boot MyBatis 把选课接口跑通3.1 项目依赖与数据库连接配置后端用 Spring Boot 起步持久层选 MyBatis原因是选课场景里 SQL 逻辑比较重统计、联表、条件更新MyBatis 比 JPA 更直观。pom.xml里引入spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java三个核心依赖即可。application.yml的连接配置spring: datasource: url: jdbc:mysql://localhost:3306/course_system?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 connection-timeout: 3000serverTimezone必须显式指定否则 MySQL 8.0 驱动会报时区错误。maximum-pool-size设 10 对小型院系够用选课高峰期如果并发上来这个值要往上调但别超过 MySQL 的max_connections。3.2 选课接口的 Service 层实现选课不是简单插一条记录它要同时做三件事检查课程是否还有名额、扣减名额、插入选课记录。这三步必须在一个事务里完成。Service public class CourseService { Autowired private CourseMapper courseMapper; Autowired private ScMapper scMapper; Transactional(rollbackFor Exception.class) public String selectCourse(String studentId, String courseId) { // 1. 带条件更新只有还有名额时才扣减返回影响行数 int updated courseMapper.decreaseSelected(courseId); if (updated 0) { return 课程已满或不存在; } // 2. 插入选课记录唯一索引会拦截重复选课 try { scMapper.insert(studentId, courseId); } catch (DuplicateKeyException e) { // 3. 重复选课时手动回滚名额扣减 throw new RuntimeException(已选过该课程); } return 选课成功; } }对应的 Mapper SQLupdate iddecreaseSelected UPDATE course SET selected selected 1 WHERE course_id #{courseId} AND selected lt; capacity /updatedecreaseSelected把「检查容量」和「扣减名额」合并成一条原子 SQLWHERE selected capacity保证不会超卖。如果返回 0说明课程满了或课程号不存在直接返回失败。插入sc记录时如果触发唯一索引冲突Spring 会抛DuplicateKeyException此时事务回滚刚才扣减的名额自动恢复。这套组合拳比「先 SELECT 再 UPDATE」可靠得多后者在并发下会出现两个线程都查到有名额、然后都扣减的情况。注意Transactional默认只对RuntimeException回滚所以rollbackFor Exception.class要显式写上否则受检异常不会触发回滚。4. 并发选课下的名额超卖三种方案与排查手段4.1 乐观锁、悲观锁、条件更新的取舍选课高峰期同一门课可能几十个学生同时点「选课」。如果 Service 层写成「先查selected和capacity再决定是否插入」两个线程可能同时读到selected49, capacity50然后都执行插入最终selected变成 51。常见做法有三种。第一种是悲观锁在查询课程时加SELECT ... FOR UPDATE把行锁住直到事务结束。优点是逻辑简单缺点是锁持有时间长并发高时大量请求排队。第二种是乐观锁给course表加version字段更新时带WHERE version ?冲突就重试。适合冲突不频繁的场景。第三种就是上面用的条件更新把判断逻辑写进WHERE子句靠数据库的行锁保证原子性。选课场景下第三种最简洁不需要额外字段也不需要重试逻辑。4.2 用 EXPLAIN 和慢查询日志定位选课卡顿选课接口变慢时先看 SQL 有没有走索引。在 MySQL 客户端执行EXPLAIN SELECT * FROM sc WHERE course_id CS101;如果type列显示ALL说明全表扫描idx_course索引没生效。常见原因是字段类型不匹配比如course_id是VARCHAR但查询时传了数字MySQL 会做隐式转换导致索引失效。慢查询日志的开启方式SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; SET GLOBAL slow_query_log_file /var/log/mysql/slow.log;long_query_time 1表示超过 1 秒的查询会被记录。选课接口正常应该在 100ms 内返回如果慢日志里频繁出现sc表的查询就要检查是不是缺索引或者事务范围太大。4.3 选课失败时先查这三处学生反馈「选不了课」时按顺序排查第一查course表的selected和capacity确认是否真的满了第二查sc表里该学生是否已有该课程记录排除重复选课第三看后端日志有没有DuplicateKeyException或Deadlock found。死锁在选课场景不常见但如果多个事务以不同顺序更新多张表就可能触发。解决办法是统一更新顺序比如永远先更新course再插入sc。5. 成绩录入与角色权限教师和管理员各管什么5.1 教师录入成绩的批量更新写法教师期末录入成绩时一个班几十条记录逐条 UPDATE 效率太低。用 MyBatis 的foreach拼批量更新update idbatchUpdateScore foreach collectionlist itemitem separator; UPDATE sc SET score #{item.score} WHERE student_id #{item.studentId} AND course_id #{item.courseId} /foreach /update这条 SQL 会拼成多条 UPDATE 用分号隔开需要在 JDBC URL 上加allowMultiQueriestrue。更稳妥的做法是用INSERT ... ON DUPLICATE KEY UPDATE但sc表的主键是自增id联合唯一索引不是主键所以ON DUPLICATE KEY不会按预期触发。批量 UPDATE 配合事务是更直接的选择。5.2 三种角色的权限边界怎么划管理员能操作所有表包括新增课程、重置学生密码、导出成绩。教师只能查自己授课的课程和学生名单只能更新自己课程的score字段。学生只能查自己的选课记录和成绩只能操作自己的选课和退课。实现上登录成功后把角色和用户 ID 放进 Session 或 JWT每个接口入口做一次校验。比如教师查成绩的接口SQL 里必须带AND teacher_id #{currentUserId}不能只靠前端隐藏按钮。后端不做校验的话学生改个 URL 参数就能看到别人的成绩这是教务系统最常被忽略的安全问题。提示退课接口要加时间窗口判断比如开学两周后禁止退课。这个逻辑放在 Service 层用select_time和当前时间比较别只靠前端禁用按钮。6. 从能跑到好用选课系统的压测与索引调优技巧项目能跑通之后真正拉开差距的是高峰期表现。我一般会先用 JMeter 或ab对选课接口做一轮压测命令很简单ab -n 1000 -c 50 -p select.json -T application/json http://localhost:8080/api/select-n 1000是总请求数-c 50是并发数select.json里放学生 ID 和课程 ID。跑完之后重点看两个指标Requests per second和Time per request。如果 QPS 卡在几十上不去先查数据库连接池是不是被打满再看course表的行锁等待时间。索引方面除了sc表的联合唯一索引和course_id索引还有一个容易被忽略的点course表的teacher_id上应该加索引。教师登录后第一件事就是查自己的课程列表没有索引的话每次都要全表扫描。加索引的语句ALTER TABLE course ADD INDEX idx_teacher (teacher_id);另一个技巧是把「已选人数」的统计从实时COUNT(*)改成维护selected字段。COUNT(*)在sc表数据量到几十万时即使走索引也要几百毫秒而selected字段的读取是 O(1)。代价是每次选课退课都要同步更新但选课接口本来就在事务里多一条 UPDATE 影响不大。最后说一个我自己的习惯每次改完 SQL 或索引一定用EXPLAIN再看一遍执行计划确认key列显示的是预期索引rows列估算的行数在可接受范围。这个动作花不了十秒但能挡住大部分「改完更慢了」的翻车现场。选课系统的复杂度不在代码量而在并发下的数据一致性把事务边界和索引这两件事做扎实后面加功能就是顺水推舟。希望帮到你。本文还有配套的精品资源点击获取
返回列表