外观
在线考试后台服务技术总结
基于三个 ASP.NET Core 后台服务(作答记录脏键落盘、考生状态脏键同步、考试日志队列入库)的代码实现,总结其中涉及的技术点与实践方式。
一、整体架构
三个服务均继承 BackgroundService,作为托管后台服务常驻运行,定期将 Redis 中的热数据批量同步到数据库,实现「先写 Redis、异步落库」的读写分离与削峰填谷策略。
二、核心技术点
1. BackgroundService:托管后台服务
三个服务都继承 Microsoft.Extensions.Hosting.BackgroundService,应用启动后自动运行。
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try { await ProcessAsync(); }
catch (Exception ex) { _logger.LogError(...); }
await Task.Delay(_processingInterval, stoppingToken);
}
}要点:定时轮询、异常兜底、支持 CancellationToken 优雅停止、处理间隔可配置。
2. 脏键(Dirty Keys)同步模式
作答落盘服务与考生状态脏键服务采用同一套脏键落盘流程:
- Rename 原子抢占:
Rename DirtyKeys -> SyncingKeys,将当前脏键集合整体「抢」过来,避免多实例并发处理同一批数据 - Read:
SMembers读取 SyncingKeys 中所有 key - Fetch:根据 key 批量读取 Hash / 缓存数据
- Persist:批量更新数据库
- Delete:删除 SyncingKeys(无论成功失败都删,防止堆积和重复处理)
设计亮点:通过 Redis 原子 Rename 实现无锁并发安全,简单可靠。
3. Redis 技术栈
3.1 数据结构与命令
| 数据结构 | 用途 | 典型命令 |
|---|---|---|
| Set | 脏键集合 | SMembers, Rename, Del, Exists |
| Hash | 作答记录、缓存 | HGetAll, HMGet, HSet, HGet |
| List | 日志队列 | LRange, RPOPLPUSH |
| String | 分布式锁 | SetNx |
3.2 Pipeline 批量读取
作答落盘服务中使用 Pipeline 批量读取 Hash,减少网络往返:
using var pipe = _redis.StartPipe();
foreach (var ki in batch)
pipe.HGetAll(ki.KeyStr);
var results = pipe.EndPipe();3.3 Lua 脚本原子批处理
日志队列入库服务使用 Lua 脚本在 Redis 服务端循环执行 RPOPLPUSH,一次网络往返完成多条数据的迁移:
local results = {}
for i = 1, ARGV[1] do
local item = redis.call('RPOPLPUSH', KEYS[1], KEYS[2])
if item then table.insert(results, item)
else break end
end
return results3.4 分布式锁 SetNx
日志队列入库服务使用 SetNx 实现简单分布式锁,保证多实例部署时仅一个实例消费日志队列:
if (await _redis.SetNxAsync("LogQueue:Lock", Process.GetCurrentProcess().Id, 10))
{ /* 获取锁成功,执行逻辑 */ }4. FreeSql 技术栈
4.1 分表
三个服务均使用 SelectBySplitTable、UpdateBySplitTable、InsertBySplitTable,按考试中心 / 考试编号分表存储,降低单表压力。
4.2 批量写入
- ExecuteSqlBulkCopyAsync:大批量插入时使用 SqlBulkCopy,高效落库
- SetSource + UpdateColumns:批量更新时指定更新列,减少无效更新
- NoneParameter():避免参数化带来的额外开销(按需使用)
4.3 工作单元(Unit of Work)
日志服务通过 CreateUnitOfWork() 管理事务,保证多表批量插入的原子性:
using var unitOfWork = _freeSql.CreateUnitOfWork();
try { /* 批量插入 */ unitOfWork.Commit(); }
catch { unitOfWork.Rollback(); }5. 缓存与配置
5.1 EasyCaching
作答落盘服务使用 IEasyCachingProvider 缓存作答记录字典,避免重复查库。
5.2 IOptionsMonitor
三个服务均通过 IOptionsMonitor 注入配置,支持运行时热更新(如处理间隔、批次大小)。
6. 数据一致性保障
6.1 时间戳比较
考生状态服务通过 LastUpdateTime 判断缓存与数据库新旧,避免旧数据覆盖新数据:
if (state.LastUpdateTime.Value <= dbUnixSeconds) continue; // 跳过,缓存较旧6.2 可靠队列与恢复
日志服务采用 RPOPLPUSH 模式:先从主队列 RPOPLPUSH 到备份队列,处理成功后再删除备份;服务启动时从备份队列恢复未完成数据,避免丢数。
三、整体流程
[业务写入] → Redis (Hash/Set/List)
↓
[BackgroundService] 定时轮询
↓
[脏键/队列] → Rename/Lua 原子操作
↓
[批量读取] → Pipeline / MGet / RPOPLPUSH
↓
[批量落库] → FreeSql 分表 + BulkCopy / SetSource
↓
[数据库] (持久化)四、总结
三个后台服务共同体现了:BackgroundService 托管后台任务、脏键 + Rename 无锁并发同步、Redis 多种数据结构与 Pipeline/Lua 优化、FreeSql 分表与批量落库、EasyCaching / IOptionsMonitor 缓存与配置、以及 RPOPLPUSH 可靠队列、时间戳防覆盖 等数据一致性手段,适用于高并发在线考试场景下的读写分离与异步落库需求。
贡献者
更新日志
2026/2/10 15:01
查看所有更新日志
4ba3e-vault backup: 2026-02-10 15:01:51于a6ec1-vault backup: 2026-02-10 14:54:20于
版权所有
版权归属:hi35401984@aliyun.com
许可证:CC0 1.0 通用 (CC0)
