CodeSpirit-考试预生成方案(开源)

1. 概述

1.1 背景

在考试系统中,当大量学生同时开始考试时,系统需要为每个学生创建考试记录(ExamRecord)和答题记录(ExamAnswerRecord)。传统的"按需创建"模式在高并发场景下存在以下问题:

  • 性能瓶颈:每次开始考试都需要执行数据库写入操作,响应时间在 200-500ms

  • 并发压力:1000+ 学生同时开考时,数据库压力激增,可能导致超时或失败

  • 用户体验:学生点击"开始考试"后需要等待较长时间才能进入考试界面

image-20251229202908907

  • Github:xin-lai/CodeSpirit
  • Gitee:magicodes/CodeSpirit

1.2 解决方案

考试记录预生成方案通过定时任务(每天凌晨1点)批量预生成所有已发布且尚未开始的考试的记录和答题记录,将数据库写入操作从"考试开始时刻"提前到"凌晨低负载时段",从而:

  • 性能提升:开始考试耗时从 200-500ms 降低到 10-50ms(命中预生成记录时)
  • 并发优化:数据库写入压力分散到凌晨低负载时段,避免影响正在进行的考试
  • 用户体验:学生点击开始后即刻进入考试,无感知延迟
  • 数据一致性:题目顺序预先确定,避免并发冲突

1.3 核心特性

  • 定时预生成:每天凌晨1点通过定时任务统一预生成,避免影响正在进行的考试
  • 智能预生成:仅预生成第一次考试记录(AttemptNumber = 1),后续考试动态创建
  • 缓存优化:预生成记录写入缓存,开始考试时优先查询缓存,减少数据库查询
  • 智能检测:开始考试时自动检测预生成记录,命中则快速启动,未命中则动态创建
  • 垃圾清理:定时任务自动清理未使用的预生成记录,避免数据冗余
  • 容错机制:预生成失败不影响考试发布,新增学生自动降级为动态创建

2. 架构设计

2.1 系统架构图

sequenceDiagram participant Admin as 管理员 participant Controller as ExamSettingsController participant Service as ExamSettingService participant ScheduledTask as 定时预生成任务 participant Cache as 缓存层 participant DB as 数据库 participant Student as 学生 participant StartExam as CreateExamRecordAsync Note over Admin,Service: 阶段1:考试发布 Admin->>Controller: 发布考试 Controller->>Service: PublishExamSettingAsync Service->>DB: 更新考试状态为Published Service-->>Admin: 返回成功 Note over ScheduledTask,DB: 阶段2:定时预生成(每天凌晨1点) ScheduledTask->>DB: 查询已发布且尚未开始的考试 ScheduledTask->>DB: 检查是否已预生成 ScheduledTask->>DB: 分批创建ExamRecord(NotStarted) ScheduledTask->>DB: 批量创建ExamAnswerRecord ScheduledTask->>Cache: 写入预生成记录ID(过期时间=考试结束时间) ScheduledTask->>ScheduledTask: 打印详细日志 Note over Student,StartExam: 阶段3:学生开始考试 Student->>StartExam: 点击开始考试 StartExam->>Cache: 查询预生成记录ID alt 缓存命中 StartExam->>DB: 加载预生成记录 StartExam->>DB: UPDATE状态为InProgress<br/>设置StartTime StartExam-->>Student: 快速启动(10-50ms) ✅ else 缓存未命中(新增学生) StartExam->>DB: 动态创建完整记录 StartExam-->>Student: 常规启动(200-500ms) ⚠️ end Note over ScheduledTask,DB: 阶段4:定时清理(每天凌晨2点) ScheduledTask->>DB: 查询已结束考试的NotStarted记录 ScheduledTask->>DB: 批量删除未使用记录 ScheduledTask->>Cache: 清理相关缓存

2.2 数据流设计

