Skip to content

集群架构与高可用深度解析

面试官:Redis 的高可用方案有哪些?哨兵和 Cluster 的区别是什么?

:Redis 有三种高可用方案:主从复制(手动切换)、哨兵模式(自动故障转移)、Cluster 模式(分片 + 高可用)。哨兵适合数据量不大但需要高可用的场景,Cluster 适合大数据量或高 QPS 需要水平扩展的场景。

面试官:那哨兵是怎么判断主库宕机的?为什么区分主观下线和客观下线?

这个追问考察是否理解哨兵的核心机制。能说清 SDOWN 和 ODOWN 的区别及原因的,证明真正掌握了哨兵原理。


Q1:Redis 主从复制的原理是什么?全量和增量的区别?必考

Section titled “Q1:Redis 主从复制的原理是什么?全量和增量的区别?”

回答要点

复制流程

从库启动,向主库发送 PSYNC 命令
├─ 全量复制(首次连接或断线时间过长)
│ │
│ ├─ 主库执行 BGSAVE,生成 RDB
│ ├─ 主库将 RDB 发送给从库
│ ├─ 从库清空本地数据,加载 RDB
│ └─ 主库将 RDB 生成期间的写命令发给从库
└─ 增量复制(断线重连,断线时间短)
├─ 从库发送 PSYNC <replid> <offset>
├─ 主库从 repl_backlog 中找到 offset 之后的命令
└─ 主库发送增量命令给从库

全量复制流程

T1: 从库发送 PSYNC ? -1(首次连接)
T2: 主库执行 BGSAVE,fork 子进程生成 RDB
├─ 子进程遍历内存,写 RDB 文件
└─ 主库继续接收写请求,写入 repl_backlog 缓冲区
T3: 主库发送 RDB 文件给从库(网络传输)
T4: 从库加载 RDB(清空本地数据)
T5: 主库发送 repl_backlog 中的增量命令
T6: 从库执行增量命令,数据同步完成

增量复制流程

T1: 从库断线重连,发送 PSYNC <replid> <offset>
T2: 主库检查 replid 是否匹配
├─ 匹配 → 检查 offset 是否在 repl_backlog 范围内
│ ├─ 在范围内 → 增量复制(发送 offset 之后的命令)
│ └─ 不在范围内 → 全量复制(repl_backlog 已被覆盖)
└─ 不匹配 → 全量复制(主库切换了)

repl_backlog 的作用

repl_backlog 是主库维护的环形缓冲区:
- 默认大小:1MB
- 存储内容:最近的写命令(如 SET、INCR)
- 作用:支持断线重连后的增量复制
问题:如果从库断线时间过长,repl_backlog 被覆盖
→ 只能全量复制(开销极大)
优化:调整 repl_backlog_size
# 估算:断线时间(秒)× 主库写入速度(bytes/s)
# 例如:允许断线 60s,写入速度 10MB/s → 至少 600MB
repl-backlog-size 600mb

PSYNC2 协议(Redis 4.0+)

旧版 PSYNC 的问题:
主库切换后,从库必须全量复制(因为 run_id 变了)
PSYNC2 的改进:
主库维护两个 replid:
- replid:当前主库的复制 ID
- replid2:前一个主库的复制 ID(保留给故障切换场景)
示例:
T0: 主库 A(replid=ID_A)宕机
T1: 从库 B 提升为新主库(replid=ID_B, replid2=ID_A)
T2: 从库 C(原主库 A 的从库)重连主库 B
T3: 从库 C 发送 PSYNC ID_A offset
T4: 主库 B 识别 ID_A 是自己的 replid2
T5: 主库 B 找到 offset 对应的命令,增量复制 ✅

性能数据

测试场景:主库 10GB 数据,从库首次同步
- 全量复制:
- BGSAVE 耗时:约 200ms(fork + 生成 RDB)
- RDB 传输耗时:约 10 秒(1GB/s 网络)
- 从库加载 RDB:约 20 秒
- 总耗时:约 30 秒
- 增量复制(断线 10 秒):
- 发送增量命令:约 1 秒
- 总耗时:约 1 秒
结论:增量复制比全量复制快 30 倍,生产环境应尽量避免全量复制。

