SpringBoot+Vue3选课系统高并发架构实战 1. 项目概述现代选课系统的技术架构演进十年前我参与第一个选课系统开发时还是基于JSPServlet的单体架构每次选课季服务器必崩。如今这套SpringBootVue3MyBatis的前后端分离方案完美支撑了某高校3万师生同时在线选课的需求。本文将完整呈现从技术选型到具体实现的每个关键细节。选课系统的核心矛盾在于高并发时段的系统稳定性与业务逻辑的复杂性。传统方案往往顾此失彼而我们采用的这套技术栈通过以下方式破局前端Vue3的Composition API处理动态课表渲染SpringBoot的异步线程池应对选课高峰MyBatis的动态SQL实现多条件课表查询MySQL的乐观锁控制选课冲突2. 技术栈深度解析2.1 SpringBoot后端设计要点选课系统的SpringBoot工程采用多模块设计course-system ├── course-api // 接口定义 ├── course-biz // 业务逻辑 ├── course-dao // 数据访问 └── course-web // 控制器层关键配置类示例Configuration EnableAsync public class AsyncConfig implements AsyncConfigurer { Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(20); // 选课核心线程数 executor.setMaxPoolSize(100); // 高峰时段扩容 executor.setQueueCapacity(500); // 等待队列 executor.setThreadNamePrefix(course-select-); executor.initialize(); return executor; } }重要提示线程池参数需要根据压测结果调整我们通过JMeter测试发现当并发超过800时MySQL连接池会成为瓶颈2.2 Vue3前端工程化实践前端采用Vue3VitePinia的技术组合目录结构如下src/ ├── api/ // 接口封装 ├── assets/ // 静态资源 ├── components/ // 公共组件 │ └── CourseTable.vue // 动态课表组件 ├── composables/ // 组合式函数 │ └── useCourseSelect.ts ├── router/ // 路由配置 └── stores/ // Pinia状态管理动态课表的核心实现script setup const columns reactive([ { key: mon, title: 周一 }, { key: tue, title: 周二 } // ...其他星期 ]) const fetchCourses async () { const { data } await api.get(/courses, { params: { semester: store.currentSemester, major: user.major } }) courses.value processCourseData(data) } /script2.3 MyBatis动态SQL实战课表查询需要处理多达12种筛选条件MyBatis的动态SQL完美应对select idselectCourses resultMapcourseResult SELECT * FROM courses where if testsemester ! null AND semester #{semester} /if if testcourseType ! null AND type #{courseType} /if !-- 其他条件 -- if testtimeRange ! null AND NOT EXISTS ( SELECT 1 FROM course_schedule WHERE course_id courses.id AND time_slot IN foreach itemslot collectiontimeRange open( separator, close) #{slot} /foreach ) /if /where ORDER BY credit DESC LIMIT #{offset}, #{pageSize} /select3. 核心业务实现细节3.1 选课并发控制方案我们对比测试了三种方案悲观锁SELECT FOR UPDATE性能差数据库唯一索引无法处理复杂约束乐观锁Redis原子操作最终方案具体实现代码public boolean selectCourse(Long studentId, Long courseId) { // Redis原子操作判断名额 Long remain redisTemplate.opsForValue() .decrement(course: courseId :quota); if (remain 0) { redisTemplate.opsForValue() .increment(course: courseId :quota); return false; } try { // 数据库乐观锁更新 int updated courseMapper.updateSelection( studentId, courseId, LocalDateTime.now(), // 使用版本号控制并发 getCurrentVersion(courseId)); return updated 0; } catch (Exception e) { // 回滚Redis计数 redisTemplate.opsForValue() .increment(course: courseId :quota); throw e; } }3.2 课表冲突检测算法核心冲突检测逻辑public boolean checkScheduleConflict(ListCourse selected, Course newCourse) { MapInteger, SetTimeSlot weekMap new HashMap(); // 转换已选课程时间 for (Course c : selected) { for (Schedule s : c.getSchedules()) { weekMap.computeIfAbsent(s.getWeekday(), k - new HashSet()) .addAll(expandTimeSlots(s)); } } // 检查新课程 for (Schedule s : newCourse.getSchedules()) { SetTimeSlot existing weekMap.get(s.getWeekday()); if (existing ! null) { for (TimeSlot newSlot : expandTimeSlots(s)) { if (existing.contains(newSlot)) { return true; } } } } return false; }4. 性能优化实战记录4.1 MySQL索引优化方案经过EXPLAIN分析我们为课程表设计了复合索引ALTER TABLE courses ADD INDEX idx_query ( semester, college_id, credit, capacity ); ALTER TABLE course_schedule ADD UNIQUE INDEX idx_course_time ( course_id, weekday, time_slot );4.2 前端性能提升技巧课表组件虚拟滚动template VirtualScroll :itemscourses :item-size60 template #default{ item } CourseItem :dataitem / /template /VirtualScroll /templateAPI请求防抖处理import { debounce } from lodash-es; const searchCourses debounce(async (keyword: string) { loading.value true; try { data.value await api.searchCourses(keyword); } finally { loading.value false; } }, 300);5. 部署架构与监控方案5.1 生产环境部署拓扑----------------- | CDN/OSS | ---------------- | ---------------------------------------------------------------- | Nginx (负载均衡) | | --------------------- --------------------- | | | SpringBoot节点1 | | SpringBoot节点2 | | | -------------------- -------------------- | | | | | | ----------v---------- ----------v---------- | | | Redis集群 | | MySQL主从 | | | --------------------- --------------------- | ----------------------------------------------------------------5.2 关键监控指标配置Prometheus监控配置示例- job_name: springboot metrics_path: /actuator/prometheus static_configs: - targets: [app1:8080, app2:8080] - job_name: mysql static_configs: - targets: [mysql-exporter:9104]Grafana监控看板包含选课成功率API响应时间P99MySQL活跃连接数Redis内存使用率6. 典型问题排查实录6.1 选课超时问题排查现象选课高峰期出现5%的请求超时排查过程发现线程池满日志Thread pool is EXHAUSTED检查MySQL连接池使用率高达90%发现课程查询SQL缺少索引定位到CourseSchedule表全表扫描解决方案-- 添加覆盖索引 ALTER TABLE course_schedule ADD INDEX idx_course_query ( course_id, weekday, classroom_id );6.2 内存泄漏问题处理通过Arthas排查步骤# 1. 监控堆内存 dashboard -i 5000 # 2. 分析堆对象 heapdump /tmp/heap.hprof # 3. 定位到课程缓存未清理 ognl com.example.CacheManagerinstance.cacheMap.size()最终发现是课程详情缓存未设置TTL添加Redis过期配置Bean public RedisCacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config RedisCacheConfiguration .defaultCacheConfig() .entryTtl(Duration.ofHours(2)) // 设置2小时过期 .disableCachingNullValues(); return RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); }7. 安全防护方案7.1 防刷课机制实现滑动窗口限流算法public boolean allowSelection(Long studentId) { String key limit: studentId; Long now System.currentTimeMillis(); // 移除1分钟外的记录 redisTemplate.opsForZSet().removeRangeByScore( key, 0, now - 60_000); // 添加当前请求 redisTemplate.opsForZSet().add( key, UUID.randomUUID().toString(), now); // 检查1分钟内请求次数 return redisTemplate.opsForZSet() .zCard(key) 30; // 每分钟不超过30次 }7.2 SQL注入防护始终使用MyBatis参数化查询!-- 错误示范 -- select idfindCourses SELECT * FROM courses WHERE name LIKE %${name}% /select !-- 正确做法 -- select idfindCourses SELECT * FROM courses WHERE name LIKE CONCAT(%,#{name},%) /select启用MyBatis安全审计插件Intercepts({ Signature(type StatementHandler.class, methodprepare, args{Connection.class,Integer.class}) }) public class SqlAuditInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) { StatementHandler handler (StatementHandler) invocation.getTarget(); String sql handler.getBoundSql().getSql(); if (sql.matches(.*(drop|alter|truncate).*)) { throw new SecurityException(危险SQL操作); } return invocation.proceed(); } }8. 项目演进方向在实际运行中我们还规划了以下优化方向选课结果实时推送改用WebSocket替代轮询分布式事务方案引入Seata处理跨服务选课弹性扩缩容基于K8s的HPA自动扩缩容智能推荐基于学生历史数据的课程推荐算法特别分享一个性能调优经验在课程查询接口添加二级缓存后QPS从1200提升到6500。关键实现是使用Caffeine作为本地缓存Redis作为分布式缓存通过发布订阅机制保持各节点缓存一致性。

本月热点