博客 / 詳情

返回

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+ 學生)
  • ✅ 高併發開考場景
  • ✅ 對響應時間敏感的應用
  • ✅ 需要提升用户體驗的場景
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.