本质一句话:主从复制分全量(首次或断线过长)和增量(断线重连),增量依赖 repl_backlog,异步复制存在延迟。


Q2:主从复制延迟会导致什么问题?如何监控?高频

Section titled “Q2:主从复制延迟会导致什么问题?如何监控?”

回答要点

主从延迟的产生

主从异步复制:
主库写成功 → 立即返回客户端 → 异步发给从库
→ 从库数据落后主库
延迟原因:
1. 网络延迟:主从跨机房部署
2. 从库性能差:从库处理慢(如正在进行全量同步)
3. 主库写入速度过快:从库追不上
4. 主库执行慢命令:如大 key 操作,阻塞主线程

延迟导致的问题

问题1:读写分离场景,读到旧数据
客户端 A 写入主库(key=100)
客户端 B 立即读从库(读到旧值 key=50)
→ 数据不一致
问题2:主库宕机,从库提升为主库,丢失数据
主库写入 100 条数据,未同步到从库
主库宕机,从库提升为主库
→ 丢失 100 条数据
问题3:缓存场景,缓存穿透
主库写入 key=100,删除缓存
从库还未同步,客户端读从库未命中
→ 穿透到数据库

监控延迟

Terminal window
# 方法1:INFO replication
redis-cli INFO replication
# master_repl_offset:10000 主库偏移量
# slave0:offset=9500 从库偏移量
# 延迟 = 10000 - 9500 = 500 条命令
# 方法2:LATENCY LATEST
redis-cli LATENCY LATEST
# 查看命令延迟分布
# 方法3:监控工具(Redis Exporter + Prometheus + Grafana)
# 指标:redis_master_link_lag_seconds(主从延迟秒数)

解决方案

方案1:读主库(强一致场景)
对数据一致性要求高的读请求,直接读主库
缺点:增加主库压力
方案2:等待从库同步(Redis WAIT 命令)
SET key value
WAIT 1 1000 # 等待至少 1 个从库确认,超时 1000ms
缺点:增加写延迟
方案3:监控告警
监控主从延迟,超过阈值告警
自动切换读请求到主库
方案4:优化主从性能
- 减少慢查询(避免大 key)
- 提升网络带宽
- 提升从库硬件配置

WAIT 命令示例

# 写入并等待从库确认
def write_with_wait(key, value):
redis.set(key, value)
# 等待至少 1 个从库确认,超时 1000ms
replicas = redis.wait(1, 1000)
if replicas < 1:
raise Exception("No replica confirmed")
# 性能影响:
# - 无 WAIT:写入耗时约 0.1ms
# - WAIT 1 1000:写入耗时约 2ms(等待从库 ACK)
# - 增加 20 倍延迟

本质一句话:主从异步复制存在延迟,可通过 INFO replication 监控,强一致场景用读主库或 WAIT 命令。


Q3:哨兵是怎么判断主库宕机的?为什么区分主观下线和客观下线?必考

Section titled “Q3:哨兵是怎么判断主库宕机的?为什么区分主观下线和客观下线?”

回答要点

哨兵架构

┌──────────┬──────────┬──────────┐
│ 哨兵 1 │ 哨兵 2 │ 哨兵 3 │ ← 哨兵集群(奇数个,通常 3 个)
└─────┬────┴────┬─────┴────┬─────┘
│ │ │
└─────────┼──────────┘
│ 监控
┌─────┴─────┐
│ 主库 │
└─────┬─────┘
┌─────┴─────┐
│ 从库 1 │ 从库 2
└───────────┘

主观下线(SDOWN)

流程:
哨兵定期(每秒)ping 主库
若超过 down-after-milliseconds(默认 30s)无响应
→ 该哨兵判定主库"主观下线"
特点:
- 单个哨兵的判断
- 可能误判(哨兵和主库之间网络故障,但主库实际正常)

客观下线(ODOWN)

流程:
哨兵 A 判定主库主观下线
→ 向其他哨兵询问:你觉得主库下线了吗?
若超过 quorum(法定人数,通常 2)个哨兵都认为下线
→ 主库"客观下线",触发故障转移
示例:
3 个哨兵,quorum = 2
哨兵 A 认为主库下线
哨兵 B 认为主库下线
哨兵 C 认为主库正常
→ 2/3 认为 SDOWN → ODOWN ✅

