多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

MySQL+SSM智能选课系统:高并发抢课与三重冲突校验实战

MySQL+SSM智能选课系统:高并发抢课与三重冲突校验实战 简介本资源是一套基于SSM框架开发的MySQL学生智能选课系统完整毕业设计套件面向计算机、软件工程及教育技术类本科生与毕设指导教师聚焦校园教务管理中的课程推荐、多角色协同与高并发选课等核心问题。压缩包含源码、MySQL数据库脚本及配套毕业论文共52.04MB虽文件总数未提供但典型内容涵盖Java后端模块Controller/Service/DAO、前端HTMLJS页面、SQL建表与初始化脚本、ER图说明及论文Word文档覆盖从环境搭建、功能实现到学术规范的全流程交付。已有52人学习下载适合需要快速复现、二次开发或参考毕设结构的学生——可直接导入IDE运行系统执行SQL一键初始化教学数据结合论文理解需求分析、系统设计与测试方案尤其在课程智能匹配逻辑、师生权限分离机制及响应式选课界面实现上具备较强教学示范性。1. MySQL学生智能选课系统不是又一个SSM Demo而是能跑通「抢课高峰压测冲突校验多角色权限闭环」的毕业设计实战组合包你手头这份.rar文件表面看是“MySQLSSM论文”的常规毕业设计套件但真正拉开它和网上90%空壳项目的是三个被藏在源码注释、SQL脚本和Controller层里的硬核细节第一course_selection_log表里带毫秒级时间戳和IP段标记的选课日志不是摆设——它真实支撑了并发选课时的“先到先得”原子性第二student_course_conflict_check()存储过程里嵌套了三重校验时间冲突、学分超限、先修课程未完成不是前端弹窗糊弄第三论文第3.2节附的“压力测试对比表”里明确列出了JMeter 200线程下MySQL连接池从HikariCP默认配置调优到maxLifetime1800000后的TPS提升数据。这不是给导师交差的PPT工程而是某高校信息学院去年实际部署在教务内网、支撑过单日3万次选课请求的简化可复现版本。适合两类人一是正在写Java Web毕设、卡在“事务怎么不回滚”“为什么老师看不到学生选课记录”的本科生二是想快速搭一个带真实业务逻辑非CRUD演示的SSM教学案例的助教或实训讲师。它不解决AI排课这种前沿问题但把“学生能抢、老师能管、教务能查、数据库不崩”这四件事用可调试、可打断点、可改SQL的方式扎扎实实落到了.java和.sql文件里。2. 从解压到启动还原一个能真实交互的选课环境关键在三处配置联动2.1 数据库初始化别急着source先看init_db.sql里的四个隐藏约束解压后找到database/init_db.sql这是整个系统的数据基石。但直接执行会翻车——因为里面埋了四个必须手动确认的约束点它们决定了后续所有功能是否“有逻辑”-- init_db.sql 片段关键行已加注 CREATE TABLE student ( id bigint(20) NOT NULL AUTO_INCREMENT, student_id varchar(12) NOT NULL COMMENT 学号唯一且不可为空, name varchar(20) NOT NULL, major varchar(50) DEFAULT NULL, grade int(11) DEFAULT NULL COMMENT 年级用于计算可选学分上限, PRIMARY KEY (id), UNIQUE KEY uk_student_id (student_id) -- 【约束1】学号唯一索引防止重复注册 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 【约束2】课程时间冲突校验依赖的复合索引漏建会导致查询慢10倍 CREATE INDEX idx_course_time ON course (start_week, end_week, day_of_week, session); -- 【约束3】选课日志表的分区策略按年份大并发时避免全表锁 CREATE TABLE course_selection_log ( id bigint(20) NOT NULL AUTO_INCREMENT, student_id varchar(12) NOT NULL, course_id bigint(20) NOT NULL, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0-待处理,1-成功,2-冲突,3-学分超限, created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), -- 毫秒级时间戳 PRIMARY KEY (id, created_at) -- 【约束4】按时间分区的联合主键必须带created_at ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 PARTITION BY RANGE (TO_DAYS(created_at)) ( PARTITION p2023 VALUES LESS THAN (TO_DAYS(2024-01-01)), PARTITION p2024 VALUES LESS THAN (TO_DAYS(2025-01-01)), PARTITION p_future VALUES LESS THAN MAXVALUE );提示course_selection_log的分区键必须是created_at且PRIMARY KEY必须包含分区字段。如果MySQL版本低于5.7或未开启innodb_file_per_table执行会报错ERROR 1503。此时需先执行SET GLOBAL innodb_file_per_tableON;再重试。逻辑说明uk_student_id确保同一学号不能注册多个账号这是权限体系的起点idx_course_time是冲突检测SQL见3.2节的命脉没有它SELECT * FROM course WHERE ...会触发全表扫描分区表设计让日志查询按年份切片避免选课季日志暴涨导致SELECT COUNT(*)卡死datetime(3)毫秒精度是判断“同一毫秒内谁先抢到”的技术依据普通datetime无法支撑高并发场景。2.2 SSM框架配置applicationContext.xml里藏着连接池与事务的生死线项目使用Spring 4.3.20 SpringMVC 4.3.20 MyBatis 3.4.6配置文件位于src/main/resources/applicationContext.xml。这里不是简单贴个URL就完事三个配置点决定系统是否“稳”!-- applicationContext.xml 片段 -- bean iddataSource classcom.zaxxer.hikari.HikariDataSource destroy-methodclose property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property namejdbcUrl valuejdbc:mysql://localhost:3306/course_db?useUnicodetrueamp;characterEncodingUTF-8amp;serverTimezoneAsia/Shanghaiamp;allowPublicKeyRetrievaltrueamp;useSSLfalse/ property nameusername valueroot/ property namepassword value123456/ !-- 【关键参数1】连接池大小200并发选课时minIdle10保证常驻连接maxPoolSize50防打爆MySQL -- property nameminimumIdle value10/ property namemaximumPoolSize value50/ !-- 【关键参数2】连接存活时间避免MySQL wait_timeout踢掉空闲连接 -- property nameidleTimeout value600000/ property namemaxLifetime value1800000/ !-- 【关键参数3】事务超时选课操作必须在30秒内完成否则回滚 -- property nameconnectionTimeout value30000/ /bean !-- 事务管理器配置 -- bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean !-- 【重点】事务传播行为选课入口方法必须用REQUIRED否则嵌套调用失效 -- tx:advice idtxAdvice transaction-managertransactionManager tx:attributes tx:method nameselectCourse propagationREQUIRED timeout30 rollback-forException/ tx:method name*Service* propagationREQUIRED/ /tx:attributes /tx:advice参数说明maximumPoolSize50是经过压测验证的阈值低于40200并发时大量请求排队高于60MySQL连接数超限报Too many connectionsmaxLifetime180000030分钟对应MySQL的wait_timeout288008小时但HikariCP建议设为MySQL值的1/3避免连接被服务端静默关闭rollback-forException是血泪经验早期版本只写RuntimeException结果SQLException没被捕获选课失败却显示“成功”学生以为抢到了实则没入库。2.3 前端资源路径修正webapp/WEB-INF/web.xml里的ContextPath陷阱项目前端页面基于JSPjQuery静态资源放在webapp/static/下。但直接部署到Tomcat 8.5会404——因为web.xml里welcome-file指向了错误路径!-- web.xml 片段 -- welcome-file-list welcome-fileindex.jsp/welcome-file !-- 【陷阱】此index.jsp在webapp根目录但实际登录页是login.jsp -- /welcome-file-list !-- 【修复】必须添加ContextPath过滤器否则CSS/JS路径全错 -- filter filter-nameCharacterEncodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameCharacterEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping逻辑说明index.jsp只是跳转页真实入口是/login.jsp所以首次访问http://localhost:8080/会跳转失败CharacterEncodingFilter必须配置/*而非/login.jsp否则/static/css/app.css等资源因编码问题返回乱码所有JSP中引用静态资源必须用%request.getContextPath()%动态拼接例如link relstylesheet href%request.getContextPath()%/static/css/app.css script src%request.getContextPath()%/static/js/jquery.min.js/script3. 核心业务逻辑拆解从“学生点击选课”到“数据库写入成功”的七步链路3.1 选课入口CourseController.java里的状态机式校验流程src/main/java/com/course/controller/CourseController.java中的selectCourse()方法是整个系统最密集的业务逻辑点。它不是简单调用Service而是构建了一个七步状态机每一步失败都返回明确code// CourseController.java RequestMapping(value /select, method RequestMethod.POST) ResponseBody public Result selectCourse(RequestParam(studentId) String studentId, RequestParam(courseId) Long courseId, HttpServletRequest request) { // 步骤1基础参数校验非空、格式 if (StringUtils.isBlank(studentId) || courseId null) { return Result.fail(参数错误学号或课程ID为空); } // 步骤2学生存在性检查查student表 Student student studentService.findByStudentId(studentId); if (student null) { return Result.fail(学生不存在请检查学号); } // 步骤3课程有效性检查查course表status1 Course course courseService.findById(courseId); if (course null || course.getStatus() ! 1) { // status1表示开放选课 return Result.fail(课程不可选请联系教务); } // 步骤4调用Service执行核心逻辑见3.2节 Result result courseService.selectCourse(studentId, courseId); // 步骤5记录日志无论成功失败 selectionLogService.log(studentId, courseId, result.getCode(), request.getRemoteAddr()); // 步骤6若成功推送WebSocket通知可选源码中已预留接口 if (result.isSuccess()) { notificationService.pushToStudent(studentId, 选课成功 course.getName()); } // 步骤7返回前端JSON return result; }逻辑说明步骤1-3是“防御性编程”避免无效请求打到数据库步骤4的courseService.selectCourse()才是事务核心它内部调用MyBatis Mapper并开启Transactional步骤5的日志记录是教务审计的关键remote_addr用于追溯异常IP步骤6的WebSocket是扩展点源码中notificationService已实现STOMP协议但默认未启用如需实时通知需在pom.xml中取消spring-websocket注释。3.2 冲突检测CourseServiceImpl.java中三重校验的SQL实现原理selectCourse()的核心校验逻辑在CourseServiceImpl.java的checkSelectionConflict()方法中它通过三条独立SQL完成原子化检查而非在Java层循环遍历// CourseServiceImpl.java private boolean checkSelectionConflict(String studentId, Long courseId) { // 【校验1时间冲突】查该学生已选课程中是否存在与courseId时间重叠的课 // 使用EXISTS比JOIN快且能短路一找到就停 String timeSql SELECT 1 FROM student_course sc JOIN course c ON sc.course_id c.id WHERE sc.student_id ? AND c.id ! ? AND c.start_week (SELECT end_week FROM course WHERE id ?) AND c.end_week (SELECT start_week FROM course WHERE id ?) AND c.day_of_week (SELECT day_of_week FROM course WHERE id ?) AND c.session (SELECT session FROM course WHERE id ?) ! 0; if (jdbcTemplate.queryForObject(timeSql, Integer.class, studentId, courseId, courseId, courseId, courseId, courseId) ! null) { return false; // 有冲突 } // 【校验2学分超限】查该生当前总学分 本课学分 年级上限 String creditSql SELECT IFNULL(SUM(c.credit),0) (SELECT credit FROM course WHERE id ?) FROM student_course sc JOIN course c ON sc.course_id c.id WHERE sc.student_id ?; Integer totalCredit jdbcTemplate.queryForObject(creditSql, Integer.class, courseId, studentId); Integer gradeLimit getCreditLimitByGrade(studentId); // 从student表查grade映射到limit if (totalCredit gradeLimit) { return false; } // 【校验3先修课程】查course表pre_requisite字段是否为空非空则检查学生是否已修 String preSql SELECT pre_requisite FROM course WHERE id ?; String preReq jdbcTemplate.queryForObject(preSql, String.class, courseId); if (StringUtils.isNotBlank(preReq)) { String[] preIds preReq.split(,); for (String preId : preIds) { String checkPreSql SELECT 1 FROM student_course WHERE student_id ? AND course_id ?; if (jdbcTemplate.queryForObject(checkPreSql, Integer.class, studentId, Long.parseLong(preId)) null) { return false; // 先修未完成 } } } return true; // 全部通过 }参数说明时间冲突SQL中c.session (SELECT session FROM ...) ! 0是位运算技巧session字段用二进制位表示节次如00000101表示第1、3节运算快速判断是否有交集学分SQL用IFNULL(SUM(),0)避免NULL导致比较失败先修课程用字符串分割而非关联表是为降低复杂度适合课程关系不频繁变更的场景。3.3 权限路由ShiroConfig.java中RBAC模型的极简落地系统采用Apache Shiro做权限控制配置在ShiroConfig.java中。它没用复杂的数据库权限表而是用RequiresPermissions注解内存角色映射兼顾安全与简洁// ShiroConfig.java Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean bean new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager); // 【权限映射表】key是URL patternvalue是权限字符串 MapString, String filterChainDefinitionMap new LinkedHashMap(); filterChainDefinitionMap.put(/static/**, anon); // 静态资源放行 filterChainDefinitionMap.put(/login.jsp, anon); filterChainDefinitionMap.put(/login, anon); filterChainDefinitionMap.put(/logout, logout); // 【角色权限】学生只能访问选课相关教师可管理课程管理员全通 filterChainDefinitionMap.put(/student/**, authc,roles[student]); filterChainDefinitionMap.put(/teacher/**, authc,roles[teacher]); filterChainDefinitionMap.put(/admin/**, authc,roles[admin]); // 【细粒度控制】即使同是teacher编辑课程需额外权限 filterChainDefinitionMap.put(/teacher/course/edit, authc,perms[course:edit]); filterChainDefinitionMap.put(/teacher/course/delete, authc,perms[course:delete]); bean.setFilterChainDefinitionMap(filterChainDefinitionMap); return bean; } // 【角色-权限绑定】在Realm中硬编码生产环境应从DB加载 Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo info new SimpleAuthorizationInfo(); String username (String) principals.getPrimaryPrincipal(); // 根据用户名查角色模拟DB查询 if (student001.equals(username)) { info.addRole(student); info.addStringPermission(course:select); } else if (teacher001.equals(username)) { info.addRole(teacher); info.addStringPermission(course:manage); info.addStringPermission(course:edit); } else if (admin.equals(username)) { info.addRole(admin); info.addStringPermission(all); } return info; }逻辑说明roles[student]是粗粒度控制确保学生无法访问/teacher/路径perms[course:edit]是细粒度控制即使伪造URL进入页面点击“编辑”按钮也会被Shiro拦截角色权限在doGetAuthorizationInfo()中硬编码是为了降低毕业设计复杂度如需对接真实DB只需修改此处SQL查询即可。4. 避坑指南五个让90%新手卡住的致命细节与血泪解决方案4.1 现象启动Tomcat后首页空白浏览器F12看到GET /static/css/app.css net::ERR_ABORTED 404原因web.xml中未配置CharacterEncodingFilter或JSP中静态资源路径未用%request.getContextPath()%拼接导致Tomcat将/static/识别为Servlet路径而非静态资源目录。解决确认web.xml中CharacterEncodingFilter的url-pattern为/*检查所有JSP文件将link href/static/css/app.css改为link href%request.getContextPath()%/static/css/app.css清理Tomcat工作目录work/Catalina/localhost/ROOT/重启服务。4.2 现象登录成功后跳转到/student/index.jsp但页面显示“欢迎null”原因StudentController.java中getSession().getAttribute(student)取不到值因为Shiro的Subject未正确存入Session。解决检查ShiroConfig.java中securityManager的rememberMeManager是否配置了CookieRememberMeManager源码中已注释掉需取消注释在login.jsp的登录表单中添加input typecheckbox namerememberMe/记住我确保ShiroFilterFactoryBean的loginUrl指向/login.jsp而非/login。4.3 现象学生选课时提示“选课成功”但student_course表无记录course_selection_log里status0原因CourseServiceImpl.java中selectCourse()方法未加Transactional或事务管理器未扫描到该Service类。解决检查CourseServiceImpl类上是否有Service注解确认applicationContext.xml中context:component-scan的base-package包含com.course.service在selectCourse()方法上显式添加Transactional(rollbackFor Exception.class)不要依赖类级别注解。4.4 现象MySQL执行init_db.sql报错ERROR 1071 (42000): Specified key was too long原因MySQL 5.7默认innodb_large_prefixOFF而student.name等字段用utf8mb4编码索引长度超767字节。解决登录MySQL执行SET GLOBAL innodb_file_format Barracuda;执行SET GLOBAL innodb_file_per_table ON;执行SET GLOBAL innodb_large_prefix ON;重启MySQL服务再执行SQL。4.5 现象JMeter压测时course_selection_log表写入缓慢TPS卡在30以下原因course_selection_log表未按created_at分区或HikariCP连接池maxPoolSize过小。解决确认init_db.sql中分区语句已执行用SHOW CREATE TABLE course_selection_log;查看是否含PARTITION BY RANGE将applicationContext.xml中maximumPoolSize从默认20改为50在my.cnf中增加innodb_buffer_pool_size 1G根据服务器内存调整重启MySQL。5. 论文写作与答辩实战如何把代码里的“if-else”变成答辩PPT里的技术亮点5.1 论文第三章“系统设计”用UML图说清三层架构的耦合与解耦毕业论文第三章常被写成文字堆砌但评审老师最想看到的是“你理解了架构”。这里教你把CourseServiceImpl.java里的代码转化成有说服力的UML图层级关键类/接口职责与其它层的依赖表现层CourseController接收HTTP请求调用Service返回JSON依赖CourseService接口不依赖其实现类业务层CourseServiceImpl实现选课核心逻辑冲突校验、事务控制依赖StudentMapper、CourseMapper接口不依赖MyBatis具体实现持久层StudentMapper.xml定义SQL语句与数据库交互仅被CourseServiceImpl调用不反向依赖业务层提示画类图时在CourseServiceImpl与StudentMapper之间标注uses在CourseController与CourseServiceImpl之间标注depends on。这种标注比画箭头更能体现设计思想。5.2 答辩高频问题预演从“为什么用SSM”到“怎么证明它可靠”答辩时老师必问“为什么不用Spring Boot”、“并发怎么保证”、“数据一致性怎么验证”。答案必须来自你的代码而非百度Q为什么用SSM而不是Spring BootA“因为本系统需深度定制MyBatis的SQL如3.2节的位运算时间冲突检测SSM的XML配置更直观可控且学校实验环境要求部署到Tomcat 7而Spring Boot 2.x最低支持Tomcat 8.5。”Q200人同时抢课怎么保证不超选A“看CourseServiceImpl.java第87行selectCourse()方法加了Transactional且course_selection_log表的status字段在插入时设为0待处理只有校验全部通过才更新为1。我用JMeter写了脚本200线程下SELECT COUNT(*) FROM student_course WHERE course_id1001始终≤最大容量。”Q怎么验证数据没丢A“看database/test_data.sql里面有10条预置选课记录。我写了VerifyDataIntegrityTest.java单元测试它会1查student_course总数2查course_selection_log中status1的数量3查course表selected_count字段。三者必须相等否则断言失败。”5.3 性能优化报告把applicationContext.xml里的数字变成答辩加分项论文第四章“系统实现”常缺量化数据。直接抄源码里的配置就是最硬的证据优化项配置位置优化前优化后提升效果验证方式连接池大小applicationContext.xmlmaxPoolSize20maxPoolSize50TPS从28→47JMeter 200线程响应时间P95从1200ms→650ms日志分区init_db.sql无分区按年份分区SELECT COUNT(*)耗时从8.2s→0.3sEXPLAIN SELECT COUNT(*) FROM course_selection_log PARTITION(p2024);事务超时applicationContext.xml默认30秒显式设timeout30失败请求平均回滚时间缩短40%SHOW ENGINE INNODB STATUS\G查TRANSACTIONS部分从那以后我每次改完配置都强制走一遍JMeter最小化脚本10线程×10次请求用curl -s http://localhost:8080/student/select?studentIdstudent001courseId1001 | jq .code验证返回码。不是为了炫技是怕哪天Transactional注解手抖删了自己还蒙在鼓里。希望帮到你。本文还有配套的精品资源点击获取
返回列表