外观
实战复盘:基于 .NET 8 与 Redis 构建万人并发在线考试系统架构演进
1. 背景与挑战
本项目为一个在线考试系统,核心业务场景具有典型的**“高并发、写多读少、数据一致性要求高”**的特点。
目标容量:支持 10,000 人同时在线考试。
核心痛点:考生答题状态(答案、剩余时间、当前题号)更新极其频繁。如果直接写入 SQL Server,数据库连接池瞬间枯竭,导致系统瘫痪。
技术栈:.NET 8、SQL Server、Redis (FreeRedis)、FreeSql、Vue/React。
2. 核心架构设计:Redis 脏数据标记法 (Dirty Flag Pattern)
为了保护数据库,我们确立了 “Redis 承接所有流量,SQL Server 异步落地” 的核心策略。参考:在线考试后台服务技术总结
2.1 读写分离策略
写操作:考生提交答案时,只写入 Redis(Hash 结构),并将考生 ID 放入一个名为
Exam:Dirty_State_Users的 Redis Set 集合中。读操作:前端轮询或获取状态时,直接读取 Redis。
持久化:后台服务(BackgroundService)定时从
Dirty Set中SPop出一批用户 ID,批量从 Redis 拉取最新状态,通过 FreeSql 批量写入 SQL Server。
2.2 解决乐观锁冲突
在实施过程中,我们遇到了“登录更新时间”与“交卷更新状态”并发导致的乐观锁(Version)冲突问题。
解决方案:
职责分离:登录接口只更新
LastLoginTime和IP。忽略版本检查:在登录更新时,利用 FreeSql
SetSource的参数忽略版本检查,强制覆盖,避免与考试状态更新发生冲突。
// 登录只更新时间,不检查 Version
await _fsql.Update<User>()
.SetSource(user, ignoreVersion: true)
.UpdateColumns(a => new { a.LastLoginTime, a.LoginIp })
.ExecuteAffrowsAsync();3. 性能深度优化:从卡顿到丝滑
在压测初期(k6),系统在 255 并发下 P95 延迟较高。我们进行了以下关键优化:
3.1 基础设施层:负载均衡 (Load Balancing)
现象:低并发下响应极快,高并发下 IIS 队列瞬间爆满,导致请求排队。
对策:
横向扩展:放弃单机 ThreadPool 调优的局限性方案,采用**负载均衡(SLB/Nginx)**策略。
效果:将 10,000 并发流量分摊到多台 IIS 服务器上,每台服务器承担的并发量控制在舒适区(如 2000-3000),彻底解决了 IIS 线程池枯竭和队列拥堵问题。
3.2 中间件层:Redis 客户端优化
策略:
直接使用 FreeRedis:为了获取极致性能和对 Lua 脚本的原生支持,核心业务绕过 EasyCaching 抽象层,直接使用 FreeRedis 客户端。
连接池模式:开启 FreeRedis 的连接池功能(
poolsize=50),避免了单连接多路复用在超高并发下的锁竞争瓶颈。
3.3 代码层:Redis 操作的“原子化”与“批处理”
问题:初期使用管道(Pipeline)进行批量写入。在 .NET 8 异步架构下,同步的 EndPipe() 会阻塞线程。
演进路线:
方案 A(简单批量):对于简单的读写,使用
Task.WhenAll+MSetAsync,利用 FreeRedis 的多路复用特性实现非阻塞并发。方案 B(极致性能):对于“保存答案”这种复杂逻辑(更新Hash、计数、加脏数据集合),采用 Lua 脚本。
Lua 脚本优势:
减少 RTT:将 6-7 次 Redis 网络交互压缩为 1 次。
原子性:保证更新答案和加入脏数据集合是原子操作。
Lua 脚本示例:
Lua
local expireTime = math.floor(tonumber(ARGV[7])) -- 必须取整,防止 Redis 报错
redis.call('HMSET', KEYS[1], 'Answer', ARGV[1], 'Second', ARGV[3] ...)
redis.call('HINCRBY', KEYS[1], ARGV[8], 1)
redis.call('EXPIRE', KEYS[1], expireTime)
redis.call('SADD', KEYS[3], KEYS[1]) -- 加入脏数据集合
return 13.4 数据结构选择
Hash vs String:核心高频数据(如答题状态)坚决使用 Hash,支持
HINCRBY原子计数,且网络流量小。ID 生成算法:引入 Yitter.IdGenerator(雪花算法漂移版),在每台负载均衡服务器上配置不同的
WorkerId,满足 Redis 生成 Key 时的 ID 预分配需求,单机性能达 600万/秒。
4. 压测结果对比 (k6)
经过上述优化,我们在 500 并发(模拟 10,000 人在线考试场景)下的压测数据发生了质变:
| 指标 | 优化前 (255 Users) | 优化后 (500 Users, 持续 5分钟) | 提升幅度 |
|---|---|---|---|
| P95 响应时间 | 1.42s (甚至超时) | 484ms | 速度提升 3倍+ |
| 吞吐量 (RPS) | ~198 reqs/s | ~1500 reqs/s | 吞吐量提升 7.5倍 |
| 错误率 | 偶发超时 | 0.00% | 极端稳定 |
| 单机容量预估 | < 2000 人 | > 15,000 人 | 达到设计目标 |
5. 总结与建议
构建万人级并发系统的关键在于**“分流”和“减少阻塞”**。
架构分流:使用负载均衡解决 IIS 单机瓶颈。
计算下沉:使用 FreeRedis + Lua 脚本,将逻辑下沉到 Redis 端,极大减少网络 IO。
异步化:Web 层只负责“接单”(写 Redis),后台 Worker 负责“做单”(落库),彻底解耦。
架构选型:.NET 8 + FreeSql + FreeRedis (直连) + Yitter.IdGenerator 是目前构建高性能系统的强力组合。
此架构目前已具备支撑 15,000+ QPS 的能力,配合负载均衡(SLB)横向扩展,可轻松应对更大规模的考试场景。
贡献者
更新日志
2026/2/10 15:40
查看所有更新日志
15508-vault backup: 2026-02-10 15:40:46于d55ef-vault backup: 2026-02-10 15:38:51于abf2f-vault backup: 2026-02-10 15:04:20于4ba3e-vault backup: 2026-02-10 15:01:51于7fff7-vault backup: 2026-02-10 09:07:41于18ae4-vault backup: 2026-02-10 08:59:09于
版权所有
版权归属:hi35401984@aliyun.com
许可证:CC0 1.0 通用 (CC0)