为什么区分 SDOWN 和 ODOWN?

场景:哨兵 A 与主库之间网络故障
只有 SDOWN:
哨兵 A 误判主库下线
→ 触发故障转移
→ 将从库提升为主库
→ 但原主库实际正常,仍在服务
→ 出现两个主库(脑裂)❌
SDOWN + ODOWN:
哨兵 A 误判主库下线(SDOWN)
→ 询问其他哨兵
→ 哨兵 B、C 认为主库正常
→ 不满足 quorum,不触发故障转移 ✅
→ 避免误判

故障转移流程

T1: 主库客观下线(ODOWN)
T2: 哨兵 Leader 选举(Raft 协议)
├─ 多个哨兵竞争成为 Leader
└─ Leader 负责执行故障转移
T3: Leader 选择新主库
├─ 过滤掉与主库断线时间过长的从库
├─ 优先选 slave-priority 最高的
├─ 优先选复制偏移量最大的(数据最新)
└─ 相同条件下选 run_id 最小的
T4: Leader 执行故障转移
├─ 新主库执行 SLAVEOF NO ONE(提升为主库)
├─ 通知其他从库 REPLICAOF 新主库
└─ 通知客户端更新连接地址

哨兵配置示例

Terminal window
# sentinel.conf
port 26379
sentinel monitor mymaster 192.168.1.1 6379 2 # 监控主库,quorum=2
sentinel down-after-milliseconds mymaster 30000 # 30s 无响应判定 SDOWN
sentinel failover-timeout mymaster 180000 # 故障转移超时 180s
sentinel parallel-syncs mymaster 1 # 同时同步的从库数量

性能数据

测试场景:主库宕机,哨兵自动故障转移
- 主库宕机检测:30s(down-after-milliseconds)
- 哨兵 Leader 选举:约 5s
- 选择新主库:约 1s
- 新主库提升:约 2s
- 从库切换:约 5s
- 总耗时:约 43s
优化:
- 减少 down-after-milliseconds 到 10s → 总耗时约 23s
- 缺点:可能增加误判率

本质一句话:SDOWN 是单哨兵判断,可能误判;ODOWN 是多哨兵投票,降低误判率,触发故障转移。


Q4:哨兵选举新主库的原则是什么?高频

Section titled “Q4:哨兵选举新主库的原则是什么?”

回答要点

选举原则

1. 过滤掉不健康的从库:
- 与主库断线时间过长(超过 down-after-milliseconds × 10)
- 最近未响应哨兵的 ping
2. 选择 slave-priority 最高的:
- slave-priority 是从库配置的优先级(默认 100)
- 值越小,优先级越高
- 可以手动调整优先级
3. 选择复制偏移量最大的:
- 复制偏移量(repl_offset)越大,数据越新
- 越接近主库的 repl_offset,数据越完整
4. 选择 run_id 最小的:
- 如果以上条件都相同,选 run_id 最小的(随机选择)

示例

从库列表:
从库 A:slave-priority=100, offset=10000, run_id="abc"
从库 B:slave-priority=50, offset=9500, run_id="def"
从库 C:slave-priority=100, offset=10000, run_id="ghi"
选举过程:
1. 过滤:无
2. 比较 slave-priority:
- 从库 B 优先级最高(50 < 100)→ 选从库 B ✅
如果从库 B 被过滤:
1. 比较 offset:
- 从库 A 和 C offset 最大(10000)
2. 比较 run_id:
- 从库 A run_id 更小("abc" < "ghi")→ 选从库 A ✅

配置示例

Terminal window
# 从库配置
slave-priority 50 # 优先级,值越小优先级越高
# 查看从库信息
INFO replication
# slave_priority:50
# master_repl_offset:10000

手动提升从库优先级

场景:某个从库硬件配置更好,希望优先提升为主库
配置:
slave-priority 10 # 高优先级
结果:
主库宕机时,优先选择该从库作为新主库

本质一句话:选举原则是”过滤不健康 → 优先级最高 → 偏移量最大 → run_id 最小”,slave-priority 允许手动控制。


Q5:Redis Cluster 的 16384 个 slot 是怎么分配的?为什么是 16384 而不是 65536?必考

