2025-12-17 // web · 高并发 · 全栈 · Docker · sibuchen
HCES
HCES — 高并发在线考试系统
项目概述
HCES(High Concurrency Exam System)是一套面向大规模在线考试场景的全栈 Web 应用。系统支持教师创建考试、管理题库,学生在线答题、实时倒计时、自动阅卷与成绩查询,并通过 Redis 分布式锁和 RabbitMQ 消息队列应对考试开始/结束瞬间的并发洪峰。
技术栈: Spring Boot + Vue 3 + MySQL + Redis + RabbitMQ + Docker Compose
系统架构
┌─────────────────────────────────────────────────────────────────┐
│ Vue 3 前端 (Vite) │
│ Pinia 状态管理 · Vue Router · Element Plus · Axios │
└───────────────────────────┬─────────────────────────────────────┘
│ HTTP REST
┌───────────────────────────▼─────────────────────────────────────┐
│ Spring Boot 后端 (Java) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐│
│ │ 用户模块 │ │ 题目模块 │ │ 考试模块 │ │ 通用模块 ││
│ │ Auth │ │ Question │ │ Exam │ │ ApiResponse ││
│ │ User │ │ │ │ │ │ GlobalException ││
│ └──────────┘ └──────────┘ └────┬─────┘ └──────────────────┘│
│ │ │
│ ┌──────────┴──────────┐ │
│ │ ExamCommandService │ │
│ │ (高并发命令写入) │ │
│ └──┬──────────┬───────┘ │
│ │ │ │
└────────────────────────────┼──────────┼──────────────────────────┘
│ │
┌──────────────▼┐ ┌────▼──────────────┐
│ Redis │ │ RabbitMQ │
│ 分布式锁 │ │ exam.submit.* │
│ 状态缓存 │ │ 异步削峰队列 │
└───────────────┘ └────────┬──────────┘
│ 消费者异步落库
┌────────▼──────────┐
│ MySQL 8 │
│ JPA / Hibernate │
└───────────────────┘
技术选型与职责
| 组件 | 技术 | 职责 |
|---|---|---|
| 前端框架 | Vue 3 + TypeScript + Vite | 单页应用,学生端/教师端双角色 UI |
| 状态管理 | Pinia (authStore / examStore) | 用户登录态、考试进入态的全局管理 |
| UI 组件库 | Element Plus | 表单、表格、消息提示、对话框 |
| 后端框架 | Spring Boot | REST API、依赖注入、事务管理 |
| ORM | Spring Data JPA + Hibernate | 数据持久化,ddl-auto: update 自动建表 |
| 缓存/锁 | Redis 7.2 (StringRedisTemplate) | 分布式锁防重、学生状态缓存、submitToken 存储 |
| 消息队列 | RabbitMQ 3.13 (AMQP) | 考试提交异步削峰,Producer → DirectExchange → Consumer |
| 数据库 | MySQL 8.0 (utf8mb4) | 业务数据持久存储 |
| 容器化 | Docker Compose | 一键编排 MySQL + Redis + RabbitMQ,共享内部网络 |
后端分层设计
后端按领域(Domain)组织包结构,每个领域严格遵循 Controller → Service → Repository 三层分离:
com.hces
├── common # 通用层
│ ├── api/ApiResponse # 统一响应封装 { success, message, data }
│ ├── controller # Health / Diagnostic / Time 健康检查
│ └── exception/GlobalExceptionHandler # @RestControllerAdvice 全局异常处理
│
├── user # 用户域
│ ├── controller/AuthController # POST /api/auth/register, /login
│ ├── domain/User # 实体:userId, username, password, role, classId
│ ├── service/UserService # 接口:findByUsername, findByUserId, save
│ └── repository/UserRepository # JPA Repository
│
├── question # 题目域
│ ├── controller/TeacherQuestionController # 教师端题库 CRUD
│ ├── domain/Question # 实体:questionId, content, optionsJson, type
│ ├── service/QuestionService # 接口 + QuestionServiceImpl
│ └── repository/QuestionRepository
│
└── exam # 考试域(核心)
├── controller
│ ├── TeacherExamController # 教师端:创建考试、管理考试列表、删除
│ ├── StudentExamController # 学生端:历史成绩、试卷回顾
│ ├── ExamQueryController # 查询:按考试ID/按班级、含学生状态推导
│ └── ExamAccessController # 高并发入口:进入考试、提交考试
├── domain
│ ├── Exam # 考试实体(title, startTime, endTime, duration, classId...)
│ ├── ExamQuestion # 考试-题目关联表(score, orderNo)
│ ├── ExamAnswerRecord # 答卷记录(answersJson, score)
│ ├── StudentExamRecord # 学生考试记录(status, enteredAt, submittedAt)
│ ├── WrongQuestion # 错题本
│ ├── ExamStatus # 枚举:NOT_STARTED / ONGOING / ENDED
│ └── StudentExamStatus # 枚举:NOT_ENTERED / ENTERED / SUBMITTED
├── service
│ ├── ExamService # 查询服务 + 状态推导(resolveStatus)
│ ├── ExamCommandService # 命令服务:enterExam / submitExam(带并发控制)
│ └── StudentExamRecordService
├── messaging # MQ 异步消息层
│ ├── ExamMessagingConfig # DirectExchange + Queue + Binding 声明
│ ├── ExamSubmitProducer # 生产者:发送提交消息到 MQ
│ ├── ExamSubmitConsumer # 消费者:监听队列,算分 + 持久化
│ └── ExamSubmitMessage # 消息体 record
└── support/ExamCacheKeys # Redis Key 统一管理(防硬编码)
核心业务流程
1. 学生进入考试(高并发入口)
学生请求 POST /api/exams/{examId}/enter
│
▼
ExamCommandServiceImpl.enterExam()
│
├── 1. 校验考试是否存在、状态是否为 ONGOING
├── 2. Redis 分布式锁 (examId:userId, TTL=3s) ← 防重复进入
├── 3. 先查数据库是否已 SUBMITTED,再查 Redis 缓存状态
├── 4. 若已 ENTERED → 直接返回缓存的 submitToken(幂等)
├── 5. 若首次进入 → 写入 StudentExamRecord + Redis 状态缓存
│ 生成 UUID submitToken 存入 Redis(TTL = 考试时长 + 5min)
└── 6. 释放锁,返回 EnterExamResponse
2. 学生提交考试(MQ 异步削峰)
学生请求 POST /api/exams/{examId}/submit
│
▼
ExamCommandServiceImpl.submitExam()
│
├── 1. 校验 submitToken(Redis 优先 → DB 兜底) ← 防非法提交
├── 2. Redis 分布式锁 (examId:userId, TTL=3s) ← 防重复提交
├── 3. 标记 StudentExamRecord 为 SUBMITTED
├── 4. 更新 Redis 缓存状态为 SUBMITTED
├── 5. ★ 将答卷数据投递到 RabbitMQ(exam.submit.exchange)
│ 返回 SubmitExamResponse(立即响应,不等算分)
└── 6. 释放锁
│
▼ 异步消费
ExamSubmitConsumer.onExamSubmit() [@RabbitListener]
│
├── 1. 序列化答案 → 持久化 ExamAnswerRecord
├── 2. 遍历题目自动阅卷计算成绩
├── 3. 将成绩同步写入 StudentExamRecord.score
└── 4. 同步错题本(WrongQuestion)
3. 考试状态推导(无状态设计)
考试状态不在数据库中存储,而是根据时间窗口实时推导:
// ExamService.resolveStatus()
if (now.isBefore(exam.startTime)) → NOT_STARTED
if (now.isAfter(exam.endTime)) → ENDED
else → ONGOING
这种设计避免了定时任务同步状态的复杂性,任何时刻查询都能得到正确状态。
高并发设计要点
Redis 分布式锁
通过 StringRedisTemplate 的 setIfAbsent(即 Redis SETNX)实现轻量级分布式锁:
- 锁粒度:
exam:{examId}:user:{userId}:lock,精确到每个学生的每次操作 - TTL: 3 秒自动过期,防止死锁
- 作用: 进入考试和提交考试两个写操作均加锁,保证幂等性
Redis 状态缓存
- 学生考试状态(
ENTERED/SUBMITTED)缓存在 Redis,TTL = 考试时长 + 5 分钟 - submitToken 缓存在 Redis,提交时先从 Redis 校验,未命中再查 DB
- 考试全局状态缓存,减少对考试时间窗口的重复计算
RabbitMQ 异步削峰
这是应对高并发提交的关键设计:
- 为什么不直接入库? 考试结束瞬间大量学生同时提交,直接写 MySQL 会造成连接池耗尽和锁竞争
- Producer 端:
submitExam()只做状态标记 + 发送 MQ 消息,立即返回响应(毫秒级) - Consumer 端:
@RabbitListener单线程顺序消费,平稳写入 MySQL,自动算分 + 同步错题 - 消息可靠性: 使用持久化队列(
durable=true)+ JSON 序列化(替代 Java 原生序列化,避免安全风险)
SubmitToken 防重机制
- 学生进入考试时服务端生成 UUID token,存入 Redis 并返回前端
- 提交时必须携带 token,服务端校验 token 匹配后才允许提交
- 有效防止了重复提交和跨会话伪造提交
前端设计
双角色体系
通过路由守卫和 authStore 中的 role 字段区分教师端和学生端:
| 角色 | 登录后落地页 | 页面 |
|---|---|---|
| 教师 | 题库管理 | 题库管理、创建考试、发布管理、消息中心 |
| 学生 | 考试总览 | 考试总览、在线答题、历史成绩、错题本、消息中心 |
考试页面(StudentExamView)
前端考试页面包含完整的答题交互逻辑:
- 进入考试 → 调用
enterExam获取 submitToken - 实时倒计时(基于服务端时间偏移校准,防止客户端篡改)
- 逐题切换导航,记录答案到
answersref - 提交时携带 submitToken,提交成功后跳转
- 已提交学生无法再次进入(前后端双重校验)
API 封装
前端 API 层按职责拆分,统一通过 http.ts 中的 Axios 实例发送请求:
api/
├── http.ts # Axios 实例(baseURL、拦截器)
├── auth.ts # 登录/注册
├── exam.ts # 考试进入/提交/查询
├── student.ts # 历史成绩/试卷回顾/错题本
├── teacher.ts # 教师端考试管理/题库管理
└── time.ts # 服务端时间同步
数据库设计
核心表(由 JPA 实体 + ddl-auto: update 自动映射):
| 表(实体) | 说明 | 关键字段 |
|---|---|---|
user | 用户表 | userId, username, password, role(student/teacher), classId |
question | 题库表 | questionId, content, optionsJson, type, answer |
exam | 考试表 | examId, title, startTime, endTime, durationMinutes, classId, createdBy |
exam_question | 考试-题目关联 | examId, questionId, score, orderNo |
student_exam_record | 学生考试记录 | examId, userId, status, enteredAt, submittedAt, score, submitToken |
exam_answer_record | 答卷详情 | examId, userId, answersJson, score, submittedAt |
wrong_question | 错题本 | userId, examId, questionId, yourAnswer, correctAnswer |
部署架构
使用 Docker Compose 一键部署三个中间件,后端和前端可分别在 IDE / CLI 中启动:
# docker-compose.yml
services:
06_mysql: # MySQL 8.0 端口映射 ${MYSQL_PORT}:3306
06_redis: # Redis 7.2 端口映射 ${REDIS_PORT}:6379
06_rabbitmq: # RabbitMQ 3.13 端口映射 ${RABBITMQ_AMQP_PORT}:5672 + 管理后台 ${RABBITMQ_WEB_PORT}:15672
networks:
06_HCES: # 三容器共享桥接网络,通过服务名互相通信
volumes:
mysql_data / redis_data / rabbitmq_data # 持久化数据卷
API 接口一览
认证模块
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/auth/register | 用户注册 |
| POST | /api/auth/login | 用户登录 |
考试模块(高并发)
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/exams/{examId}/enter | 进入考试(分布式锁 + 状态缓存 + 幂等) |
| POST | /api/exams/{examId}/submit | 提交考试(submitToken 校验 + MQ 异步落库) |
考试查询
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/exams/{examId}?userId= | 查询单场考试详情(含学生状态) |
| GET | /api/exams/class/{classId}?userId= | 按班级查询考试列表 |
教师端
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/teacher/exams | 创建考试 |
| GET | /api/teacher/exams?creatorUserId= | 查询教师创建的考试 |
| GET | /api/teacher/exams/{examId}/questions | 查询考试题目列表 |
| DELETE | /api/teacher/exams/{examId} | 删除考试 |
学生端
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/student/history-scores?userId= | 查询历史成绩 |
| GET | /api/student/exam-review/{examId}?userId= | 试卷回顾 |
| GET | /api/student/wrong-questions?userId= | 错题本 |
项目结构
HCES/
├── docker/
│ ├── docker-compose.yml # 容器编排
│ └── .env.example # 环境变量模板
├── hces-backend/
│ └── src/main/
│ ├── java/com/hces/
│ │ ├── common/ # 通用:ApiResponse, GlobalExceptionHandler
│ │ ├── user/ # 用户域:AuthController, UserService
│ │ ├── question/ # 题目域:TeacherQuestionController, QuestionService
│ │ └── exam/ # 考试域:Controller + Service + Messaging + Repository
│ └── resources/
│ └── application.yml # Spring Boot 配置
├── hces-frontend/
│ └── src/
│ ├── api/ # HTTP 请求封装(6个模块)
│ ├── components/ # 侧边栏组件
│ ├── router/ # 路由配置(学生端 + 教师端)
│ ├── stores/ # Pinia 状态(authStore, examStore)
│ └── views/ # 12个页面视图
└── hces3.sql # 数据库初始化脚本