高并发在线考试系统稳定性保障方案

第一章 项目背景与核心挑战

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 交卷前置检查(关键保障)

交卷请求处理流程

  1. 获取分布式锁(防止重复提交)

  2. 检查考试状态(已提交则拒绝)

  3. 更新状态为 SUBMITTING(阻止新保存)

  4. 等待用户队列清空(四重检查)

├─ 队列消息数 = 0

├─ 无 SAVING/AUTO_SAVING 状态

├─ 3秒内无保存操作

└─ 无未确认消息

  1. 获取最终答案(Redis最新版本)

  2. 发送高优先级提交消息

  3. 返回"提交已受理"

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 故障恢复机制

保存失败恢复

  1. 前端保留本地草稿

  2. 定时任务每30秒重试

  3. 最多重试10次

  4. 仍失败 → 上报监控,人工介入

交卷失败恢复

  1. 自动重试3次(间隔1s, 2s, 3s)

  2. 重试失败 → 进入死信队列

  3. 死信消费者记录错误

  4. 开发人员人工修复

  5. 补偿提交(管理员后台)

数据不一致修复

  • 每分钟对账任务

  • 对比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恢复后回放

交卷保障

  1. 主库写入失败

  1. 写入从库(标记为故障)

  1. 记录异常交卷日志

  1. 主库恢复后补偿写入

5.3 人工干预通道

强制提交和数据修复

第六章 性能测试报告

6.1 瓶颈分析

主要瓶颈

  1. Redis CPU(建议扩容集群)

  2. 数据库连接池(建议增加连接数)

  3. RabbitMQ磁盘I/O(建议SSD)

优化措施

  • Redis:由3节点扩容至6节点

  • 数据库:连接池由50增至100

  • MQ:消息持久化优化

第七章 总结与最佳实践

7.1 核心原则

┌─────────────────────────────────────────┐

│ 考 试 系 统 设 计 三 大 法 则 │

├─────────────────────────────────────────┤

│ 1. 用户操作必须串行化 │

│ → 一人一队列,交卷必须等保存完成 │

│ │

│ 2. 写请求必须异步化 │

│ → 先写缓存,异步落库,削峰填谷 │

│ │

│ 3. 关键操作必须幂等化 │

│ → operationId + messageId双重保障 │

└─────────────────────────────────────────┘

7.2 方案亮点

  1. 顺序性保障:用户专属队列 + 单线程消费,绝对FIFO

  2. 资源高效:共享消费者池,避免10万+线程开销

  3. 数据可靠:三级缓存 + 版本控制 + 幂等设计

  4. 弹性伸缩:基于流量预测的自动扩缩容

  5. 快速恢复:完善的降级与补偿机制

7.3 适用场景

  • 在线考试系统

  • 在线测评平台

  • 大规模问卷调查

  • 任何需要保证操作顺序的高并发场景

附录:术语表

术语 解释
User Queue 用户专属队列,用于保证操作顺序
OperationId 业务操作ID,前端生成,用于幂等
MessageId 消息唯一ID,后端生成,用于去重
Prefetch Count 消费者预取消息数,设为1保证顺序
Single Active Consumer RabbitMQ单活消费者模式
Dead Letter Queue 死信队列,处理失败消息
相关推荐
gAlAxy...3 小时前
MyBatis-Plus 核心 CRUD 操作全解析:BaseMapper 与通用 Service 实战
java·开发语言·mybatis
好好研究4 小时前
MyBatis - Plus(二)常见注解 + 常见配置
数据库·spring boot·mybatis·mybatis plus
树码小子5 小时前
Mybatis(8)#{}和${}的区别,排序,模糊查询
mybatis
在坚持一下我可没意见10 小时前
ideaPool论坛系统测试报告
java·spring boot·功能测试·selenium·jmeter·mybatis·压力测试
树码小子12 小时前
Mybatis(7)其他查询操作(多表查询)
spring boot·mybatis
Byte不洛14 小时前
TCP 服务器如何支持高并发?单进程、多进程、多线程模型详解
linux·网络编程·高并发·tcp·socket编程
NGC_661114 小时前
Mybatis处理流程
数据库·oracle·mybatis
SelectDB技术团队15 小时前
Apache Doris 4.0.3 版本正式发布
apache·mybatis
青云计划1 天前
知光项目知文发布模块
java·后端·spring·mybatis