Section titled “Q5:Redis Cluster 的 16384 个 slot 是怎么分配的?为什么是 16384 而不是 65536?”

回答要点

slot 分配规则

Redis Cluster 将所有数据分为 16384 个 slot(哈希槽)
key → CRC16(key) % 16384 → slot 编号 → 该 slot 所在的节点
示例(3 主 3 从):
节点 A:slot 0 ~ 5460
节点 B:slot 5461 ~ 10922
节点 C:slot 10923 ~ 16383
客户端请求:
GET user:1001
→ CRC16("user:1001") % 16384 = 8192
→ slot 8192 在节点 B
→ 向节点 B 发送 GET user:1001

为什么是 16384 而不是 65536?

原因:心跳包大小
Cluster 节点之间通过 Gossip 协议通信:
- 每秒随机选几个节点发送 PING/PONG
- 消息中包含一个 bitmap,表示该节点负责哪些 slot
16384 slot 的 bitmap:
- 大小 = 16384 / 8 = 2048 字节 = 2KB
- 心跳包大小可接受 ✅
65536 slot 的 bitmap:
- 大小 = 65536 / 8 = 8192 字节 = 8KB
- 心跳包太大,网络开销高 ❌
实际需求:
- Redis Cluster 推荐节点数 ≤ 1000
- 16384 个 slot,每个节点平均 16 个 slot,完全够用

CRC16 算法

import crc16
def get_slot(key):
# 计算 CRC16
crc = crc16.crc16xmodem(key.encode())
# 取模 16384
return crc % 16384
# 示例
get_slot("user:1001") # 返回 8192
get_slot("order:2001") # 返回 12000

MOVED 重定向

客户端请求:
客户端向节点 A 发送 GET user:1001
节点 A 检查:
- CRC16("user:1001") % 16384 = 8192
- slot 8192 不在节点 A,在节点 B
节点 A 返回:
MOVED 8192 192.168.1.2:6379 ← 重定向到节点 B
客户端更新路由表:
下次直接向节点 B 发送请求

性能数据

测试场景:3 主 3 从 Cluster,写入 100 万个 key
- 无 MOVED(客户端路由表正确):QPS 约 10 万
- 有 MOVED(客户端路由表错误):QPS 约 5 万(多一次网络往返)
结论:
客户端应维护正确的路由表,减少 MOVED 重定向。

本质一句话:16384 slot 是权衡心跳包大小和节点数量的结果,CRC16 计算哈希,MOVED 重定向引导客户端。


Q6:MOVED 和 ASK 重定向的区别是什么?slot 迁移期间如何保证数据可用?高频

Section titled “Q6:MOVED 和 ASK 重定向的区别是什么?slot 迁移期间如何保证数据可用?”

回答要点

MOVED 重定向(永久)

场景:slot 已稳定分配给目标节点
流程:
客户端向节点 A 请求 GET user:1001
→ 节点 A 发现 slot 8192 在节点 B
→ 返回 MOVED 8192 192.168.1.2:6379
→ 客户端更新路由表,下次直接访问节点 B

ASK 重定向(临时)

场景:slot 正在从节点 A 迁移到节点 B
流程:
客户端向节点 A 请求 GET user:1001
情况1:key 还在节点 A
→ 节点 A 返回数据 ✅
情况2:key 已迁移到节点 B
→ 节点 A 返回 ASK 8192 192.168.1.2:6379
→ 客户端向节点 B 发送 ASKING + GET user:1001
→ 节点 B 返回数据 ✅
→ 客户端不更新路由表(临时重定向)

MOVED vs ASK 对比

维度MOVEDASK
触发场景slot 稳定分配slot 迁移中
客户端行为更新路由表不更新路由表
下次请求直接访问新节点仍先访问原节点
持续时间永久临时(迁移期间)

slot 迁移流程

