尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

从数据库设计到事务并发:学生选课系统实战指南

发布时间:2026/9/26 1:41:57

资讯中心
01
ARTICLE

从数据库设计到事务并发:学生选课系统实战指南

从数据库设计到事务并发:学生选课系统实战指南
简介学生网上选课系统的设计与实现资料包面向高校计算机相关专业学生及Java初学者解决传统选课信息管理效率低、易出错的问题。系统涵盖教室管理、老师管理、课程管理、教学计划管理、选课管理、成绩管理、学生管理等核心模块基于Spring Boot框架与MySQL数据库开发前端采用Vue技术栈。压缩包共402个文件包含Java源码、Vue页面组件、SQL数据库脚本及设计文档另有SVG图标、PNG图片等资源整体大小13.32MB结构清晰便于按模块检索。已有88人学习下载适合用于毕业设计、课程设计或项目实训。通过该资料可快速理解学生选课系统的前后端实现思路掌握Spring Boot与Vue整合开发流程同时获取可直接运行或二次开发的完整工程及配套数据库脚本与说明文档有助于完成设计报告和系统演示。1. 学生网上选课系统是什么排课冲突、并发抢课与三类用户的管理闭环一门限选 20 人的《软件工程导论》凌晨 0 点开放选课30 个学生同时点鼠标后台如果只做“先查人数再插入”结果多半是选课记录出现 25 条管理员对着数据库说不出哪 5 条是超录的。学生网上选课系统要解决的正是这个把选课从教务 Excel 表搬到网页上用数据库事务把“校验名额、写入选课记录、扣减名额”三步绑成一个整体再让管理员和教师分别拿到发布课程、查看名单的入口。这个课题是数据库课程设计和毕业设计里出现频率最高的一类交付物通常就是“代码 数据库脚本 LW 说明文档”。理解它的核心表结构和事务边界之后你完全可以把它改造成讲座预约、活动报名、会议室预订这类同样带“名额限制”的在线申请系统。2. 从数据库设计说起核心表结构、外键取舍与初始化 SQL很多同学拿到这个题目第一反应是找后端框架我反而建议先把表结构定下来。选课系统的业务复杂度不高但数据关系是典型的多对多一个学生可以选多门课一门课可以被多个学生选。如果表结构没设计好后面写再多接口也会在“人数超限”“重复选课”“退课后名单对不上”这些地方反复返工。更关键的是LW 文档里数据库设计占的分值很高你把 ER 图和数据字典画清楚整个项目的可信度就立住了一半。2.1 三种角色的用例拆解学生选课、教师确认、管理员排课我先做用例拆分而不是直接建表。系统里至少有三类人学生、教师、管理员他们的核心诉求完全不一样。学生端的主要动作是浏览可选课程、选课、退课、查看个人已选列表。这里的核心约束是“不能重复选同一门课”和“课程名额不能超”。教师端的诉求是维护自己授课的课程信息、查看选了这门课的学生名单偶尔还要手动调一下容量。管理员端的诉求是维护用户账号、审核教师发布的课程、设定选课时间段。很多做课程设计的同学把管理员和学生权限混在一起导致后面前端按钮一会儿显示一会儿隐藏逻辑越写越乱。正确做法是从数据库层面就把角色字段固定下来前面提的“用户表加 role 字段”就是这个目的它也是后端拦截器和前端菜单渲染的统一依据。在画 ER 图的时候用三个实体就够了用户、课程、选课记录。选课记录就是学生和课程之间那个多对多关系的“中间表”。需要注意的是选课记录不是简单的关联表它还要承载选课时间、选课状态这类业务字段所以它本身就是一张业务表不是纯粹为了拆多对多硬凑出来的。2.2 核心表设计用户表、课程表、选课记录表的字段清单我按最常跑的 MySQL 版本给你一套能直接建表、能直接做增删改查的 SQL。用户表里把学生、教师、管理员统一成一张表用 role 区分这样登录校验只需要写一次。课程表里用一个 teacher_id 指向用户表再用一个 selected_count 字段记录已选人数这个字段看起来冗余但它就是后面防超卖的关键。CREATE DATABASE IF NOT EXISTS course_select DEFAULT CHARACTER SET utf8mb4; USE course_select; -- 用户表学生、教师、管理员统一存储 CREATE TABLE user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(32) NOT NULL, password VARCHAR(128) NOT NULL, role TINYINT NOT NULL COMMENT 1学生,2教师,3管理员, real_name VARCHAR(32) NOT NULL, major VARCHAR(64) DEFAULT NULL COMMENT 学生专业教师可为空, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 课程表 CREATE TABLE course ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, course_name VARCHAR(64) NOT NULL, teacher_id INT UNSIGNED NOT NULL, credit DECIMAL(2,1) NOT NULL DEFAULT 2.0, capacity INT NOT NULL COMMENT 总容量, selected_count INT NOT NULL DEFAULT 0 COMMENT 已选人数, class_time VARCHAR(32) DEFAULT NULL COMMENT 上课时间用于冲突检测, status TINYINT NOT NULL DEFAULT 1 COMMENT 1可选,0关闭, PRIMARY KEY (id), KEY idx_teacher (teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表; -- 选课记录表 CREATE TABLE course_selection ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, student_id INT UNSIGNED NOT NULL, course_id INT UNSIGNED NOT NULL, select_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, status TINYINT NOT NULL DEFAULT 1 COMMENT 1有效,0已退, PRIMARY KEY (id), UNIQUE KEY uk_student_course (student_id, course_id), KEY idx_course (course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT选课记录表;这条建表语句里有三个细节你要看清楚。第一user 表的主键是 id但 username 上加了唯一索引这保证账号不允许重复。第二course_selection 表除了自增主键 id还加了一个联合唯一索引 uk_student_course它的含义是“同一个学生不能对同一门课产生两条有效记录”这是防重复选课的数据库兜底。第三course 表里 capacity 和 selected_count 都是 INT后续更新已选人数时用selected_count selected_count 1这种原子操作而不是先查出来再写回去。2.3 选课表用复合主键还是自增主键防重复与分页的平衡这里很容易被老师追问也是 LW 答辩时的高频问题选课记录表用(student_id, course_id)做复合主键不行吗为什么还要加一个自增 id复合主键在理论上完全成立它天然保证了同一个学生选同一门课只存在一条记录逻辑上更简洁。但在实际开发里自增主键加唯一索引的做法更保险原因有三点。第一选课记录表还会有退课、统计、分页等操作Java 侧用 MyBatis 之类的 ORM 操作单条记录时习惯上按主键 id 定位复合主键要写两列条件容易漏条件把别的学生记录误删。第二如果后续需要把“一条选课记录”对应“一条成绩记录”自增 id 可以直接作为外键传给成绩表复合主键做不到这一点。第三联合唯一索引已经把重复选课的问题解决了数据库层面并没有因此失去约束力。所以我的建议是保留自增主键同时保留(student_id, course_id)唯一索引。这样既拿到了复合主键的防重复能力又保留了单主键在代码和分页上的便利。这其实就是设计模式里“单一职责”的一种体现主键只管定位行唯一约束管业务规则。2.4 初始化 SQL 脚本课程数据、账号数据与“跑通全流程”的最小集表建完必须有一批能直接登录的数据不然项目给出去别人跑不起来。我一般会准备一套“最小可跑通集合”一个学生、一个老师、一个管理员、两门课其中一门课容量故意设成 1用来测超员场景。密码在演示项目里直接写 MD5 值方便在文档里说明实际项目必须换加盐哈希。INSERT INTO user (username, password, role, real_name, major) VALUES (student01, MD5(123456), 1, 张同学, 软件工程), (teacher01, MD5(123456), 2, 李老师, 软件工程), (admin, MD5(admin123), 3, 教务管理员, NULL); INSERT INTO course (course_name, teacher_id, credit, capacity, selected_count, class_time, status) VALUES (软件工程导论, 2, 2.0, 20, 0, 周一 3-4节, 1), (数据库原理, 2, 3.0, 30, 0, 周三 1-2节, 1); -- 往选课记录表里插一条数据方便测试“已选课学生”的重复选课拦截 INSERT INTO course_selection (student_id, course_id, status) VALUES (1, 1, 1);这段初始化脚本里student01 已经选了软件工程导论所以你在写完选课接口后拿这个账号再去选同一门课就能立刻验证唯一索引是否生效。数据库导出给别人的时候要把建表语句、初始化数据、以及后面要讲的存储过程或定时任务脚本分文件存放LW 文档里按“数据字典”把它们逐字段解释一遍这个项目的完整度立刻上一个档次。3. 后端代码实现选课事务、行锁与名额释放数据库表结构定好之后后端最核心的工作就是写选课接口。这个接口看起来只是 INSERT 一条记录但它在并发下踩的坑最多。我在这一章会直接把选课、退课、定时释放名额三个接口写给你看重点讲清楚事务边界和行锁怎么用。很多课程设计的项目能单机跑通一部署到有人同时访问就挂问题都出在这个接口上。3.1 技术选型Spring Boot MyBatis 与 JSP/Servlet 的取舍技术栈选择直接影响你做这个项目的速度和答辩时的说服力。我的判断是如果时间紧、目标是“能跑通、好答辩”直接选 Spring Boot MyBatis MySQL前端用原生 HTML 加一点 Ajax 就够了不要为了炫技引入分布式锁、消息队列那套东西。毕业设计的核心是讲清楚“业务逻辑怎么落进数据库”框架只是工具。如果是纯数据库课程设计有时候老师要求必须用 JSP Servlet 写那也没问题底层事务逻辑是一样的只是把 HTTP 处理从注解换成 Servlet。Spring Boot 的好处是集成默认配置省事内置的 HikariCP 连接池不用额外装。连接池这个参数在后端选课系统中极其关键默认连接池大小只有 10几十个学生同时抢课数据库连接瞬间不够用接口报错率直线上升。我会在后面的压测章节专门讲怎么调它。MyBatis 比 JPA 在这个场景更适合因为你要写SELECT ... FOR UPDATE这种更可控的 SQLMyBatis 的 XML 能直接写原生语句调试起来一目了然。项目结构上控制层只做参数校验业务逻辑放进 Service 层事务写在 Service 方法上。3.2 选课接口查人数、插记录、扣名额三步包进同一个事务选课接口最忌讳的写法是这样的先 SELECT 查一下selected_count判断小于 capacity再 INSERT 选课记录再 UPDATE course 表把人数加一。这中间任何一个环节并发进来都会出问题。正确做法是先把课程行锁住再操作我一般会这样写Service public class CourseSelectionService { Resource private CourseMapper courseMapper; Resource private CourseSelectionMapper selectionMapper; Transactional(rollbackFor Exception.class) public void selectCourse(Long studentId, Long courseId) { // 1. 锁住课程行并发请求在这里排队 Course course courseMapper.selectByIdForUpdate(courseId); if (course null) { throw new BizException(课程不存在); } if (course.getStatus() null || course.getStatus() ! 1) { throw new BizException(课程不在可选状态); } // 2. 在锁保护下做名额判断 if (course.getSelectedCount() course.getCapacity()) { throw new BizException(该课程人数已满); } // 3. 插入选课记录唯一索引兜底 try { selectionMapper.insertSelection(studentId, courseId); } catch (DuplicateKeyException e) { throw new BizException(你已经选过这门课了); } // 4. 已选人数加一 courseMapper.increaseSelectedCount(courseId); } }对应的 MyBatis 映射里关键是那条加锁查询select idselectByIdForUpdate resultTypecourse_select.entity.Course select id, course_name, capacity, selected_count, status from course where id #{courseId} for update /select这段代码能防超卖的核心在于SELECT ... FOR UPDATE。InnoDB 在事务里执行这语句后会对 course 表里这条 id 对应的行加排他锁第二个请求进来做同样查询时会一直等待直到第一个事务提交或回滚。也就是说同一时刻只有一个请求能通过名额校验后续请求拿到的都是更新后的 selected_count。配合事务注解一旦第 3 步插入失败第 4 步的更新也会回滚课程人数不会出现“记录没插进去名额却少了一个”的脏状态。有个细节要注意事务必须从调用进入 Service 方法时就开始所以你不能在 Controller 里先调一个查询再调这个 Service 方法那样锁的范围就不对了。自研的BizException是运行时异常才能触发 rollbackFor。还有一种简化写法是直接用UPDATE course SET selected_count selected_count 1 WHERE id ? AND capacity selected_count受影响行数为 0 就报满员这个思路也能用但我还是推荐 FOR UPDATE因为它的语义对 LW 文档更友好答辩时更容易解释。3.3 退课接口的幂等处理与定时释放名额退课比选课简单但一样有坑。退课的直观逻辑是 DELETE 选课记录再把课程的已选人数减一。这里要注意的是“幂等”和“删改一致”如果学生从来没选过这门课后端不能直接返回成功否则人数会被错误减成负数。Transactional(rollbackFor Exception.class) public void dropCourse(Long studentId, Long courseId) { // 只删除状态为有效的记录 int deleted selectionMapper.deleteActiveSelection(studentId, courseId); if (deleted 0) { throw new BizException(当前没有选课记录无法退课); } // 删除成功后再把人数减回去 courseMapper.decreaseSelectedCount(courseId); }这里有一个容易被忽略的点deleteActiveSelection的 SQL 条件必须是student_id ? AND course_id ? AND status 1不能只按课程 ID 删否则一个学生多条历史记录会被一起删掉。我建议给 course_selection 表加上status字段而不是物理删除这样你就可以保留学生大学四年完整的历史选课记录教师端查名单时也只要过滤status 1就行了。定时释放名额这个功能不是所有选课系统都需要但很多题目会写“课程名额按时释放”或“预选超时失效”。常见做法是在选课记录表加一个created_at字段用 Spring 的定时任务扫描“创建时间超过 30 分钟且状态还是预选”的记录把它们批量标记为失效同时回退课程人数。定时任务上要加EnableScheduling才能生效Component public class SelectionTimeoutTask { Resource private CourseSelectionMapper selectionMapper; Resource private CourseMapper courseMapper; // 每 5 分钟执行一次 Scheduled(cron 0 0/5 * * * ?) public void releaseExpiredSelection() { ListLong expiredIds selectionMapper.listExpiredSelectionIds(30); if (expiredIds.isEmpty()) { return; } int updated selectionMapper.batchUpdateStatus(expiredIds, 0); if (updated 0) { courseMapper.batchDecreaseSelectedCount(expiredIds); } } }这段逻辑注意两点。第一批量失效和批量扣减要放在同一个事务里或者至少保证扣减次数不超过失效记录数。第二定时任务和用户手动选课可能同时操作同一门课程所以批量扣减的 SQL 里也要带条件WHERE selected_count 0防止负数。你可以把这两层保护写进 LW 的详细设计里老师会觉得你对边界考虑得很全。3.4 容易踩的锁顺序问题两个选课请求同时进来的死锁隐患FOR UPDATE 用对了并发安全就有六成保障但要小心死锁。死锁的典型场景是事务 A 选了课程 1接着又要选课程 2事务 B 选了课程 2接着又要选课程 1。两个事务分别锁住了对方需要的课程行互相等待InnoDB 检测到死锁后会随机回滚一个事务接口层收到死锁异常用户看到“操作失败”。解决方式很简单在一个事务里如果操作多门课必须规定一个统一的加锁顺序。我习惯在 Service 入口对课程 ID 列表做排序按升序逐个加锁这样所有事务都以同样的顺序获取锁就不会出现循环等待。如果把“批量选课”做成一个后端接口这条路很好走先排序再循环调用选课逻辑。如果你只想一次选一门课死锁概率极低但教师端批量调课时要注意。另外要明白 FOR UPDATE 锁的是索引记录如果 WHERE 条件没走索引InnoDB 会把锁升级成表锁整个课程表的写操作全部串行。所以主键查询没问题但如果你用course_name去锁一定要确保那个字段有索引否则不能用它做并发控制的查询条件。4. 前端页面与交互从登录到个人课表的完整链路后端逻辑再完善前端页面烂一样拉低整个项目印象分。这个选课系统的前端不用做得花哨但页面之间的跳转关系必须完整登录页、可选课程列表页、个人课表页、教师端选课名单页。我见过很多课程设计的源码后端接口写了一大堆前端只有一个默认主页点开连“选课”按钮都没实现这种项目在答辩时基本是送分给老师挑刺。这一章我把最少能跑通的前端骨架和交互代码写出来并告诉你配套 LW 里页面流程要怎么组织。4.1 页面结构登录、选课列表、个人课表三个页面的最小实现如果是 Spring Boot 项目前端文件放在src/main/resources/static和templates目录。一个标准的最小结构是三个 HTML 页面加一个公共样式文件login.html、course_list.html、my_courses.html。教师端视图可以复用course_list.html只是按钮区域换成“查看名单”管理员视图则可以通过隐藏字段控制。登录页是最容易被人忽略的页面但很多课程设计都挂在登录校验上。我建议后端登录接口在成功之后把userId和role写进 Session同时设置 Session 超时时间。前端登录跳转用普通表单提交就可以不需要加 Ajax简化一下代码量。选课列表页的核心是展示课程 ID、名称、老师、已选人数、容量和操作按钮。我用原生 HTML 加 Thymeleaf 写一个最常用的循环关键信息都能在页面上看到后端把数据放进 Model 渲染即可。table tr th课程名称/thth教师/thth已选/容量/thth操作/th /tr tr th:eachc : ${courseList} td th:text${c.courseName}/td td th:text${c.teacherName}/td td th:text${c.selectedCount} / ${c.capacity}/td td button th:attrdata-course-id${c.id} th:onclickselectCourse(this) th:disabled${c.selectedCount c.capacity}选课/button /td /tr /table这里th:disabled让已经满员的课程按钮在页面加载时就置灰用户根本点不了。这是在 UI 层先做一层拦截但它防不了并发真正校验依然以第 3 章的后端接口为准。页面结构里还要有一个返回个人课表的入口方便学生选完课立即看到结果。4.2 用 Ajax 调选课接口按钮置灰、错误提示与列表刷新选课操作不能整页刷新否则用户选完还要重新翻列表体验很差。我一般把这些交互做成异步点击选课按钮前端对/api/select发送 POST 请求后端返回 JSON。前端拿到结果后根据 code 字段弹提示或者直接刷新当前页面。function selectCourse(btn) { const courseId btn.getAttribute(data-course-id); // 防止用户连续点击导致同一请求提交多次 btn.disabled true; fetch(/api/select, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ courseId: courseId }) }) .then(res res.json()) .then(data { if (data.code 0) { alert(选课成功); location.reload(); } else { alert(data.msg); // 选课失败要恢复按钮 btn.disabled false; } }) .catch(err { // 网络异常也要恢复按钮 btn.disabled false; alert(网络异常请重试); }); }这段代码有两个细节值得写进项目里。第一按钮一旦点击立即置灰这是看得见的防重复提交和数据库唯一索引形成前后端双保险面试官或老师问起来你可以很明确地说出这两层各自的作用。第二失败时要把按钮恢复否则用户没选上却再也点不了只能刷新页面体验很差。还有一个容易被忽略的问题fetch默认不带 Cookie如果你的选课系统依赖 Session 做登录校验需要在 fetch 里加credentials: same-origin否则后端取不到登录用户接口直接 401。很多同学做跨域前后端分离项目时在这里卡很久其实解决方案也就是这么一行参数。4.3 教师端与管理端名单展示和课程发布的不同权限教师端页面可以复用同一套后端模板只是接口返回的数据范围变了。教师登录后只能看到自己担任教师的课程这个过滤条件在 SQL 里通过teacher_id完成。每个课程列表项旁边放一个“学生名单”按钮点击后弹出一个表格列出选了这门课的学生姓名和学号。管理员端需要的东西反而更少一条用户列表、一个课程发布表单。课程发布表单的核心字段就是第 2 章 course 表的那几个字段课程名称、教师、学分、容量、上课时间。发布动作本质上就是 INSERT 一条 course 记录不需要复杂设计。管理员端还要有一个 user 管理入口一般就是普通增删改查把用户表的 account 和 role 展示出来。前端页面如果只有这几张表整个系统已经能串成一条完整业务线管理员建课、教师查看名单、学生选课。如果还有富余时间你可以再做一个“学生个人课表”按周几和时间段把已选课程排列成表格这个功能在 LW 里能作为亮点展示但技术上不需要引入额外框架。4.4 配套 LW 怎么写页面流程描述与系统架构图的组织方式这里说的 LW 对应的是交付清单里的说明文档很多学校叫“课程设计报告”或“毕业设计论文”。代码写完之后文档别直接照抄模板要按你自己的系统结构来写。我习惯在文档里安排这么几个大块引言、需求分析、系统设计、数据库设计、系统实现与测试。其中系统实现部分每写一个功能就配一张页面截图和一段核心代码代码不需要贴全部贴最关键的那几行并解释它解决什么问题。页面流程部分我建议画一张简单的调用链图用户在浏览器点击选课按钮浏览器发 Ajax 请求到 ControllerController 调 ServiceService 调 MapperMapper 操作数据库数据结果再逐层返回。这张图不需要专门的画图工具用编辑器的文本画出方框和箭头就够了重点是让老师看到你理解请求是怎么串起来的。数据库设计部分的组织方式也很固定放一张 ER 图然后给每个表做一个字段表格字段名、类型、约束、说明四列对齐。写到这里时把第 2 章那张建表 SQL 里的注释直接搬过去就能保证数据库字段和文档描述完全一致不会出现文档写“选课记录表没有唯一索引”代码里却建了唯一索引这种低级失误。5. 网上选课系统避坑指南并发超选、中文乱码与部署失败的排查这个系统做完之后真正让你改到怀疑人生的往往不是功能没实现而是这些隐蔽问题并发超选、中文乱码、请求 403、页面加载巨慢、部署后白屏。这一章我按“现象、原因、解决”给你写清楚每一条都是做课设的人血泪经验换来的。5.1 同一课程被选超员并发窗口让“先查再插”失效现象一门容量 20 的课选课结束后 course_selection 表里存在 21 条有效记录course 表里 selected_count 已经到 20但多出来的 1 个人还是选上了后台数据对不上。原因选课逻辑没有锁课程行。两个请求同时执行SELECT selected_count FROM course WHERE id 1都读到 19都判断小于 20然后都 INSERT最终 selected_count 只更新到 20但记录有两条漏网之鱼。解决用SELECT ... FOR UPDATE锁住课程行或把“人数校验 扣减”合并成一条原子 UPDATE比如UPDATE course SET selected_count selected_count 1 WHERE id 1 AND selected_count capacity受影响行数为 0 时返回“人数已满”。如果你用的是 MyBatis直接在第 3 章的 selectByIdForUpdate 上做代码改造即可插入前一定要保证锁已经拿到。5.2 中文课程名入库变成乱码Navicat 里看字段全是问号现象课程管理页填“操作系统”保存后页面回显变成“?????????”数据库表里看到的中文全是问号但数字和字母正常。原因数据库连接的 JDBC URL 少了字符集参数或数据库表本身是 latin1 字符集。很多同学在 Navicat 里建数据库时直接点“确定”默认字符集可能不是 utf8mb4而项目里的连接串又是老旧的写法两端字符集不一致就乱码。解决创建数据库时显式指定CHARACTER SET utf8mb4JDBC 连接串里加useUnicodetruecharacterEncodingutf8mb4。如果你已经建了表用ALTER TABLE course CONVERT TO CHARACTER SET utf8mb4转换一次再清掉乱码数据重新导入。这一步做完记得重启项目否则连接池里的旧连接还是老字符集。5.3 Ajax 请求返回 403登录状态失效与拦截器配置现象学生用浏览器登录系统后选课列表页能打开但点击“选课”按钮控制台打印 403刷新页面后又被强行跳回登录页。原因项目中配置了登录拦截器或 Spring SecurityPOST 请求被拦截器拦下。Spring Security 默认会开启 CSRF 防护而你的前端从页面加载到 Ajax POST 没带 CSRF Token于是 403。如果登录会话超时这个现象更隐蔽后端拦截器已失效但前端还停留在旧页面。解决开发环境可以直接关闭 CSRF但如果项目要求保留安全机制前端必须从后端获取 CSRF Token 并在请求头里带上。同时把拦截器的白名单配置好CSS、JS、登录接口、选课接口这些路径要能通过否则页面样式和异步请求都会被拦截。如果是前后端分离给接口加一个简单的 Token 头把用户 ID 一起传过去就不用依赖 Cookie Session 了。5.4 选课列表加载慢N1 查询把数据库拖到连接池耗尽现象单机测试没问题几十个人同时打开选课页面页面转圈好几秒数据库 CPU 打满日志里大量数据库连接超时异常。原因列表页循环里执行了查询比如每查一门课程又查一次教师姓名或者每查一条选课记录又查一次学生姓名。前者是 N1 查询后者是连接池被循环 SQL 占满。此时即使 HikariCP 连接池调到 50也只会拖垮数据库。解决用一条 JOIN 语句把课程和教师信息一次查出来例如SELECT c.*, u.real_name AS teacher_name FROM course c LEFT JOIN user u ON c.teacher_id u.id。在 Java 层不要再循环查 Mapper。另外给 course_selection 表的外键字段加好索引把慢查询日志打开看哪条 SQL 消耗时间最长。这属于优化里性价比最高的改动改完列表页耗时通常会降到 100ms 以内。5.5 本地跑通、部署到服务器后白屏端口与上下文路径的坑现象本地 Intellij 运行一切正常打成 jar 包丢到服务器上访问域名或 IP 显示 404页面白屏后端日志没报错。原因常见有三种原因。第一项目设置了server.servlet.context-path/select你直接访问/自然 404第二你把 MySQL 地址写成了localhost那台服务器上并没有数据库第三服务器端口被占用或防火墙没放行你访问的 8080 根本没连上应用。解决先在服务器上执行nohup java -jar course-select.jar log.out 21 用tail -f log.out看启动日志确认端口和数据库连接都正常。如果启用了 context-path访问 URL 必须加上前缀。数据库连接串要用服务器能访问到的地址不要用 localhostWindows 本地连接和 Linux 服务器连接要分开配置。最后确认netstat -tlnp | grep 8080能看到进程监听再排查 Nginx 或防火墙。6. 并发压测和效果验证给选课系统做一次上线前体检代码全部写完、页面能跑通这只是完成了 80%最后一步是验证并发的正确性。我见过太多项目单机演示很流畅老师现场让学生同时点两个按钮就露馅了。所以发布前我会用并发工具模拟“多人同时选同一门课”的场景用数据证明系统没有超卖。我用压测工具的方式是准备一门容量为 10 的课程然后用 Apache 自带的 ab 命令模拟 50 个并发请求同时选这门课。由于系统有登录校验先手动拿一个有效的 SessionId写进 ab 的请求头里让 50 个并发请求共用这个登录态。这样做可以过滤掉登录接口的性能干扰把测试目的锁定在选课并发上。ab -n 200 -c 50 -H Cookie: JSESSIONIDxxxxx \ -p select.json -T application/json \ http://localhost:8080/api/selectselect.json 的内容只有{courseId:1}。测试结束后看两个关键数字Failed requests 和 Time per request。如果 Failed requests 是 0再进数据库核对两边的数据SELECT COUNT(*) FROM course_selection WHERE course_id 1 AND status 1; SELECT capacity, selected_count FROM course WHERE id 1;这里的 count 必须等于 capacity 中的值selected_count 也必须等于 10才能说明并发控制真正生效。我通常还会把压测结果截图放进 LW 文档的测试章节作为“系统具备并发选课能力”的直接证据。如果压测结果发现 Failed requests 不低优先看项目日志里有没有死锁异常或连接池超时异常。死锁异常说明锁顺序有问题连接池超时则说明连接池上限太小。HikariCP 的默认配置在低并发场景很够用但选课场景可以把 maximumPoolSize 调到 20 到 30同时把 connection-timeout 设成 3000ms避免用户长时间卡住。这个项目做到最后我自己的体会是数据库设计和事务边界决定了一个选课系统能不能经得起同时点击前端页面决定的只是好不好看。每次发布新功能前我都会先跑一轮并发脚本验证数据一致性再把这个测试记录写进文档。做完这一步整个“代码 数据库 LW”交付物才算真正闭环。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。