ARTICLE DETAIL

资讯详情

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

从CRUD到并发控制:Spring Boot选课系统毕设完整复盘

从CRUD到并发控制:Spring Boot选课系统毕设完整复盘 每年毕设季Java方向最不缺的就是“基于Spring Boot的大学生选课系统”这类题目。我当年选它说实话一开始的想法很简单Spring Boot熟、业务逻辑直观、网上参考资料多。但真正把系统写完、论文改完、答辩通过之后我才意识到这个题目远没有想象中那么“模板化”——选课系统的核心难点不在CRUD而在并发、约束、数据一致性这些看不见的地方。这篇文章我就以过来人的身份把整个系统的技术选型、数据库建模、并发处理、论文撰写和答辩要点完整复盘一遍给正在做类似题目的同学一条可以直接参考的路线。1. 为什么“简单”的选课系统能成为高频毕业设计选题很多人在选题时都会纠结选课系统是不是太普通了网上代码一抓一大把怎么做出差异化我的看法恰恰相反——选课系统的高频出现正是因为它覆盖了软件工程实践中足够多的重要命题多角色权限、复杂业务规则、高并发写操作、数据一致性、前后端交互。把这些点做扎实论文和答辩都有东西可讲。1.1 选课业务里的核心矛盾不是“写不写得出”而是“正确性”如果只是实现“学生选课、教师录入成绩、管理员管理用户”那这个系统确实简单两三天就能写完。但真实选课场景里有几个让人头疼的约束课程有容量上限比如某门课只能选30人第31个人必须被拒绝。学生同一时间段不能选两门课这里的时间冲突判断要精确到“星期几第几节”。学生有每学期学分上限防止有人把所有课都选走。选课窗口期内可能上千人同时点选课按钮系统不能被拖垮数据也不能出错。这些约束叠在一起才让选课系统从“管理系统”变成了“业务系统”。毕业论文里真正值得写的不是登录注册和增删改查而是你如何设计表结构、如何用事务和锁保证这些规则在并发下依然成立。1.2 从热搜词看毕业设计和市场需求的错位我写这篇复盘之前扫了一圈最近的热搜关键词很有意思springboot版本太高、maven项目构建方法、vue打包放进springboot、springboot配置、springboot默认使用cglib代理……这些全是Spring Boot开发里最真实的痛点也是毕设阶段最容易卡住的地方。这说明什么问题说明大家遇到的坑高度一致但网上能把这个链条讲完整的文章很少。比如“Spring Boot版本太高”这个搜索词背后是Spring Boot 3.x把javax迁移到了jakarta包名老教程的代码全编译不过“Vue打包放进Spring Boot”则涉及前端构建产物与后端静态资源目录的整合问题。这些内容我后面会专门展开讲。毕业设计和市场需求之间的错位就在于课本教的是SSH、Servlet基础面试和实操考的是Spring Boot全家桶和工程化能力而毕设正好是补上这个差距的好机会。2. 技术选型复盘Spring Boot 3.2 MyBatis Plus Vue3 的取舍选型这件事很多同学是随便定的但这部分恰恰是论文第一章“技术介绍”里必须自圆其说的核心。我的最终方案是后端Spring Boot 3.2.4 MyBatis Plus 3.5.5前端Vue3 Element Plus数据库MySQL 8.0构建工具MavenIDE用的IntelliJ IDEA。下面讲清楚每个选择背后的逻辑。2.1 为什么是Spring Boot而不是SSH、SSM手写早几年的毕设还是SSM为主流——Spring Spring MVC MyBatis要写一堆XML配置、一堆web.xml配置。Spring Boot最大的价值在于自动配置和约定优于配置内嵌Tomcat一个jar包就能跑起来。对毕设来说这意味着你可以把精力放在业务逻辑而不是环境搭建上对论文来说Spring Boot本身就是当前企业级开发的主流框架写在简历上、答辩论据上都不虚。但这里要提醒一句Spring Boot版本不是越高越好。我见过太多同学直接选了当时最新的Spring Boot 3.3或3.4结果发现网上很多教程和依赖版本都对不上报错一堆。Spring Boot 3.x对Java版本有硬性要求必须在Java 17及以上同时包名从javax.xxx变成了jakarta.xxx。如果是第一次做毕设、没有时间踩坑我更建议用Spring Boot 2.7 Java 8这个组合稳兼容性好如果想顺便练一下新技术那用3.2.x正合适。我当时选3.2.4是因为它兼顾了Spring Boot 3的新特性和生态的稳定性。2.2 ORM选型JPA vs MyBatis Plus数据访问层我最终选了MyBatis Plus而不是Spring Data JPA。理由很实际选课系统的查询逻辑复杂比如“查询学生课表需要连三张表”“查询课程余量需要聚合统计”MyBatis的SQL可控性更强写复杂查询心里有底。MyBatis Plus提供了BaseMapper单表CRUD几乎不用写SQL开发效率比原生MyBatis高很多对毕设来说非常友好。论文答辩时你可以很明确地说出MyBatis Plus的优缺点优点是开发效率高、内置分页插件、代码生成器缺点是在超复杂动态SQL上需要自己控制。JPA的优势是自动建表和对象关系映射更“自动”但对新手来说实体类和表结构的隐式映射一旦出问题排查成本很高。对选课系统这种业务规则明确的场景SQL显式表达反而更稳妥。2.3 前后端分离部署Vue构建产物如何打进Spring Boot jar这是热搜词里最真实的一个坑。很多同学学了Vue就习惯前后端分离开发——前端用npm跑在8080端口后端Spring Boot跑在8081端口——开发没问题但毕设演示时你不能让老师帮你启动两个终端最好就是一个java -jar把整个系统跑起来。解决方案是把Vue项目构建后的dist目录放进Spring Boot的src/main/resources/static目录。具体做法是# 进入前端项目目录执行构建 npm install npm run build构建完成后dist目录下会产生index.html和static子目录。把这整个dist里的内容复制到Spring Boot的src/main/resources/static目录下重新打包后端项目访问http://localhost:8080/直接就是前端页面再也不用开两个服务。这里有个隐藏问题Vue Router如果用history模式刷新页面时会出现404因为Spring Boot默认找不到前端的路由路径。解决办法是实现一个WebMvcConfigurer配置类把找不到的路径转发到index.htmlConfiguration public class WebMvcConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/).setViewName(forward:/index.html); } }如果前端路由层级很深更稳妥的方案是实现一个ErrorViewResolver拦截所有非/api开头的路径统一forward到index.html。我的经验是API接口路径统一加上/api前缀这样静态资源转发和接口请求互不干扰这个习惯能省掉你后面大量排错时间。3. 数据库建模与业务规则把选课约束落到表结构上数据库设计是论文里的重头戏也是评判你“是否认真做系统”的重要依据。我见过不少同学只建了user和course两张表选课记录用一个中间表连课程的上课时间都直接写死在一个字段里。这种设计写出来的论文外审一眼就能看出问题。3.1 核心表设计从“用户”拆出多角色选课系统至少包含学生、教师、管理员三种角色。很多模板代码会设计一张大user表用role字段区分身份。我建议拆表设计因为拆表不仅能清晰表达业务模型后续扩展权限和数据隔离也方便。我最终的核心表结构是这样表名主要字段说明studentid, student_no, name, class_name, major, password学生信息teacherid, teacher_no, name, department, title, password教师信息adminid, username, password管理员账号courseid, course_name, credit, capacity, selected_count, teacher_id, status课程基本信息course_scheduleid, course_id, week_day, start_section, end_section, weeks, classroom课程排课时间student_courseid, student_id, course_id, select_time, status选课记录注意course表和course_schedule表是分开的。为什么因为一门课可能每周上多个时段如果只有一个time字段时间冲突检测根本没法做。我当时设计的是week_day星期几1-7start_section和end_section第几节到第几节weeks字段存“1-16周”或“1-8,10-16周”这种字符串用来支持单双周。3.2 唯一约束与冗余字段我最想提醒新人的点数据库里有两个设计点是直接决定系统正确性的第一student_course表必须加联合唯一约束。同一个学生对同一门课只能选一次这个约束应该在数据库层面、而不是只在代码里判断ALTER TABLE student_course ADD UNIQUE KEY uk_student_course (student_id, course_id);不加这个约束的话代码里先查询再插入会出现竞态条件——两个请求同时查到“未选过”然后同时插入成功数据就错了。第二course表里的selected_count字段是一个典型的冗余字段。为什么不每次都去count选课记录表因为选课高峰期count操作非常频繁每次还要关联查询性能差。维护一个selected_count字段选课成功时加1退课时减1查询余量时直接读出来。但这带来了一个新问题selected_count和student_course里的真实记录数可能不一致。解决办法是在同一个事务里更新并且在论文测试部分专门做一致性验证。3.3 业务规则的表结构表达选课规则也需要在表层面或服务层面有清晰设计。我的做法是增加一张course_selection_window表用来配置选课时间窗口字段含义id主键semester学期比如2024-2025-1start_time选课开始时间end_time选课结束时间credit_limit本学期学分上限选课接口被调用时第一步先检查当前时间是否在窗口内第二步判断学分是否超限。这些规则如果写死在代码里后面改规则就要改代码放在表里管理员在页面上就能配置这才是“可配置系统”该有的样子。论文的数据库设计章节也能多写两页内容。4. 并发选课的“那个瞬间”锁、事务与超卖问题的处理接下来是整篇博客的重头戏——并发。选课系统看起来只是insert一条记录但一旦进入高峰期问题就来了。课程容量只有30人但第31个、第32个……甚至第100个并发请求同时进来了最后实际入选了35个人这就是典型的“超卖”问题。我课设时真遇到过整个班的人同时选课容量10人的课选进去了15人老师当场脸色就变了。4.1 为什么“先查询再插入”会失效很多人写的第一版逻辑都是这样的伪代码// 错误的写法先查询再判断再插入 if (course.getSelectedCount() course.getCapacity()) { throw new BizException(课程已满); } // 执行insert studentCourseMapper.insert(...); // 更新selected_count courseMapper.incrementSelectedCount(courseId);问题出在哪两个线程A和B同时读到了selectedCount29都判断3029为false也就是还没满然后都执行了插入。你以为数据库在帮忙排队实际上我们什么都没加数据库看到的只是两条独立的insert。这就是经典的时间差导致的数据不一致。4.2 悲观锁方案SELECT ... FOR UPDATE我最终采用的是悲观锁方案核心思路是在同一事务中先锁定课程行再操作选课记录。Transactional(rollbackFor Exception.class) public void selectCourse(Long studentId, Long courseId) { // 1. 检查选课窗口 CourseSelectionWindow window windowService.getCurrentWindow(); if (window null || !window.isInWindow()) { throw new BizException(当前不在选课时间内); } // 2. 悲观锁锁定课程行其他事务必须等这里释放才能继续操作 Course course courseMapper.selectByCourseIdForUpdate(courseId); if (course null) { throw new BizException(课程不存在); } if (course.getStatus() ! 1) { throw new BizException(课程不在可选状态); } // 3. 判断容量 if (course.getSelectedCount() course.getCapacity()) { throw new BizException(课程已满); } // 4. 检查时间冲突 ListCourseSchedule schedules scheduleMapper.selectByCourseId(courseId); ListCourse myCourses courseMapper.selectSelectedCoursesByStudentId(studentId); for (CourseSchedule schedule : schedules) { for (Course myCourse : myCourses) { ListCourseSchedule mySchedules scheduleMapper.selectByCourseId(myCourse.getId()); if (hasConflict(schedule, mySchedules)) { throw new BizException(上课时间与课程[ myCourse.getCourseName() ]冲突); } } } // 5. 插入选课记录 StudentCourse sc new StudentCourse(); sc.setStudentId(studentId); sc.setCourseId(courseId); sc.setSelectTime(new Date()); studentCourseMapper.insert(sc); // 6. 更新课程已选人数 courseMapper.incrementSelectedCount(courseId); }这条SQL很关键SELECT * FROM course WHERE id #{courseId} FOR UPDATE;FOR UPDATE是MySQL里标准的悲观锁写法。事务A锁住了course表里这一行之后事务B再来执行同样的SELECT FOR UPDATE就只能阻塞等待直到事务A提交或回滚。这样“判断容量”和“插入记录”就变成原子操作了。4.3 悲观锁和乐观锁怎么选在选课系统里我推荐用悲观锁而不是乐观锁理由是选课场景是“写多读少”一旦进入选课窗口选同一门课的学生非常多。乐观锁是用版本号或条件更新来判断冲突冲突了再重试在高竞争场景下“重试”次数会很多反而慢。悲观锁虽然会让同一门课的请求串行化但一门课最多也就几十个容量串行处理几十个请求也就是毫秒级的事。不过悲观锁也有限制SELECT FOR UPDATE必须走主键或索引如果锁的是整张表就麻烦了。所以我的锁粒度始终是“课程ID”一次只锁一门课。另外事务里千万不要做远程调用、不要写日志文件、不要做任何耗时的IO操作锁的持有时间越短越好。曾经有同学因为事务里调了一个短信发送接口导致锁长达几秒不释放整个选课接口直接超时。4.4 从学生端角度看数据一致性选课完成后学生端课表里应该立刻出现这门课。数据流向是选课接口成功 → student_course插入记录 → course表selected_count加1 → 前端刷新课表。这里我用的是同步逻辑不走消息队列保证数据即时可见。如果为了论文扩展可以提一个“高并发下引入Redis预扣减库存”的优化方案但实际项目里我选择不过度设计——毕设最重要的是跑通、正确、可演示。时间冲突检测的核心是一个函数判断两个时间段是否重叠。把星期几放在第一位节次按startSection和endSection判断即可private boolean hasConflict(CourseSchedule s1, CourseSchedule s2) { if (!s1.getWeekDay().equals(s2.getWeekDay())) { return false; } // 判断时间段是否重叠 return s1.getStartSection() s2.getEndSection() s2.getStartSection() s1.getEndSection(); }再加上周次范围的重叠判断比如单周和双周三分钟就能写完。但没写这段代码的设计选课系统就是残缺的答辩时基本必问。5. 写论文和答辩时要重点展示的几张图与几个数据系统写完只算完成了一半论文才是毕业设计的另一半。很多同学程序写得很溜论文却写成流水账——第一章讲Java历史第二章讲Spring Boot是什么第三章讲数据库是什么……答辩老师一眼看穿是凑字数。这里我讲讲写论文时真正值得花篇幅的内容。5.1 系统架构图和用例图怎么画才符合答辩组习惯论文里最核心的图有这几张系统架构图分四层画——表现层Vue页面、控制层Spring MVC Controller、业务层Service、数据访问层MyBatis Plus Mapper再加底层MySQL。功能用例图图中心放“选课系统”外围放学生、教师、管理员三个角色学生能做的操作浏览课程、选课、退课、查看课表、查成绩、教师能做的开课申请、录入成绩、管理员能做的用户管理、课程审核、选课窗口配置分别连线。数据库ER图和核心表关系图一定要把student_course和course_schedule这两张中间关联表画出来这个图能直接体现你对“多对多关系”的理解。时序图选“学生选课”这个核心流程画一张时序图从Controller到Service到Mapper到数据库每个步骤标注清楚答辩时照着这张图讲不会慌。画图工具有很多我建议用Draw.io画完导出矢量图插到Word里清晰度高答辩PPT也可以直接用。别用网上截来的模糊图片答辩老师非常反感。5.2 性能测试怎么测、怎么解释论文通常需要一节“系统测试”大多数人都只会写功能测试比如“登录用例通过”“选课用例通过”。这当然可以但是加分项在于性能测试。选课系统的核心是并发那你就要用自己的数据说话。我当时的做法是用JMeter模拟并发选课设置线程数为100并发对容量只有30的课程发起选课请求。并发线程数总请求数成功选课数失败数平均响应时间(ms)是否超卖101010045否5050302092否1001003070156否20020030100310否这个表格一放出来答辩老师的第一个反应就是“这个学生是真做了压测的”因为你用数据证明了锁机制的有效性——并发100个请求最终依然只有30个成功没有一条数据越界。这比你在文字里吹“系统能抗高并发”有力得多。还要注意一点压测的预期结果应该是“成功数等于课程容量”而不应该是“100个全部成功”。如果100个请求全部成功说明容量判断形同虚设这才是最大的bug。5.3 创新点怎么找别把登录注册和换肤当成创新点我见过很多论文把“个性化推荐”“智能排课算法”写进创新点实际上代码里根本没有这些功能答辩一追问就露馅。创新点要小而真实哪怕用朴素的语言写清楚也比虚标强。我的建议是把这几个点写进论文的创新与特色基于数据库行级锁的选课并发控制有效解决课程超选问题并给出了压测数据验证。课程排课时间段与周次的精细建模精确到节次级别的时间冲突检测。前后端分离、单包部署的工程实践Vue构建产物直接内嵌到Spring Boot jar包方便部署演示。这三个点每一个都在论文里能找到对应的设计图、代码段和测试结果这才是“创新点”该有的样子。关于答辩我再提醒两点。第一准备好“选课高峰期”这个压力场景的讲法从锁机制讲到事务边界讲清楚你的系统是如何保证正确性的这基本能占据答辩一半的亮点。第二准备好几个边界问题退课时容量怎么恢复课程被删除时选课记录怎么办同时选的课程时间冲突怎么办这些问题其实代码里都有处理但你能不能当场讲清楚决定了老师对你的最终印象。我做完整套系统之后最大的感受是选课系统不是一个“写出来就行”的毕设它是一个可以真实上线、接受并发冲击的业务系统。把它作为毕业设计其实是在提前预习工作里会遇到的数据库并发、分布式锁、前后端工程化这些真问题。顺着这条路线把系统做完、把细节讲透论文和答辩都不会差。
返回列表