ARTICLE DETAIL

资讯详情

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

SSM学生选课系统:并发选课与事务一致性实战

SSM学生选课系统:并发选课与事务一致性实战 简介这份资源是面向高校计算机相关专业毕业生的学生选课管理系统答辩PPT基于JavaSSMSpringSpringMVCMyBatis技术栈与Mysql数据库开发配合Vue.js、Vue-Router、Vuex及Element UI完成前后端分离与响应式布局可直接用于毕业设计答辩、课程设计汇报或项目实训讲演。PPT内容按研究背景、开发意义、可行性分析经济、技术、操作三方面、界面展示与总结展开完整呈现了从需求分析、数据字典、功能模块与数据库表设计到ER图构建的论述脉络界面部分覆盖登录、课程信息、选课、成绩查询与作业等模块并配有答辩收尾页便于对照讲解技术选型与模块职责。压缩包共1个pptx文件约20.09MB页面为演示文稿风格直接替换姓名与院校信息即可使用。目前已有129人学习下载适合需要快速搭建答辩框架、梳理技术要点与讲演节奏的本科及专科毕业生参考。1. 答辩现场被追问最多的不是界面而是选课瞬间的数据一致性很多同学把 SSM 学生选课管理系统做成了一套能登录、能点选、能看成绩的演示界面干净Vue Element UI 摆出来也好看可一旦评委问两个学生同时抢最后一个名额会怎样同一个学生连点两次选课按钮会不会出现两条记录回答就开始含糊。这道题在 Java 面试八股文里也是高频题本质是并发场景下的幂等与事务边界。选课系统看起来只是教务管理里的一块但它同时踩中了多对多关系建模、状态流转、容量控制和权限校验四个技术点是一套很适合拿来练手的 Java Web 项目。这篇内容按表怎么设计、后端怎么分层、选课怎么写、本地怎么跑起来的顺序把一套基于 SSM 的选课系统拆到你可以在自己机器上复现。2. 把选课业务拆成表学籍、课程与选课记录的 ER 建模数据库表结构定下来之后后端代码的复杂度基本就确定了。很多选课系统写到最后 Service 层一团乱根因不在代码而在于一开始把选课记录当成课程表里的一个字段而不是独立的关系实体。先把实体和关系理清后面的 MyBatis 映射和事务处理才会有清晰的落点。2.1 选课业务里的四类核心实体抛开界面不谈一套选课系统真正落库的实体并不多学生student学号、姓名、所属院系、年级登录账号一般就是学号。教师teacher工号、姓名、所属院系用于发布课程。课程course课程号、课程名、学分、授课教师、容量上限、已选人数、状态开放/关闭。选课记录student_course学生与课程之间的选课关系附带成绩字段。这四张表撑起了登录、课程信息、选课、成绩查询、作业这几块界面。登录界面背后查的是 student 或 teacher 表课程信息界面查 course 表选课界面同时读写 course 与 student_course成绩信息界面读 student_course 里的成绩字段。2.2 多对多关系必须拆出中间表学生和课程天然是多对多一个学生能选多门课一门课能被多个学生选。如果图省事在 course 表里塞一个student_ids字符串字段用逗号拼接短期能跑但一旦要做某门课已选多少人某学生选了几门课按课程统计成绩分布SQL 就会变成字符串处理索引完全失效。常见做法是拆出student_course中间表把关系本身当成一个有属性的实体成绩、选课时间、状态都挂在关系上。这样 count 统计、唯一约束、外键校验都能走索引MyBatis 的 association 与 collection 映射也有明确目标。2.3 建表 SQL 与关键约束下面是这套系统里最核心的三张表字段做了精简但并发相关的约束必须保留。注意student_course上的联合唯一索引它是防止重复选课的最后一道防线。-- 课程表capacity 是容量上限selected_count 是已选人数 CREATE TABLE course ( id BIGINT NOT NULL AUTO_INCREMENT, course_code VARCHAR(32) NOT NULL COMMENT 课程编号, course_name VARCHAR(128) NOT NULL COMMENT 课程名称, teacher_id BIGINT NOT NULL COMMENT 授课教师ID, credit DECIMAL(3,1) NOT NULL DEFAULT 0 COMMENT 学分, capacity INT NOT NULL DEFAULT 50 COMMENT 容量上限, selected_count INT NOT NULL DEFAULT 0 COMMENT 已选人数, status TINYINT NOT NULL DEFAULT 1 COMMENT 1开放 0关闭, PRIMARY KEY (id), UNIQUE KEY uk_course_code (course_code), KEY idx_teacher (teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选课记录表联合唯一索引避免同一学生重复选同一门课 CREATE TABLE student_course ( id BIGINT NOT NULL AUTO_INCREMENT, student_id BIGINT NOT NULL COMMENT 学生ID, course_id BIGINT NOT NULL COMMENT 课程ID, score DECIMAL(5,1) DEFAULT NULL COMMENT 成绩未出分时为NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_student_course (student_id, course_id), KEY idx_course (course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几点需要说明的地方uk_student_course这个联合唯一索引把同一学生对同一课程只能有一条记录交给数据库强制保证。应用层再怎么判断都可能因为并发穿透索引是兜底。selected_count是冗余字段用于避免每次查课程容量都去 count 选课记录。冗余带来的代价是要在选课/退课事务里同步维护这一点在第四章会详细说。课程状态用status控制教务关闭选课之后Service 层要拒绝新选课请求但已选记录保留。表引擎必须是 InnoDBMyISAM 不支持事务选课和容量扣减无法保证原子性。表名主键关键约束主要用途studentid学号唯一登录、选课归属teacherid工号唯一发布课程courseid课程号唯一、容量字段课程信息展示student_courseid(student_id, course_id) 联合唯一选课记录、成绩把表结构定下来前端的课程列表、选课按钮、成绩展示就有了稳定的数据源后端也才有清晰的切入点。3. SSM 三层骨架Mapper 动态 SQL 到 SpringMVC 接口落库SSM 组合Spring SpringMVC MyBatis是这套系统的技术底座。前端用 Vue.js 配合 Vue-Router 和 Vuex 做路由与状态管理通过 Ajax 与后端通信后端用 SpringMVC 暴露接口、Spring 管理 Bean 与事务、MyBatis 负责 SQL 映射。分层的意义不只是看起来规范而是把并发控制、事务边界这类横切逻辑收拢在 Service 层Controller 只管参数校验和返回结构。3.1 包结构与依赖注入顺序项目目录一般这么分controller接收 HTTP 请求做参数校验调用 Service。service与service.impl业务逻辑、事务控制。mapperMyBatis 接口与 XML 一一对应。entity与数据库表对应的实体类。configSpring、SpringMVC、MyBatis 的配置文件。启动顺序是 Spring 容器先加载数据源和 SqlSessionFactory再扫描 Service 与 Mapper最后由 SpringMVC 的 DispatcherServlet 接管请求。写过 Java 环境变量配置的同学在这里会少踩一个坑JDK 版本和 Spring 版本要对齐Spring 5 需要 JDK 8 以上项目里pom.xml的maven.compiler.source和实际运行环境不一致启动时会报 UnsupportedClassVersionError。3.2 MyBatis 动态 SQL 与结果映射选课相关的查询里课程列表常需要按名称模糊、按教师、按状态过滤用whereif拼动态 SQL 最合适。下面这段 Mapper XML 是典型的课程分页查询!-- 课程分页查询支持按名称模糊、教师、状态过滤 -- select idselectCoursePage resultMapCourseMap SELECT c.id, c.course_code, c.course_name, c.credit, c.capacity, c.selected_count, c.status, t.teacher_name FROM course c LEFT JOIN teacher t ON c.teacher_id t.id where if testname ! null and name ! AND c.course_name LIKE CONCAT(%, #{name}, %) /if if testteacherId ! null AND c.teacher_id #{teacherId} /if if teststatus ! null AND c.status #{status} /if /where ORDER BY c.id DESC LIMIT #{offset}, #{limit} /select !-- 结果映射把 teacher_name 映射到实体字段 -- resultMap idCourseMap typecom.example.entity.Course id propertyid columnid/ result propertycourseCode columncourse_code/ result propertycourseName columncourse_name/ result propertyselectedCount columnselected_count/ result propertyteacherName columnteacher_name/ /resultMap逻辑上where会自动去掉第一个多余的ANDif按传入参数决定拼哪些条件#{name}走预编译占位符避免 SQL 注入。LIMIT #{offset}, #{limit}里 offset 由 Service 层根据页码算出来是(pageNum - 1) * pageSize。需要注意resultMap中的column必须和 SELECT 出来的字段别名严格对应选课系统里因为 join 出来的teacher_name不在 course 表里最容易出现映射不上的情况。3.3 SpringMVC 接口设计与返回结构后端接口建议统一返回一个包装类前端 Vue 拿到之后按 code 分支处理。选课接口是最典型的一个RestController RequestMapping(/api/course) public class CourseController { Autowired private CourseService courseService; // 选课接口学生ID从会话或Token中取禁止前端传入 PostMapping(/select) public ResultVoid selectCourse(RequestParam Long courseId, HttpSession session) { Long studentId (Long) session.getAttribute(studentId); if (studentId null) { return Result.fail(未登录); } courseService.selectCourse(studentId, courseId); return Result.success(); } }接口里第一个要划清的红线是studentId必须从会话里取不能接受前端传参。很多演示项目图方便直接从请求体里读studentId结果被测出越权改一下请求参数就能替别的学生选课。第二个点是返回结构统一前端在 Axios 拦截器里根据code决定提示还是跳登录比在每个页面里判断要省事。第三个点Controller 不做业务判断容量校验、重复选课、状态校验全部下推到 Service这样才能把事务边界圈在 Service 方法上。4. 选课瞬间的并发容量扣减、重复选课拦截与事务边界前两章铺完了表和接口真正让选课系统站得住的部分在 Service 层。这一层的每个判断都能在答辩时被追问比如为什么不用先查再更新用了锁会不会慢。下面按问题从浅到深拆一遍。4.1 先查后插的经典并发陷阱直觉写法是查选课记录是否存在不存在就插入同时给课程selected_count 1。这套逻辑在单线程下没问题但两个请求同时进来时检查阶段都读到不存在然后都执行插入选课记录里就出现两条重复。容量扣减更容易失真两个请求同时读到selected_count 49、capacity 50各自判断还有名额最终 51 人。常见做法不是靠加 synchronized而是靠数据库约束和原子更新来收口。4.2 唯一索引兜底与异常转换联合唯一索引uk_student_course能挡住重复插入但应用层得识别这个异常并把它转成友好的业务提示。Spring 里 MyBatis 抛出的DuplicatedKeyException需要转换后处理Service public class CourseServiceImpl implements CourseService { Autowired private CourseMapper courseMapper; Autowired private StudentCourseMapper scMapper; Override Transactional(rollbackFor Exception.class) public void selectCourse(Long studentId, Long courseId) { // 1. 原子扣减容量只允许在有名额且开放时加一 int updated courseMapper.increaseSelected(courseId); if (updated 0) { throw new BizException(课程已满或已关闭); } try { // 2. 插入选课记录唯一索引兜底防重复 scMapper.insert(studentId, courseId); } catch (DuplicateKeyException e) { // 3. 重复选课回滚容量扣减 throw new BizException(请勿重复选课); } } }配套的 Mapper 更新语句必须是原子的不能拆成两步!-- 只有有名额且课程开放时才自增,updated 返回受影响行数 -- update idincreaseSelected UPDATE course SET selected_count selected_count 1 WHERE id #{courseId} AND status 1 AND selected_count lt; capacity /update这段逻辑里有三个关键点。第一selected_count capacity和自增写在同一条 UPDATE 里InnoDB 会对这一行加行锁两个并发请求会被串行化后到的那个读到的是已加一的值。第二返回 0 就意味着满了或关闭了直接抛业务异常不需要先 count 再判断。第三唯一索引异常一定要捕获并回滚容量否则课程表里的数字会虚高。Transactional的rollbackFor Exception.class不能省默认只回滚 RuntimeException业务异常继承的是 Exception 时容易漏。方案是否能防重复选课是否能防超卖性能适用场景先查后插否否高仅单机低并发演示应用层 synchronized单机有效单机有效差单实例部署唯一索引 原子 UPDATE是是好本系统采用分布式锁是是中多实例部署4.3 退课与成绩录入的事务处理退课要保证两件事删掉选课记录同时selected_count - 1两者在同一事务里。订单式写法是先删记录、按返回行数判断是否存在再扣减比先查后删更稳。成绩录入则是对student_course做 UPDATE条件是student_id和course_id都存在录入人只能是自己开课的老师这个权限判断最好在 SQL 的 WHERE 里也带一道防止前端绕过。-- 退课删除记录并返回受影响行数为 0 表示未选过 DELETE FROM student_course WHERE student_id #{studentId} AND course_id #{courseId}; -- 成绩录入同时校验教师是否为授课人 UPDATE student_course sc JOIN course c ON sc.course_id c.id SET sc.score #{score} WHERE sc.student_id #{studentId} AND sc.course_id #{courseId} AND c.teacher_id #{teacherId};这里把教师校验写进 SQL 是为了让是否为授课教师和是否选过课两个条件共用一次数据库往返减少在 Service 里来回查询。代价是 SQL 稍微重一点但选课系统这种并发量下完全值得。5. 本地跑通与答辩自测启动顺序、接口核对和常见报错代码写完之后跑通和自测是两件事。跑通只需要顺序对自测需要你主动构造边界数据。建议按下面的流程走一遍比一路点界面更能发现问题。5.1 启动顺序与依赖检查启动顺序固定先 MySQL再 SSM 后端最后 Vue 前端。MySQL 建完库后先手动插一条学生、一条教师、一条课程否则登录之后看不到课程。后端启动前检查jdbc.properties里的库名、账号密码mybatis-config.xml里的 mapper 路径。前端注意axios.baseURL指向后端端口跨域问题一般在后端加CrossOrigin或配置 CorsFilter。JAVA_HOME 指到 JDK 安装目录的上一级java -version和mvn -v显示的版本要保持一致这个环境变量问题占启动报错的很大比例。5.2 用 curl 和 SQL 交叉验证关键接口不要只靠前端点按钮。选课逻辑至少构造三个场景正常选课、重复选课、容量占满后继续选。用 curl 模拟# 正常选课带上会话 Cookie curl -X POST http://localhost:8080/api/course/select?courseId1 \ -H Cookie: JSESSIONIDxxxx # 验证容量是否被正确扣减重复执行上面命令后观察 mysql -uroot -p school -e \ SELECT id, capacity, selected_count FROM course WHERE id 1; # 验证是否只有一条选课记录 mysql -uroot -p school -e \ SELECT COUNT(*) FROM student_course WHERE student_id 1 AND course_id 1;重点看两个数selected_count是否等于student_course里该课程的实际记录数以及重复请求之后记录数是否还是 1。如果容量超过 capacity说明原子 UPDATE 没生效如果记录数变成 2说明唯一索引缺失或异常没有回滚。5.3 答辩常见的三个追问与应答方向评委一般不会问界面细节三句话就能探到深度选课超员了怎么处理答原子 UPDATE 加行锁不答加 synchronized。重复选课怎么判断答联合唯一索引兜底加异常转换不是前端灰掉按钮。退课时数据怎么保持一致答删除和容量扣减在同一事务里靠受影响行数判断。这三个问题能顺着讲出表设计、事务和并发控制基本就能覆盖整个系统的技术主线。本文还有配套的精品资源点击获取
返回列表