外观
架构决策:当 Redis 达到瓶颈,高并发系统该如何扩展?
1. 核心问题:无状态 vs 有状态
在构建万人级并发系统时,我们通常会遇到两种不同的扩展场景:
Web 服务器(IIS/.NET 8):
特性:无状态(Stateless)。
扩展方案:非常简单。增加服务器数量,挂载 负载均衡(SLB/Nginx) 即可。流量会均匀分发,每台机器处理一部分请求。
Redis 服务器:
特性:有状态(Stateful)。数据存储在内存中。
扩展方案:复杂。不能简单地“加一台 Redis”就解决问题,因为新机器没有旧机器的数据。
当 Redis 达到性能瓶颈(CPU 100%、内存不足或网络带宽打满)时,我们需要做出架构选择。
2. 扩展路线图与选型分析
针对考试系统(高并发写入、依赖 Lua 脚本原子性),我们评估了以下三种扩展模式:
路线一:垂直扩展 (Scale Up)
方案:升级硬件配置(如 2核4G -> 8核16G)。
适用场景:流量增长初期。
局限性:Redis 核心是单线程模型。无论 CPU 核数多少,单机处理能力的物理上限通常在 8万 - 10万 QPS。无法解决无限制的增长。
路线二:主从读写分离 (Master-Replica)
方案:1 Master (写) + N Slaves (读)。
适用场景:“读多写少” 的系统(如微博、新闻)。
针对考试系统的弊端:考试系统的核心压力在于 “提交答案(写)”。主从模式下,写操作依然只能打到唯一的 Master 上,无法解决写瓶颈。
路线三:Redis Cluster 集群 (Sharding) —— “双刃剑”
方案:数据分片。将数据分散存储在多台 Master 上(如 Node A, Node B, Node C)。
优势:理论上无限扩展写能力和内存容量。
⚠️ Cluster 模式的“致命大坑”:Lua 脚本与 Hash Tag
在考试系统中,为了保证数据一致性(如:更新题目状态 + 加入脏数据集合),我们大量使用了 Lua 脚本。
问题:Redis Cluster 要求 一个 Lua 脚本中操作的所有 Key,必须落在同一个节点(Slot)上。
冲突:
Key1:
exam:q:1001(可能分片到 Node A)Key2:
exam:dirty_set(可能分片到 Node B)后果:直接报错
CROSSSLOT Keys in request don't hash to the same slot。
解决方案 (Hash Tag):
必须修改 Key 的命名规范,强制让它们落在一起。
Redis 协议规定:如果 Key 包含
{},则只计算{}内的 Hash。修改前:
exam:q:1001,exam:dirty_set(报错)修改后:
{exam:1001}:q:1,{exam:1001}:dirty_set(可行)
决策:对于现有系统,修改所有 Key 的命名规则成本巨大且风险高。
不用 Lua 是否就能忽略 Hash Tag?(“生死清单”)
有一种说法是:“不用 Lua 脚本的话,就不用在意 Hash Tag 了。”这是**“对了一半”**的结论。
准确地说:如果你彻底不用 Lua 脚本,也不用事务(Transaction/Multi),只用最基础的 Set/Get,那么你确实不用在意 Hash Tag {}。但只要你稍微用一点“高级”功能,Redis 集群(Cluster)的分片机制就会立刻教你做人。
✅ 绝对不用管的情况(安全区)
如果只是把 Redis 当作一个巨大的Dictionary<string, string>来用:SetAsync("user:1", "data")、GetAsync("user:2")完全没问题。- 甚至
Task.WhenAll多 Key 并发:FreeRedis 会自动把请求分发到不同节点,这是集群的并发优势。
❌ 依然会报错的情况(雷区)
即使不用 Lua,以下原生命令也要求所有 Key 必须在同一节点,否则会抛出CROSSSLOT:- 集合运算:
SDIFF、SINTER、SUNION。例如SInterAsync("class:1", "class:2")若两个 Key 在不同节点会直接报错。 - 列表移动:
RPopLPush(从一队列弹出塞入另一队列)。两队列不在同一台机器会报错。 - 重命名:
Rename。若temp_key与real_key的 Slot 不同会报错。
- 集合运算:
⚠️ 性能与原子性变差(MSet / MGet)
在集群下无 Hash Tag 时:MGetAsync("key1", "key2", "key3")会被拆成多个 GET 发到不同节点,网络 RTT 增加。- 失去原子性:可能 key1 读到、key2 在微秒级时间差内被删,结果不一致。
回到考试系统
若放弃 Lua,改用 C# 逻辑(如先SetAsync("exam:q:1", "Answer")再SAddAsync("dirty_set", "exam:q:1")):- 两步可能落在不同节点,变成非原子:第一步成功、第二步失败时,答案已写入但脏集合未记录,该考生数据可能永远不会同步到 SQL,相当于“白考了”。
| 你的选择 | 结果 | 评价 |
|---|---|---|
| 集群 + Lua | 必须用 Hash Tag | 否则报错。 |
| 集群 + 无 Lua | 部分命令报错 | SINTER、RPOPLPUSH 等多 Key 命令不能用。 |
| 集群 + 无 Lua + Task.WhenAll | 可以运行,但不安全 | 失去原子性,可能出现“写了数据但没同步”的一致性问题。 |
| 哨兵模式 (推荐) | 随便写 | Lua 随便用,Key 随便起,原子性有保障。 |
既然核心业务(保存答案)对数据一致性要求极高,原子性是底线:要么在集群里用 {} 配合 Lua,要么直接用哨兵模式。在不想改 Key 命名规则的前提下,哨兵模式依然是唯一真神。
3. 最终推荐方案:Redis Sentinel (哨兵模式)
结合目前 15,000 QPS 的实际压测数据(远未达到单机 10万 QPS 的瓶颈),我们不需要强上 Cluster,而是应该关注 “高可用性 (High Availability)”。
3.1 什么是哨兵模式?
哨兵是 Redis 的“自动故障转移管理员”。
监控:实时监控 Master 是否存活。
自动切换:如果 Master 宕机,哨兵会自动将一个 Slave 提升为 Master。
通知客户端:FreeRedis 客户端会自动感知 Master IP 的变化并重连。
3.2 为什么它是最佳选择?
解决单点故障:避免 Redis 宕机导致考试中断。
完全兼容 Lua:本质上还是单机写(Master),所有 Key 都在一台机器上。无需修改任何 Key 命名,无需 Hash Tag,现有代码直接运行。
性能足够:单机 Master 足以支撑 5万+ 人同时在线考试。
3.3 哨兵模式需要注意的两点
- 故障切换时的“复制窗口”:Master 宕机时,尚未同步到 Slave 的写可能丢失(通常只有毫秒级)。对“交卷即落库、允许极少量在切换瞬间重试”的考试场景一般可接受;若业务要求零丢数,需在应用层做写确认或补偿(如幂等重试、对账)。
- 监控与可观测性:建议对 Redis 做基础监控(连接数、内存、延迟、主从角色),并在告警里区分“主从切换”与“真正的异常”。这样切换发生后能快速确认客户端已连到新 Master,避免误判。
4. 实施指南:自建 vs 云服务
方案 A:自建 (Self-Hosted)
配置:需要至少 3 台机器部署哨兵进程,配置
sentinel.conf。代码:C# 连接字符串需要列出所有哨兵 IP (
192.168.1.x:26379,... serviceName=mymaster)。风险:运维复杂,容易配置错误导致脑裂。
方案 B:购买云服务 (推荐)
产品:阿里云/腾讯云/Azure 的 “Redis 高可用版 / 主从版”。
优势:
自带 VIP (虚拟IP) 或 Proxy。
对代码透明:连接字符串看起来像单机一样简单,云厂商在后台处理故障切换。
SLA 保障:比自建更稳定。
高并发下的客户端注意:万人并发时,应用层需合理配置连接池大小、超时与重试(如 FreeRedis 的
PoolSize、ConnectTimeout),避免连接打满或长时间阻塞,形成“Redis 没挂但客户端先挂”的隐性单点。
5. 总结架构演进策略
| 阶段 | 流量规模 | 推荐架构 | 关键动作 |
|---|---|---|---|
| 阶段一 (当前) | < 10,000 并发 QPS < 50,000 | Redis 哨兵模式 (Sentinel) | 1. 购买云数据库高可用版。 2. 保持 Lua 脚本原子性。 3. 无需修改 Key 命名。 |
| 阶段二 (未来) | > 50,000 并发 QPS > 100,000 | Redis 集群模式 (Cluster) | 1. 必须重构 Key 命名,引入 {Hash Tag}。2. 确保 Lua 脚本内的 Key 都在同一 Slot。 |
若未来真的上 Cluster:可提前在现有哨兵环境下按 {exam:xxx}:... 规范新 Key,Lua 和业务逻辑在单 Master 上先跑通,再迁集群,能减少一次性改造成本和风险。
最终结论:
对于当前的万人并发考试系统,不需要为了“未来可能达到的瓶颈”去引入复杂的 Cluster 和 Hash Tag。Redis 哨兵模式 (Sentinel) 是兼顾稳定性、开发成本和性能的最优解。
贡献者
更新日志
2026/2/11 13:59
查看所有更新日志
c6744-vault backup: 2026-02-11 13:59:39于68db1-vault backup: 2026-02-11 13:55:09于f6b1e-vault backup: 2026-02-11 12:19:25于b5c3b-vault backup: 2026-02-11 12:17:09于70d12-vault backup: 2026-02-11 12:14:26于3f4ad-vault backup: 2026-02-11 12:13:15于
版权所有
版权归属:hi35401984@aliyun.com
许可证:CC0 1.0 通用 (CC0)