T1: 管理员发起迁移命令
CLUSTER SETSLOT 8192 IMPORTING 192.168.1.2:6379 (节点 B)
CLUSTER SETSLOT 8192 MIGRATING 192.168.1.2:6379 (节点 A)
T2: 节点 A 逐个迁移 key
MIGRATE 192.168.1.2 6379 user:1001 0 5000
T3: 迁移期间,客户端请求
- key 在节点 A → 正常返回
- key 在节点 B → ASK 重定向
T4: 迁移完成,更新 slot 分配
CLUSTER SETSLOT 8192 NODE <node_B_id>
T5: 后续请求
- 客户端收到 MOVED,更新路由表
- 直接访问节点 B

ASKING 命令的作用

节点 B 在迁移期间的行为:
- 正常情况:只处理属于自己的 slot
- ASKING 后:临时处理正在导入的 slot
客户端请求流程:
GET user:1001 → 节点 A
→ ASK 8192 节点 B
→ ASKING(告诉节点 B:临时处理 slot 8192)
→ GET user:1001
→ 节点 B 返回数据

性能影响

测试场景:slot 迁移 100 万个 key
- 无迁移:GET 耗时约 0.1ms
- 迁移中(ASK 重定向):GET 耗时约 0.3ms(多一次网络往返)
- 迁移完成(MOVED):GET 耗时约 0.1ms(客户端已更新路由)
结论:
slot 迁移对客户端透明,只增加一次网络往返,不影响数据可用性。

本质一句话:MOVED 是永久重定向,客户端更新路由表;ASK 是临时重定向,迁移期间保证数据可用。


Q7:什么是脑裂(Split Brain)?min-replicas-to-write 如何缓解?高频

Section titled “Q7:什么是脑裂(Split Brain)?min-replicas-to-write 如何缓解?”

回答要点

脑裂现象

正常:
主库 ─── 从库 1
└── 从库 2
脑裂(网络分区):
主库(网络隔离)
从库 1 ─── 从库 2 ─── 哨兵
哨兵认为主库下线
从库 1 提升为新主库
结果:
旧主库和新主库同时接受写入!
→ 网络恢复后,旧主库变为从库,数据被覆盖(数据丢失)

数据丢失过程

T0: 主库(网络隔离前)正常服务
T1: 网络分区,主库与从库、哨兵断开
T2: 哨兵将从库 1 提升为新主库
T3: 旧主库继续接受写入(网络隔离,不知道已被切换)
T4: 新主库接受写入
T5: 网络恢复,哨兵将旧主库降级为从库
T6: 旧主库执行全量同步,T3 期间的写入丢失 ❌

min-replicas-to-write 配置

Terminal window
# redis.conf
min-replicas-to-write 1 # 至少 1 个从库确认才返回写成功
min-replicas-max-lag 10 # 从库最大延迟 10 秒

缓解原理

