1. 项目概述
作为一名在高校信息化建设领域深耕多年的开发者,我最近完成了一个基于Spring Boot的学生选课系统开发项目。这个系统从立项到上线历时4个月,期间经历了3次重大架构调整,最终形成了一套稳定可靠的解决方案。在高峰期测试时,系统成功承载了5000+并发选课请求,平均响应时间控制在800ms以内。
传统选课系统普遍存在几个痛点:
- 选课高峰期系统崩溃(我见过最夸张的是某校系统在开放选课后2分钟瘫痪)
- 课程冲突检测不智能
- 缺乏实时数据可视化
- 移动端适配差
我们这次开发的系统针对这些痛点做了针对性优化:
- 采用Redis缓存热门课程数据
- 实现基于时间片的冲突检测算法
- 集成ECharts实现实时数据展示
- 响应式设计适配多终端
2. 技术架构设计
2.1 整体技术栈选型
经过对5种技术方案的对比测试,最终确定的技术矩阵如下:
| 层级 | 技术选型 | 版本 | 选型理由 |
|---|---|---|---|
| 前端 | Vue.js + Element UI | 2.6.x | 组件化开发效率高,社区生态完善 |
| 后端框架 | Spring Boot | 2.7.x | 快速开发,自动配置,与Spring生态无缝集成 |
| 安全框架 | Spring Security | 5.7.x | 完善的认证授权机制,支持OAuth2 |
| ORM | MyBatis-Plus | 3.5.x | 简化CRUD操作,内置分页插件 |
| 缓存 | Redis | 6.2.x | 高并发场景下性能优异,支持多种数据结构 |
| 消息队列 | RabbitMQ | 3.9.x | 解耦选课流程,实现异步处理 |
| 数据库 | MySQL | 8.0.x | 事务支持完善,高校场景数据一致性要求高 |
| 搜索引擎 | Elasticsearch | 7.17.x | 实现课程快速检索,支持中文分词 |
| 容器化 | Docker | 20.10 | 环境一致性,便于部署扩展 |
2.2 核心架构设计
系统采用经典的三层架构,但针对选课场景做了特殊优化:
code复制[前端层]
↓ HTTP/HTTPS
[API网关层] → [认证中心] → [限流熔断]
↓
[业务服务层] ←→ [缓存层]
↓
[数据持久层] ←→ [搜索引擎]
关键设计要点:
- 网关层实现请求路由和负载均衡
- 业务服务按功能垂直拆分(用户服务、课程服务、选课服务等)
- Redis缓存热点数据(课程余量、学生课表等)
- Elasticsearch提供课程检索服务
- RabbitMQ异步处理选课结果通知
3. 数据库设计
3.1 核心表结构
经过4次迭代优化,最终确定的数据库包含28张表,这里展示5个核心表:
课程表(course)
sql复制CREATE TABLE `course` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`course_code` varchar(32) NOT NULL COMMENT '课程编号',
`name` varchar(100) NOT NULL COMMENT '课程名称',
`credit` decimal(3,1) NOT NULL COMMENT '学分',
`max_students` int NOT NULL DEFAULT 100 COMMENT '最大选课人数',
`selected_count` int NOT NULL DEFAULT 0 COMMENT '已选人数',
`teacher_id` bigint NOT NULL COMMENT '授课教师ID',
`schedule_json` json DEFAULT NULL COMMENT '排课时间安排',
`is_active` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否开放选课',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`course_code`),
KEY `idx_teacher` (`teacher_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
学生选课表(student_course)
sql复制CREATE TABLE `student_course` (
`id` bigint NOT NULL AUTO_INCREMENT,
`student_id` bigint NOT NULL,
`course_id` bigint NOT NULL,
`select_time` datetime NOT NULL COMMENT '选课时间',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '1-正常 2-已退课',
`score` decimal(5,2) DEFAULT NULL COMMENT '课程成绩',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_student_course` (`student_id`,`course_id`),
KEY `idx_course` (`course_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2 性能优化方案
针对选课高峰期的性能问题,我们实施了以下优化措施:
- 读写分离:配置1主2从的MySQL集群
- 分库分表:按学年将选课记录分到不同库
- 缓存策略:
- 课程余量使用Redis原子计数器
- 学生课表缓存有效期设为6小时
- SQL优化:
- 为高频查询字段添加复合索引
- 避免使用SELECT *
- 大数据量查询使用分页
4. 核心功能实现
4.1 选课业务流程
选课流程是我们投入最多精力的模块,最终实现的流程图如下:
code复制开始
↓
[身份认证] → 失败 → 返回错误
↓ 成功
[查询可选课程] ←→ [Redis缓存]
↓
[冲突检测] → 冲突 → 返回冲突提示
↓ 无冲突
[预扣减名额] ←→ [Redis计数器]
↓
[创建选课记录] → [MySQL]
↓
[发送选课成功通知] → [RabbitMQ]
↓
结束
关键代码片段(Java):
java复制@Transactional
public CourseSelectionResult selectCourse(Long studentId, Long courseId) {
// 1. 验证学生资格
Student student = studentService.getById(studentId);
if (student == null || !student.isActive()) {
return CourseSelectionResult.fail("学生状态异常");
}
// 2. 检查课程状态
Course course = courseCacheService.getCourse(courseId);
if (course == null || !course.isActive()) {
return CourseSelectionResult.fail("课程不可选");
}
// 3. 检查名额余量
int remaining = redisTemplate.opsForValue()
.decrement("course:quota:" + courseId, 1);
if (remaining < 0) {
redisTemplate.opsForValue()
.increment("course:quota:" + courseId, 1);
return CourseSelectionResult.fail("课程已满");
}
// 4. 检查时间冲突
if (scheduleService.hasConflict(studentId, course)) {
redisTemplate.opsForValue()
.increment("course:quota:" + courseId, 1);
return CourseSelectionResult.fail("时间冲突");
}
// 5. 持久化选课记录
StudentCourse record = new StudentCourse();
record.setStudentId(studentId);
record.setCourseId(courseId);
record.setSelectTime(LocalDateTime.now());
studentCourseMapper.insert(record);
// 6. 异步通知
rabbitTemplate.convertAndSend(
"course.selection.notice",
new SelectionNotice(studentId, courseId));
return CourseSelectionResult.success();
}
4.2 冲突检测算法
我们研发了基于时间片的冲突检测方案:
- 将每周时间划分为5(天)×12(时段)=60个时间片
- 每个课程用60位二进制表示时间占用情况
- 学生课表也用同样方式表示
- 通过位运算快速检测冲突
实现代码:
java复制public boolean hasConflict(Long studentId, Course newCourse) {
// 获取学生当前课表时间分布
long studentSchedule = getStudentScheduleBitmap(studentId);
// 获取新课时间分布
long newCourseSchedule = parseScheduleToBitmap(newCourse.getScheduleJson());
// 位与运算检测冲突
return (studentSchedule & newCourseSchedule) != 0;
}
private long parseScheduleToBitmap(String scheduleJson) {
// 解析课程时间安排为位图
// 实现细节省略...
}
5. 安全与性能优化
5.1 安全防护措施
-
认证授权:
- JWT令牌认证
- 基于角色的访问控制(RBAC)
- 敏感操作二次验证
-
数据安全:
- 密码加盐哈希存储
- 敏感字段AES加密
- SQL注入防护
-
防刷机制:
- 选课API限流(Guava RateLimiter)
- 验证码校验
- 设备指纹识别
5.2 性能调优实战
通过JMeter压测发现的性能瓶颈及解决方案:
| 瓶颈点 | QPS | 响应时间 | 解决方案 | 优化后QPS |
|---|---|---|---|---|
| 课程查询 | 1200 | 450ms | 增加Elasticsearch索引 | 6500 |
| 选课操作 | 800 | 600ms | 引入Redis预扣减+异步持久化 | 3500 |
| 课表生成 | 500 | 1200ms | 添加多级缓存 | 2800 |
关键缓存配置示例:
yaml复制spring:
redis:
cache:
# 课程信息缓存
course:
ttl: 30m
max-size: 10000
# 学生课表缓存
schedule:
ttl: 6h
max-size: 5000
6. 部署方案
6.1 容器化部署
使用Docker Compose编排服务:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6.2
ports:
- "6379:6379"
elasticsearch:
image: elasticsearch:7.17.7
environment:
- discovery.type=single-node
application:
build: .
ports:
- "8080:8080"
depends_on:
- mysql
- redis
- elasticsearch
volumes:
mysql_data:
6.2 高可用架构
生产环境部署方案:
code复制[负载均衡] → [应用集群] ←→ [Redis集群]
↓
[MySQL主从] ←→ [Elasticsearch集群]
7. 踩坑经验分享
在开发过程中遇到的典型问题及解决方案:
-
选课超卖问题
- 现象:高并发下课程名额出现超卖
- 原因:MySQL更新存在并发问题
- 解决:改用Redis原子计数器+异步持久化
-
缓存一致性问题
- 现象:管理员更新课程后前端看到旧数据
- 原因:缓存未及时失效
- 解决:采用双删策略+消息队列通知
-
分布式事务问题
- 现象:选课成功但通知未发送
- 原因:本地事务无法保证消息投递
- 解决:引入事务消息表+定时任务补偿
8. 项目成果
系统上线后的关键指标:
| 指标项 | 数值 |
|---|---|
| 日均选课量 | 12,000+ |
| 最高并发量 | 5,200 QPS |
| 平均响应时间 | 380ms |
| 系统可用性 | 99.98% |
| 课程冲突检出率 | 100% |
这套系统目前已在3所高校稳定运行2个学期,期间平稳度过了4次选课高峰期的考验。最大的收获是让我们深刻理解了高并发系统设计的精髓 - 不是追求技术的复杂度,而是找到业务场景与技术方案的最佳平衡点。