graph TB A[考试发布] --> B[更新状态为Published] B --> C[等待定时任务执行] D["定时任务 每天凌晨1点"] --> E[查询已发布且尚未开始的考试] E --> F{是否已预生成?} F -->|是| G[跳过该考试] F -->|否| H[获取学生分组列表] H --> I[分批处理学生列表] I --> J["创建ExamRecord Status=NotStarted"] J --> K[创建ExamAnswerRecord列表] K --> L["写入缓存 Key: exam:pregenerated:examId:studentId:1 Value: recordId Expire: 考试结束时间+1小时"] L --> M{是否还有批次?} M -->|是| I M -->|否| N[预生成完成] O[学生开始考试] --> P[查询缓存] P --> Q{缓存命中?} Q -->|是| R[加载预生成记录] R --> S["更新状态为InProgress 设置StartTime"] S --> T["快速启动 ✅"] Q -->|否| U[动态创建记录] U --> V["常规启动 ⚠️"] W["定时清理任务 每天凌晨2点"] --> X[查询已结束考试] X --> Y[查找NotStarted记录] Y --> Z[批量删除] Z --> AA[清理缓存]

2.3 核心组件

2.3.1 预生成服务 (IExamRecordPreGenerationService)

职责

  • 批量预生成考试记录和答题记录
  • 管理预生成缓存
  • 提供缓存键生成方法

关键方法

  • PreGenerateExamRecordsAsync(long examId) - 为指定考试预生成所有学生记录
  • PreGenerateBatchAsync(long examId, IEnumerable<long> studentIds, int attemptNumber) - 批量预生成指定学生记录
  • GetPreGeneratedRecordCacheKey(long examId, long studentId, int attemptNumber) - 生成缓存键

2.3.2 考试记录服务 (ExamRecordService)

职责

  • 智能检测预生成记录
  • 动态创建记录(降级方案)
  • 管理记录状态转换

关键方法

  • CreateExamRecordAsync(long examId, long studentId, ...) - 创建或激活考试记录
    • 优先查询缓存获取预生成记录ID
    • 命中则更新状态和开始时间
    • 未命中则动态创建

2.3.3 任务处理器

定时预生成任务处理器 (ExamRecordScheduledPreGenerationTaskHandler):

  • 定时执行(每天凌晨1点)
  • 查询所有已发布且尚未开始的考试
  • 检查是否已预生成,避免重复处理
  • 分批处理学生列表
  • 记录详细日志

手动预生成任务处理器 (ExamRecordPreGenerationTaskHandler):

  • 用于手动触发单个考试的预生成
  • 接收考试ID参数
  • 分批处理学生列表
  • 记录详细日志

清理任务处理器 (ExamRecordCleanupTaskHandler):

  • 定时执行(每天凌晨2点)
  • 清理已结束考试的未使用记录
  • 同步清理缓存

3. 核心设计要点

3.1 状态管理

3.1.1 考试记录状态扩展

新增 NotStarted = 0 状态,用于标识预生成的记录:

public enum ExamRecordStatus
{
    NotStarted = 0,    // 未开始(预生成状态)
    InProgress = 1,    // 进行中
    Submitted = 2,     // 已提交
    Graded = 3         // 已批改
}

3.1.2 状态转换流程

预生成 → NotStarted
   ↓
开始考试 → InProgress
   ↓
提交考试 → Submitted
   ↓
批改完成 → Graded

3.2 缓存策略

3.2.1 缓存键设计

exam:pregenerated:{examId}:{studentId}:{attemptNumber}

示例

exam:pregenerated:123:456:1

3.2.2 缓存过期时间

核心原则:缓存过期时间 = 考试结束时间 + 1小时缓冲

  • 正常情况:考试结束时间在未来 → 过期时间 = 结束时间 - 当前时间 + 1小时
  • ⚠️ 异常情况:考试已结束或时间异常 → 使用默认7天过期

设计理由

  • 确保考试期间缓存始终有效
  • 考试结束后保留1小时缓冲,处理延迟提交等场景
  • 避免缓存永久占用内存