网络分区后:
主库失去从库连接
→ 满足不了 min-replicas-to-write = 1
→ 主库拒绝所有写命令
→ 客户端感知到写失败,不会在旧主库继续写入
流程:
T1: 网络分区,主库失去从库
T2: 主库拒绝写入(ERROR: Can't write to master with less than 1 replicas)
T3: 哨兵将从库提升为新主库
T4: 网络恢复,旧主库降级为从库
T5: 旧主库 T1~T4 期间无写入,数据丢失减少 ✅

局限性

局限1:不能完全避免
网络分区的瞬间,在主库拒绝写入之前,可能已有少量写命令成功
(从库 ACK 已到,但随后失联)
局限2:降低可用性
正常的从库重启或临时延迟也可能触发主库拒绝写入
→ 短暂不可用
局限3:不适用于 Cluster
Cluster 有自己的分区处理机制,不用哨兵

性能影响

测试场景:主从架构,min-replicas-to-write = 1
- 无配置:写入 QPS 约 10 万,数据丢失风险高
- 配置后:写入 QPS 约 9 万(等待从库 ACK),数据丢失风险低
结论:
min-replicas-to-write 增加写延迟约 10%,但显著减少脑裂期间的数据丢失。

本质一句话:脑裂时新旧主库同时写入,min-replicas-to-write 要求主库至少有 N 个从库才接受写入,减少数据丢失。


Q8:什么是大 key?有什么危害?如何发现和处理?必考

Section titled “Q8:什么是大 key?有什么危害?如何发现和处理?”

回答要点

大 key 定义

String 类型:value > 10KB
Hash/List/Set/ZSet 类型:元素数 > 5000

大 key 的危害

危害1:网络传输慢
GET big_key (10MB) → 传输耗时约 100ms(100Mbps 网络)
→ 客户端超时或阻塞
危害2:内存分配/释放耗时,主线程阻塞
DEL big_key → 同步释放 10MB 内存,耗时约 50ms
→ 主线程阻塞,影响其他请求
危害3:集群数据倾斜
big_key 在节点 A,节点 A 内存占用远高于其他节点
→ 节点 A 提前触发淘汰或 OOM

发现大 key

Terminal window
# 方法1:redis-cli --bigkeys(推荐)
redis-cli --bigkeys
# -------- summary -------
# Biggest string found 'user:session:1001' has 1048576 bytes (1MB)
# Biggest hash found 'user:profile:1001' has 10000 fields
# 方法2:redis-cli --memkeys(Redis 7.0+)
redis-cli --memkeys
# 按内存占用排序
# 方法3:MEMORY USAGE 命令
redis-cli MEMORY USAGE user:profile:1001
# (integer) 1048576 (1MB)
# 方法4:RDB 分析工具
redis-rdb-tools --command memory dump.rdb > memory.csv

删除大 key(正确姿势)

Terminal window
# ❌ 错误:用 DEL(同步删除,阻塞主线程)
DEL big_key
# 主线程阻塞数百毫秒
# ✅ 正确:用 UNLINK(异步删除)
UNLINK big_key
# 立即返回,后台线程清理
# ✅ 正确:对于 Hash/List/Set,分批删除
# Hash 示例
HSCAN big_hash 0 COUNT 100 # 每次扫描 100 个字段
HDEL big_hash field1 field2 ... field100 # 分批删除
UNLINK big_hash # 删除空 key
# List 示例
LTRIM big_list 100 -1 # 删除前 100 个元素
# 重复直到列表为空
UNLINK big_list

预防大 key

# 方案1:拆分大 Hash
# 原来:hash user:1001 有 10000 个字段
HSET user:1001 name "Alice" age 30 email "alice@example.com" ...
# 拆分后:按字段分组
HSET user:1001:profile name "Alice" age 30
HSET user:1001:settings theme "dark" lang "zh"
HSET user:1001:stats login_count 100
# 方案2:压缩大 String
import gzip
html = "<html>...</html>" # 1MB
compressed = gzip.compress(html.encode()) # 压缩到 100KB
redis.set("cache:page:1001", compressed)
# 方案3:分块存储大 List
# 原来:list logs 有 100 万条日志
LPUSH logs "log1" "log2" ...
# 分块后:按日期分块
LPUSH logs:20240301 "log1" "log2" ...
LPUSH logs:20240302 "log3" "log4" ...

性能对比

测试场景:删除 10MB 的 String key
- DEL:主线程阻塞约 100ms
- UNLINK:立即返回,后台清理约 50ms
测试场景:删除 10000 字段的 Hash
- DEL:主线程阻塞约 200ms
- UNLINK:立即返回,后台清理约 150ms
- 分批删除(每次删 100 字段):主线程阻塞约 2ms × 100 次

本质一句话:大 key 用 --bigkeysMEMORY USAGE 发现,用 UNLINK 异步删除,用拆分、压缩、分块等方式预防。


Q9:什么是热 key?有什么危害?如何解决?高频

Section titled “Q9:什么是热 key?有什么危害?如何解决?”

回答要点

热 key 现象

定义:单个 key 的 QPS 过高(如某个爆款商品,每秒访问百万次),超过单节点处理能力
现象:
爆款商品 key:item:1001
QPS:100 万/秒
单 Redis 节点 QPS 上限:约 10 万/秒
→ Redis 节点过载,响应慢或崩溃 ❌

热 key 的危害

危害1:单节点过载
热 key 在节点 A,节点 A QPS 飙升到 100 万
→ 节点 A 过载,其他节点空闲
→ 资源利用率不均
危害2:影响其他请求
节点 A 处理热 key 请求,CPU 飙升
→ 节点 A 上的其他请求响应慢
危害3:集群倾斜
Cluster 模式下,热 key 所在节点压力过大
→ 该节点成为性能瓶颈

解决方案

方案1:本地缓存(推荐)
在应用服务器内存中缓存热 key(Caffeine/Guava)
大部分请求在本地解决,不到达 Redis
示例:
应用服务器 100 台,每台缓存热 key
热 key QPS = 100 万
→ 每台应用服务器承担 1 万 QPS(本地缓存)
→ Redis QPS 降低到 1000(只缓存失效时查 Redis)
方案2:读副本分散
将热 key 复制到多个副本节点
客户端随机选择副本读取
示例:
Redis Cluster:1 主 2 从
热 key 在主库,QPS = 100 万
→ 客户端随机读主库或从库
→ QPS 分散到 3 个节点,每个节点 33 万
方案3:key 打散
将 hot_key 变成 hot_key:1、hot_key:2...hot_key:N
写入时随机选一个,读取时随机选一个
示例:
写入:随机选择 hot_key:3
SET hot_key:3 "value"
读取:随机选择 hot_key:7
GET hot_key:7
→ 如果不存在,尝试其他 key 或查 DB
→ QPS 分散到多个 key

本地缓存示例

from cachetools import TTLCache
# 本地缓存,TTL = 60 秒,最多缓存 1000 个 key
local_cache = TTLCache(maxsize=1000, ttl=60)
def get_item(item_id):
# 先查本地缓存
if item_id in local_cache:
return local_cache[item_id]
# 查 Redis
item = redis.get(f"item:{item_id}")
if item:
local_cache[item_id] = item # 写入本地缓存
return item
# 性能:
# 本地缓存命中率 95% → Redis QPS 降低 20 倍

key 打散示例

import random
N = 10 # 打散成 10 个 key
def set_hot_key(value):
# 写入时随机选一个 key
idx = random.randint(1, N)
redis.set(f"hot_key:{idx}", value)
def get_hot_key():
# 读取时随机选一个 key
idx = random.randint(1, N)
value = redis.get(f"hot_key:{idx}")
if value:
return value
# 如果未命中,尝试其他 key 或查 DB
for i in range(1, N + 1):
value = redis.get(f"hot_key:{i}")
if value:
return value
return db.query("hot_key")
# 性能:
# 热 key QPS = 100 万
# 打散成 10 个 key → 每个 key QPS = 10 万
# 单节点可承受

性能对比

测试场景:热 key QPS = 100 万
- 无优化:Redis 节点过载,响应慢(P99 > 100ms)
- 本地缓存(命中率 95%):Redis QPS = 5 万,响应快(P99 < 10ms)
- 读副本分散(3 副本):Redis QPS = 33 万/节点,响应快(P99 < 20ms)
- key 打散(10 个 key):Redis QPS = 10 万/key,响应快(P99 < 10ms)

本质一句话:热 key 用本地缓存、读副本分散、key 打散解决,核心是将 QPS 分散到多个节点或本地缓存。


  1. 主从复制:全量(BGSAVE + RDB)和增量(repl_backlog),异步复制存在延迟
  2. 哨兵模式:SDOWN(单哨兵判断)+ ODOWN(多哨兵投票),自动故障转移
  3. Redis Cluster:16384 slot 分片,MOVED/ASK 重定向,Gossip 协议通信
  4. 脑裂问题:min-replicas-to-write 要求主库有 N 个从库才接受写入,减少数据丢失
  5. 大 key:UNLINK 异步删除,拆分 Hash,压缩 String,分块 List
  6. 热 key:本地缓存、读副本分散、key 打散
  • <Badge text="必考" variant="danger" /> 主从复制的原理?全量和增量的区别?
  • <Badge text="必考" variant="danger" /> 哨兵和 Cluster 的区别?各适用什么场景?
  • <Badge text="必考" variant="danger" /> Redis Cluster 的 slot 分配原理?为什么是 16384?
  • <Badge text="必考" variant="danger" /> 什么是大 key?如何发现和处理?
  • <Badge text="高频" variant="tip" /> 哨兵如何判断主库宕机?为什么区分 SDOWN 和 ODOWN?
  • <Badge text="高频" variant="tip" /> MOVED 和 ASK 重定向的区别?
  • <Badge text="高频" variant="tip" /> 什么是脑裂?如何缓解?
  • <Badge text="高频" variant="tip" /> 什么是热 key?如何解决?