第一章 项目背景与核心挑战
1.1 业务场景描述
在线考试系统在考试开始、结束时段会面临极高的并发访问压力。典型场景:
-
考前5分钟:百万考生同时登录、加载试卷
-
考试过程中:每3分钟自动保存,持续产生写入请求
-
交卷时刻:数万人同时点击提交,形成瞬时峰值
1.2 三大核心技术挑战
| 挑战 | 问题描述 | 严重后果 |
|---|---|---|
| 时序冲突 | 自动保存未完成时用户交卷,数据丢失 | 考生答案缺失,投诉率高 |
| 并发峰值 | 交卷瞬间QPS飙升,服务雪崩 | 系统超时、交卷失败 |
| 数据一致性 | 缓存与数据库数据不一致 | 查分结果与答卷不符 |
1.3 设计目标
-
顺序性:同一用户的操作严格串行执行
-
可用性:系统可用性 ≥ 99.99%
-
一致性:提交的数据100%包含所有已保存答案
-
吞吐量:单集群支持1万+用户同时交卷
第二章 总体架构设计
2.1 架构核心理念
┌─────────────────────────────────────────────────────────────┐
│ 核 心 理 念:顺 序 化 + 异 步 化 │
├─────────────────────────────────────────────────────────────┤
│ 1. 每个用户一个操作队列 → 保证顺序性 │
│ 2. 保存操作异步化 → 削峰填谷 │
│ 3. 提交操作同步等待 → 确保数据完整 │
│ 4. 三级缓存兜底 → 性能与可靠性的平衡 │
└─────────────────────────────────────────────────────────────┘
2.2 技术选型
| 组件 | 技术选型 | 核心作用 |
|---|---|---|
| 消息队列 | RabbitMQ | 用户专属队列、操作顺序保证 |
| 缓存 | Redis Cluster | 最新答案存储、版本管理、分布式锁 |
| 数据库 | MySQL + 分库分表 | 最终数据持久化 |
| 限流 | Sentinel + 令牌桶 | 多层级限流防护 |
| 监控 | Prometheus + Grafana | 实时监控与告警 |
2.3 整体架构图
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 前端 │───▶│ 网关 │───▶│ 应用层 │───▶│ 缓存 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ RabbitMQ │ │ 数据库 │
└──────────┘ └──────────┘
│ ▲
└────────────────┘
异步持久化 & 最终一致性
第三章 核心功能实现
3.1 用户专属队列机制
3.1.1 设计原理
关键决策:每个考生一个独立队列,保证操作顺序性。
为什么不是每个用户一个消费者?
-
10万用户就需要10万消费者线程,资源不可行
-
实际方案:队列独享,消费者共享
实现方式:
// 队列命名规则
exam.user.queue.{examId}_{userId}
// 消费者配置
concurrentConsumers = 1 // 每个队列单线程消费
prefetchCount = 1 // 每次只取1条消息
x-single-active-consumer = true // 单活消费者模式
3.1.2 完整调用链路
用户开始考试
↓
创建专属队列(Redis记录元数据)
↓
分配消费者组(基于userId哈希)
↓
队列绑定到共享消费者(单线程处理)
↓
开始接收保存/自动保存消息
↓
考试结束/提交完成 → 自动删除队列
3.1.3 顺序性保障验证
| 场景 | 用户队列 | 消费者 | 消息顺序 |
|---|---|---|---|
| 单用户连续保存 | 1个队列 | 1个消费者 | 严格FIFO |
| 多用户并行保存 | 多个队列 | 多个消费者 | 用户内有序,用户间并行 |
| 交卷前等待 | 队列清空检查 | - | 所有保存完成才提交 |
3.2 保存-提交时序保障
3.2.1 状态机设计
保存/自动保存
┌─────────────────────────┐
▼ │
IN_PROGRESS ──────► SAVING/AUTO_SAVING
│ │
│ 提交 │ 保存完成
▼ ▼
SUBMITTING ──────► SUBMITTED ──────► END
│ 完成
│ 超时/失败
▼
IN_PROGRESS(回滚)
3.2.2 交卷前置检查(关键保障)
交卷请求处理流程:
获取分布式锁(防止重复提交)
检查考试状态(已提交则拒绝)
更新状态为 SUBMITTING(阻止新保存)
等待用户队列清空(四重检查)
├─ 队列消息数 = 0
├─ 无 SAVING/AUTO_SAVING 状态
├─ 3秒内无保存操作
└─ 无未确认消息
获取最终答案(Redis最新版本)
发送高优先级提交消息
返回"提交已受理"
3.2.3 超时兜底策略
| 等待时长 | 处理策略 | 用户感知 |
|---|---|---|
| < 3秒 | 继续等待 | 无感知 |
| 3-8秒 | 主动重试3次 | 轻微延迟 |
| 8-10秒 | 使用最新可用数据强制交卷 | 提示"提交成功" |
| > 10秒 | 进入异步处理队列 | 提示"提交已受理" |
3.3 高并发削峰填谷
3.3.1 三级限流策略
第一层(网关层):
-
IP级限流:单IP每秒最多10次请求
-
用户级限流:单用户每秒最多5次保存
第二层(服务层):
// 保存接口:1000 QPS
RateLimiter saveLimiter = RateLimiter.create(1000);
// 提交接口:100 QPS(更严格)
RateLimiter submitLimiter = RateLimiter.create(100);
第三层(方法级):
-
分布式限流(Redis + Lua)
-
基于考试ID的令牌桶
3.3.2 异步化处理
保存操作:
请求到达 → 生成MessageId → 发送到MQ → 立即返回"已受理"
↓
消费者处理(单线程)
↓
Redis写入(成功即返回)
↓
异步持久化到数据库
性能提升:
-
接口响应时间:从500ms降至50ms
-
数据库压力:降低80%
-
系统吞吐量:提升10倍
3.3.3 优先级队列
| 操作类型 | 队列 | 优先级 | 说明 |
|---|---|---|---|
| 提交 | exam.submit.queue |
10(最高) | 保证快速处理 |
| 手动保存 | exam.user.queue |
5 | 正常优先级 |
| 自动保存 | exam.user.queue |
1 | 最低优先级 |
3.4 数据一致性保障
3.4.1 版本控制机制
设计原则 :乐观锁 + 版本号递增
// 每次保存版本号+1
currentVersion = 5
newVersion = 6
cacheManager.save(examId, userId, answers, newVersion)
// 交卷时使用最新版本
finalVersion = cacheManager.getCurrentVersion()
submission.setVersion(finalVersion)
冲突处理:
-
版本不一致 → 返回最新版本给前端
-
前端自动重试(使用新版本)
3.4.2 幂等性设计
双ID保障:
| ID类型 | 生成方 | 作用 | 存储 |
|---|---|---|---|
operationId |
前端 | 业务操作唯一标识 | Redis 24h |
messageId |
后端 | 消息唯一标识 | Redis 30min |
幂等检查流程:
消息到达
↓
检查 messageId 是否已处理
├─ 已处理 → 直接ACK,丢弃
└─ 未处理 → 执行业务逻辑
↓
标记 messageId 已处理
↓
业务完成,ACK消息
3.4.3 三级缓存架构
| 级别 | 存储 | 特点 | 用途 |
|---|---|---|---|
| L1 | Caffeine(本地) | 毫秒级响应 | 当前用户会话数据 |
| L2 | Redis Cluster | 高可用、持久化 | 所有用户最新答案 |
| L3 | MySQL | 最终存储 | 历史数据、审计 |
写策略:
写请求 → L1缓存(立即) → L2缓存(同步) → 消息队列 → L3数据库(异步)
↓ ↓
用户无感知 响应返回
3.5 容错与降级
3.5.1 服务降级等级
一级降级(轻微压力):
-
自动保存间隔:3分钟 → 5分钟
-
非核心日志:异步写入 → 丢弃
二级降级(中等压力):
-
自动保存:改为本地存储
-
答题分析:返回缓存版本
-
历史记录:只查最近3天
三级降级(严重压力):
-
保存功能:提示用户手动保存
-
交卷功能:仅保留核心校验
-
静态资源:全部走CDN
3.5.2 故障恢复机制
保存失败恢复:
前端保留本地草稿
定时任务每30秒重试
最多重试10次
仍失败 → 上报监控,人工介入
交卷失败恢复:
自动重试3次(间隔1s, 2s, 3s)
重试失败 → 进入死信队列
死信消费者记录错误
开发人员人工修复
补偿提交(管理员后台)
数据不一致修复:
-
每分钟对账任务
-
对比Redis与MySQL数据
-
自动修复差异(以Redis为准)
-
记录修复日志
第四章 监控与运维
4.1 关键监控指标
| 指标分类 | 具体指标 | 阈值 | 告警级别 |
|---|---|---|---|
| 队列 | 保存队列积压数 | > 5000 | 黄色 |
| 提交队列积压数 | > 1000 | 红色 | |
| 死信队列消息数 | > 100 | 红色 | |
| 业务 | 保存成功率 | < 99.9% | 黄色 |
| 提交成功率 | < 99.5% | 红色 | |
| 交卷平均耗时 | > 3秒 | 黄色 | |
| 系统 | Redis命中率 | < 85% | 黄色 |
| 数据库连接池 | > 80% | 红色 | |
| CPU负载 | > 70% | 黄色 |
第五章 应急预案
5.1 高并发场景预案
| 场景 | 触发条件 | 自动响应 | 人工介入 |
|---|---|---|---|
| 流量突增 | QPS > 5000 | 开启网关限流 | 扩容应用实例 |
| 队列积压 | 积压 > 10000 | 增加消费者数量 | 排查消费瓶颈 |
| 数据库压力 | 连接 > 80% | 读写分离 | 主从切换 |
| Redis故障 | 主节点宕机 | 自动故障转移 | 恢复主节点 |
5.2 数据安全预案
双写备份:
正常流程:Redis ← 业务请求
↓
数据库
故障流程:Redis 不可用
↓
消息队列记录操作
↓
Redis恢复后回放
交卷保障:
- 主库写入失败
↓
- 写入从库(标记为故障)
↓
- 记录异常交卷日志
↓
- 主库恢复后补偿写入
5.3 人工干预通道
强制提交和数据修复
第六章 性能测试报告
6.1 瓶颈分析
主要瓶颈:
-
Redis CPU(建议扩容集群)
-
数据库连接池(建议增加连接数)
-
RabbitMQ磁盘I/O(建议SSD)
优化措施:
-
Redis:由3节点扩容至6节点
-
数据库:连接池由50增至100
-
MQ:消息持久化优化
第七章 总结与最佳实践
7.1 核心原则
┌─────────────────────────────────────────┐
│ 考 试 系 统 设 计 三 大 法 则 │
├─────────────────────────────────────────┤
│ 1. 用户操作必须串行化 │
│ → 一人一队列,交卷必须等保存完成 │
│ │
│ 2. 写请求必须异步化 │
│ → 先写缓存,异步落库,削峰填谷 │
│ │
│ 3. 关键操作必须幂等化 │
│ → operationId + messageId双重保障 │
└─────────────────────────────────────────┘
7.2 方案亮点
-
顺序性保障:用户专属队列 + 单线程消费,绝对FIFO
-
资源高效:共享消费者池,避免10万+线程开销
-
数据可靠:三级缓存 + 版本控制 + 幂等设计
-
弹性伸缩:基于流量预测的自动扩缩容
-
快速恢复:完善的降级与补偿机制
7.3 适用场景
-
在线考试系统
-
在线测评平台
-
大规模问卷调查
-
任何需要保证操作顺序的高并发场景
附录:术语表
| 术语 | 解释 |
|---|---|
| User Queue | 用户专属队列,用于保证操作顺序 |
| OperationId | 业务操作ID,前端生成,用于幂等 |
| MessageId | 消息唯一ID,后端生成,用于去重 |
| Prefetch Count | 消费者预取消息数,设为1保证顺序 |
| Single Active Consumer | RabbitMQ单活消费者模式 |
| Dead Letter Queue | 死信队列,处理失败消息 |