1. 概述
1.1 背景
在考试系统中,当大量学生同时开始考试时,系统需要为每个学生创建考试记录(ExamRecord)和答题记录(ExamAnswerRecord)。传统的"按需创建"模式在高并发场景下存在以下问题:
•性能瓶颈:每次开始考试都需要执行数据库写入操作,响应时间在 200-500ms
•并发压力:1000+ 学生同时开考时,数据库压力激增,可能导致超时或失败
•用户体验:学生点击"开始考试"后需要等待较长时间才能进入考试界面
1.2 解决方案
考试记录预生成方案通过定时任务(每天凌晨1点)批量预生成所有已发布且尚未开始的考试的记录和答题记录,将数据库写入操作从"考试开始时刻"提前到"凌晨低负载时段",从而:
• ✅性能提升:开始考试耗时从 200-500ms 降低到 10-50ms(命中预生成记录时)
• ✅并发优化:数据库写入压力分散到凌晨低负载时段,避免影响正在进行的考试
• ✅用户体验:学生点击开始后即刻进入考试,无感知延迟
• ✅数据一致性:题目顺序预先确定,避免并发冲突
1.3 核心特性
•定时预生成:每天凌晨1点通过定时任务统一预生成,避免影响正在进行的考试
•智能预生成:仅预生成第一次考试记录(
AttemptNumber = 1),后续考试动态创建•缓存优化:预生成记录写入缓存,开始考试时优先查询缓存,减少数据库查询
•智能检测:开始考试时自动检测预生成记录,命中则快速启动,未命中则动态创建
•垃圾清理:定时任务自动清理未使用的预生成记录,避免数据冗余
•容错机制:预生成失败不影响考试发布,新增学生自动降级为动态创建
2. 架构设计
2.1 系统架构图
CreateExamRecordAsync学生数据库缓存层定时预生成任务ExamSettingServiceExamSettingsController管理员CreateExamRecordAsync学生数据库缓存层定时预生成任务ExamSettingServiceExamSettingsController管理员阶段1:考试发布阶段2:定时预生成(每天凌晨1点)阶段3:学生开始考试alt[缓存命中][缓存未命中(新增学生)]阶段4:定时清理(每天凌晨2点)发布考试PublishExamSettingAsync更新考试状态为Published返回成功查询已发布且尚未开始的考试检查是否已预生成分批创建ExamRecord(NotStarted)批量创建ExamAnswerRecord写入预生成记录ID(过期时间=考试结束时间)打印详细日志点击开始考试查询预生成记录ID加载预生成记录UPDATE状态为InProgress设置StartTime快速启动(10-50ms) ✅动态创建完整记录常规启动(200-500ms) ⚠️查询已结束考试的NotStarted记录批量删除未使用记录清理相关缓存2.2 数据流设计
是 否 是 否 是 否 考试发布 更新状态为Published 等待定时任务执行 定时任务每天凌晨1点 查询已发布且尚未开始的考试 是否已预生成? 跳过该考试 获取学生分组列表 分批处理学生列表 创建ExamRecordStatus=NotStarted 创建ExamAnswerRecord列表 写入缓存Key: exam:pregenerated:examId:studentId:1Value: recordIdExpire: 考试结束时间+1小时 是否还有批次? 预生成完成 学生开始考试 查询缓存 缓存命中? 加载预生成记录 更新状态为InProgress设置StartTime 快速启动 ✅ 动态创建记录 常规启动 ⚠️ 定时清理任务每天凌晨2点 查询已结束考试 查找NotStarted记录 批量删除 清理缓存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 ↓ 批改完成 → Graded3.2 缓存策略
3.2.1 缓存键设计
exam:pregenerated:{examId}:{studentId}:{attemptNumber}示例:
exam:pregenerated:123:456:13.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 -- NotStarted4. 性能优化
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 = PublishedANDStartTime > 当前时间)• 仅预生成第一次考试记录(
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=9995.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+ 学生)
• ✅ 高并发开考场景
• ✅ 对响应时间敏感的应用
• ✅ 需要提升用户体验的场景
📝文档版本:v1.0
📅最后更新:2025年12月
👤维护团队:CodeSpirit 开发团队更多交流请关注“CodeSpirit-码灵”公众号进群!!!