BINARY GARDEN
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 BootREST API、依赖注入、事务管理
ORMSpring 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 分布式锁

通过 StringRedisTemplatesetIfAbsent(即 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
  • 实时倒计时(基于服务端时间偏移校准,防止客户端篡改)
  • 逐题切换导航,记录答案到 answers ref
  • 提交时携带 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                       # 数据库初始化脚本