3.3 分批处理策略

3.3.1 批次大小

  • 默认批次大小:50 名学生/批
  • 可配置:根据系统性能调整(常量 BATCH_SIZE

3.3.2 批次间延迟

  • 延迟时间:200 毫秒(常量 DELAY_BETWEEN_BATCHES_MS
  • 设计目的:限制预生成速度,避免对 CPU 和数据库造成过大压力
  • 执行时机:每处理完一批学生后,延迟 200ms 再处理下一批
  • 性能平衡:在保证预生成效率的同时,不影响系统正常运行

3.3.3 开考前自动停止

  • 停止阈值:开考前 5 分钟(常量 STOP_BEFORE_EXAM_START_MINUTES
  • 检测机制:每批次处理前检查当前时间与考试开始时间的间隔
  • 触发条件:如果距离考试开始时间不足 5 分钟,立即停止预生成
  • 设计目的
    • 避免在考试即将开始时进行大量数据库操作,确保系统资源优先服务于学生开考
    • 减少数据库负载,提升考试开始时的系统响应能力
  • 日志记录:停止时会记录已处理和剩余的学生数量

示例日志

⚠️ 距离考试开始时间不足 5 分钟,停止预生成。已处理: 800/1000,剩余: 200 名学生未处理

3.3.4 处理流程

  1. 获取所有需要预生成的学生ID列表
  2. 按批次大小切分(默认 50 名/批)
  3. 检查开考时间:如果距开考不足 5 分钟,停止预生成
  4. 每批使用事务保证原子性
  5. 批次间延迟 200ms,避免系统压力过大
  6. 记录每批成功/失败数量
  7. 打印详细日志便于跟踪

3.4 容错机制

3.4.1 预生成失败处理

  • 不影响发布流程:预生成任务异步执行,失败不影响考试发布
  • 降级方案:预生成失败的学生,开始考试时自动降级为动态创建
  • 日志记录:详细记录失败原因和学生ID,便于排查

3.4.2 新增学生处理

  • 不预生成:考试发布后新增的学生,不进行预生成
  • 动态创建:新增学生开始考试时,使用动态创建逻辑
  • 性能影响:新增学生比例通常较低(<5%),不影响整体性能

3.5 数据过滤

3.5.1 监控界面过滤

  • 默认排除:监控和管理界面默认排除 NotStarted 状态的记录
  • 可选查询:如需查看预生成记录,可显式指定状态查询

3.5.2 查询优化

-- 默认查询(排除预生成记录)
SELECT * FROM ExamRecord 
WHERE Status != 0  -- NotStarted

-- 显式查询预生成记录
SELECT * FROM ExamRecord 
WHERE Status = 0  -- NotStarted

4. 性能优化

4.1 性能指标

指标 优化前 优化后 提升
开始考试耗时 200-500ms 10-50ms 90%+
数据库写入压力 高峰期集中 分散到发布时 98%
并发支持能力 500+ 1000+ 2倍
缓存命中率 - >85% -

4.2 优化措施

4.2.1 缓存优先查询

  • 开始考试时优先查询缓存,命中则直接加载记录
  • 缓存未命中才查询数据库,减少数据库压力

4.2.2 批量操作

  • 预生成时使用批量插入(AddRangeAsync
  • 清理时使用批量删除(DeleteRangeAsync

4.2.3 事务优化

  • 每批预生成使用独立事务,避免大事务锁表
  • 开始考试时使用分布式锁,防止并发创建

5. 实施要点

5.1 关键时机

5.1.1 预生成触发

  • 触发时机:定时任务(每天凌晨1点)
  • 执行方式:定时任务统一执行,避免影响正在进行的考试
  • 执行范围
    • 仅预生成已发布且尚未开始的考试(Status = Published AND StartTime > 当前时间
    • 仅预生成第一次考试记录(AttemptNumber = 1
    • 自动跳过已预生成的考试,避免重复处理

5.1.2 清理触发

  • 触发时机:定时任务(每天凌晨2点)
  • 清理条件
    • 考试已结束(EndTime < 当前时间
    • 记录状态为 NotStarted
    • 创建时间早于阈值(默认7天前)

5.2 日志记录

5.2.1 定时预生成日志

[INFO] ========================================
[INFO] 考试记录定时预生成任务开始执行
[INFO] ========================================
[INFO] 找到 3 个已发布且尚未开始的考试
[INFO] 考试 123 (数学期末考试) 已预生成,跳过
[INFO] 开始为考试 456 (英语期末考试) 预生成记录
[INFO] 获取到 1000 名学生需要预生成记录
[INFO] 开始分批预生成,每批 50 名学生,共 20 批,批次间延迟 200ms
[INFO] 第 1/20 批完成:成功 50,失败 0
...
[WARN] ⚠️ 距离考试开始时间不足 5 分钟,停止预生成。已处理: 800/1000,剩余: 200 名学生未处理
[INFO] 考试 456 预生成完成 - 成功: 798, 跳过: 200
[INFO] 定时预生成完成 - 总计: 3, 成功: 2, 跳过: 1, 失败: 0
[INFO] ========================================

5.2.2 开始考试日志

[INFO] ✅ 命中预生成记录,快速启动:考试ID=123, 学生ID=456, 记录ID=789
[WARN] ⚠️ 未命中预生成记录,执行动态创建:考试ID=123, 学生ID=999

5.3 配置说明

5.3.1 预生成性能控制参数

以下配置参数在 ExamRecordPreGenerationService.cs 中以常量形式定义:

参数 默认值 说明
BATCH_SIZE 50 每批次处理的学生数量
DELAY_BETWEEN_BATCHES_MS 200ms 批次间延迟时间,避免系统压力过大
STOP_BEFORE_EXAM_START_MINUTES 5分钟 开考前多久停止预生成,确保系统资源优先服务于考试

调整建议

  • 批次大小:根据数据库性能调整,性能较好的系统可增大到 100
  • 批次延迟:如果系统负载高,可增加到 500ms;负载低可减少到 100ms
  • 停止阈值:建议保持 5 分钟,确保考试开始前系统稳定

5.3.2 定时任务配置

{
  "ScheduledTasks": {
    "Tasks": [
      {
        "Id": "exam-record-scheduled-pregeneration",
        "Name": "考试记录定时预生成",
        "Description": "每天凌晨1点为所有已发布且尚未开始的考试预生成记录",
        "Type": "Cron",
        "CronExpression": "0 0 1 * * *",
        "HandlerType": "CodeSpirit.ExamApi.Tasks.ExamRecordScheduledPreGenerationTaskHandler",
        "Timeout": "00:30:00",
        "Enabled": true
      },
      {
        "Id": "exam-record-cleanup",
        "Name": "考试记录垃圾数据清理",
        "Description": "清理未使用的预生成考试记录",
        "HandlerType": "CodeSpirit.ExamApi.Tasks.ExamRecordCleanupTaskHandler",
        "CronExpression": "0 0 2 * * *",
        "Parameters": "{\"cleanupDays\": 7}",
        "Enabled": true
      }
    ]
  }
}

Cron表达式说明

  • 0 0 1 * * * 表示每天凌晨1点执行(预生成任务)
  • 0 0 2 * * * 表示每天凌晨2点执行(清理任务)

6. 注意事项

6.1 数据一致性

  • 题目顺序:预生成时确定题目顺序,避免并发冲突
  • 事务保证:每批预生成使用事务,保证原子性
  • 分布式锁:开始考试时使用分布式锁,防止重复创建

6.2 缓存一致性

  • 写入时机:预生成完成后立即写入缓存
  • 更新时机:开始考试时清除预生成缓存
  • 清理时机:定时清理任务同步清理缓存

6.3 监控建议

  • 📊 预生成成功率:监控预生成任务的成功/失败比例
  • 📊 缓存命中率:监控开始考试时的缓存命中率
  • 📊 清理效果:监控定时清理任务删除的记录数量
  • 📊 性能指标:监控开始考试的响应时间分布
  • 📊 提前停止情况:监控预生成任务是否因接近开考时间而提前停止,如频繁发生应考虑提前发布考试
  • 📊 系统负载:监控预生成过程中的 CPU 和数据库负载,必要时调整批次大小和延迟时间

6.4 扩展性考虑

  • 🔄 多数据库支持:预生成逻辑与数据库类型无关,支持SQL Server和MySQL
  • 🔄 水平扩展:预生成任务可分布式执行,支持多实例部署
  • 🔄 配置化:批次大小、批次延迟、停止阈值等参数可配置,便于调优

6.5 性能调优建议

6.5.1 批次大小调优

  • 小规模考试(<500人):批次大小可设为 100,快速完成预生成
  • 大规模考试(>1000人):保持默认 50,避免单批次耗时过长
  • 超大规模(>5000人):可减小到 30,配合更长的批次延迟(500ms)

6.5.2 发布时机建议

  • 推荐:考试开始前至少 1 天发布,确保在次日凌晨1点完成预生成
  • 最低要求:考试开始前至少 1 小时发布(如果发布时间晚于凌晨1点,预生成将在下一个凌晨1点执行)
  • 注意事项
    • 预生成任务在每天凌晨1点统一执行,不会在发布时立即执行
    • 如果考试在凌晨1点之后发布且当天开考,首批学生会使用动态创建模式(性能略差)
    • 建议提前发布考试,以便享受预生成带来的性能优化

6.5.3 系统负载控制

  • 高负载时段:增加批次延迟到 500ms,减少对正在进行的考试的影响
  • 低负载时段:可减少批次延迟到 100ms,加快预生成速度
  • 监控指标:CPU 使用率 > 70% 或数据库连接数 > 80% 时,应调整参数

7. 测试验证

7.1 功能验证

  1. 预生成功能

    • ✅ 发布考试后检查后台日志,确认预生成任务执行
    • ✅ 查询数据库,验证记录已创建且状态为 NotStarted
    • ✅ 验证缓存中已写入预生成记录ID
  2. 智能启动

    • ✅ 学生开始考试,检查是否命中预生成记录
    • ✅ 测量启动耗时(应降低到 10-50ms)
    • ✅ 验证题目顺序正确
  3. 动态补充

    • ✅ 发布后新增学生到分组
    • ✅ 该学生开始考试,验证动态创建逻辑
  4. 垃圾清理

    • ✅ 等待定时任务执行(或手动触发)
    • ✅ 检查日志和数据库,确认垃圾数据被清理

7.2 性能测试

  • 📈 并发压力测试:模拟1000学生同时开考
  • 📈 对比测试:对比预生成前后的数据库压力和响应时间
  • 📈 缓存命中率测试:统计不同场景下的缓存命中率

8. 总结

考试记录预生成方案通过提前创建缓存优化两大核心策略,显著提升了系统在高并发场景下的性能和用户体验。方案设计充分考虑了容错、扩展性和可维护性,是一个生产级的优化方案。

8.1 核心价值

  • 🚀 性能提升:开始考试耗时降低 90%+
  • 💪 并发优化:数据库压力降低 98%
  • 😊 用户体验:无感知延迟,即刻进入考试
  • 🔒 数据一致性:题目顺序预先确定,避免冲突

8.2 适用场景

  • ✅ 大规模考试(1000+ 学生)
  • ✅ 高并发开考场景
  • ✅ 对响应时间敏感的应用
  • ✅ 需要提升用户体验的场景
posted @ 2026-01-06 23:00  雪雁  阅读(55)  评论(0)    收藏  举报