集群架构与高可用深度解析
面试官:Redis 的高可用方案有哪些?哨兵和 Cluster 的区别是什么?
你:Redis 有三种高可用方案:主从复制(手动切换)、哨兵模式(自动故障转移)、Cluster 模式(分片 + 高可用)。哨兵适合数据量不大但需要高可用的场景,Cluster 适合大数据量或高 QPS 需要水平扩展的场景。
面试官:那哨兵是怎么判断主库宕机的?为什么区分主观下线和客观下线?
这个追问考察是否理解哨兵的核心机制。能说清 SDOWN 和 ODOWN 的区别及原因的,证明真正掌握了哨兵原理。
链式追问一:主从复制
Section titled “链式追问一:主从复制”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 600mbPSYNC2 协议(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,删除缓存 从库还未同步,客户端读从库未命中 → 穿透到数据库监控延迟:
# 方法1:INFO replicationredis-cli INFO replication# master_repl_offset:10000 主库偏移量# slave0:offset=9500 从库偏移量# 延迟 = 10000 - 9500 = 500 条命令
# 方法2:LATENCY LATESTredis-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 命令。
链式追问二:哨兵模式
Section titled “链式追问二:哨兵模式”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 新主库 └─ 通知客户端更新连接地址哨兵配置示例:
# sentinel.confport 26379sentinel monitor mymaster 192.168.1.1 6379 2 # 监控主库,quorum=2sentinel down-after-milliseconds mymaster 30000 # 30s 无响应判定 SDOWNsentinel failover-timeout mymaster 180000 # 故障转移超时 180ssentinel 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 ✅配置示例:
# 从库配置slave-priority 50 # 优先级,值越小优先级越高
# 查看从库信息INFO replication# slave_priority:50# master_repl_offset:10000手动提升从库优先级:
场景:某个从库硬件配置更好,希望优先提升为主库
配置: slave-priority 10 # 高优先级
结果: 主库宕机时,优先选择该从库作为新主库本质一句话:选举原则是”过滤不健康 → 优先级最高 → 偏移量最大 → run_id 最小”,slave-priority 允许手动控制。
链式追问三:Redis Cluster 模式
Section titled “链式追问三:Redis Cluster 模式”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") # 返回 8192get_slot("order:2001") # 返回 12000MOVED 重定向:
客户端请求: 客户端向节点 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 → 客户端更新路由表,下次直接访问节点 BASK 重定向(临时):
场景: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 对比:
| 维度 | MOVED | ASK |
|---|---|---|
| 触发场景 | 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,更新路由表 - 直接访问节点 BASKING 命令的作用:
节点 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 是临时重定向,迁移期间保证数据可用。
链式追问四:脑裂问题
Section titled “链式追问四:脑裂问题”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 配置:
# redis.confmin-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 个从库才接受写入,减少数据丢失。
链式追问五:大 key 与热 key
Section titled “链式追问五:大 key 与热 key”Q8:什么是大 key?有什么危害?如何发现和处理?必考
Section titled “Q8:什么是大 key?有什么危害?如何发现和处理?”回答要点:
大 key 定义:
String 类型:value > 10KBHash/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:
# 方法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(正确姿势):
# ❌ 错误:用 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 30HSET user:1001:settings theme "dark" lang "zh"HSET user:1001:stats login_count 100
# 方案2:压缩大 Stringimport gziphtml = "<html>...</html>" # 1MBcompressed = gzip.compress(html.encode()) # 压缩到 100KBredis.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 用 --bigkeys 或 MEMORY 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 个 keylocal_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 分散到多个节点或本地缓存。
核心要点回顾
Section titled “核心要点回顾”- 主从复制:全量(BGSAVE + RDB)和增量(repl_backlog),异步复制存在延迟
- 哨兵模式:SDOWN(单哨兵判断)+ ODOWN(多哨兵投票),自动故障转移
- Redis Cluster:16384 slot 分片,MOVED/ASK 重定向,Gossip 协议通信
- 脑裂问题:min-replicas-to-write 要求主库有 N 个从库才接受写入,减少数据丢失
- 大 key:UNLINK 异步删除,拆分 Hash,压缩 String,分块 List
- 热 key:本地缓存、读副本分散、key 打散
面试高频考点
Section titled “面试高频考点”<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?如何解决?