Redis Cluster 与 Sentinel 高可用架构 / Redis Cluster and Sentinel High-Availability Architecture
📅 创建时间:2026-07-28 🏷️ 标签:#Redis #Sentinel #Cluster #高可用 #数据分片 #HashSlots #Gossip 📚 前置知识:[[/03-web/07-middleware/01-cache-layer/00-overview]]
📋 本章目标
- 理解为什么单机 Redis 不够用:内存上限、单点故障、写压力三个瓶颈
- 掌握 Redis Sentinel 的完整架构:监控、通知、自动故障转移、配置提供者
- 理解主观下线(SDOWN)与客观下线(ODOWN)的区别,以及 Quorum 投票机制
- 掌握 Redis Cluster 的数据分片原理:16384 个 hash slots、key 到 slot 的映射
- 理解节点间 Gossip 协议如何传播集群元数据
- 能解释 MOVED 重定向与 ASK 重定向的触发条件和客户端行为差异
- 能够用 ioredis Cluster 模式或 Jedis Cluster 写出正确的客户端集成代码
- 掌握集群运维的核心操作:扩容、缩容、槽迁移、节点替换
- 能够在 Sentinel 和 Cluster 之间做出正确的选型决策
第1部分:为什么需要集群
1.1 单机 Redis 的三个天花板
┌─────────────────────────────────────────────────────────────┐
│ 单机 Redis 的三大瓶颈 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 瓶颈1:内存上限 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 一台物理机/虚拟机的内存是有限的(如 64GB) │ │
│ │ 业务数据量超过 64GB → 单机放不下 │ │
│ │ │ │
│ │ 压力:数据量大 ≠ 请求量大 │ │
│ │ 解法:把数据切分到多台机器(分片/Sharding) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 瓶颈2:单点故障(SPOF) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 单台 Redis 宕机 → 所有缓存不可用 │ │
│ │ → 请求全部穿透到数据库 → 数据库被打垮 │ │
│ │ │ │
│ │ 压力:可用性风险 │ │
│ │ 解法:主从复制 + 自动故障转移(Sentinel) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 瓶颈3:写压力 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Redis 单线程处理命令(主线程) │ │
│ │ 单机写 QPS 上限 ≈ 10万(视操作复杂度) │ │
│ │ 业务增长超过这个上限 → 单机写不动 │ │
│ │ │ │
│ │ 压力:写入吞吐量不足 │ │
│ │ 解法:多个主节点分担写入(Cluster 多 Master) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 三个瓶颈的对应解法: │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 瓶颈 │ 解法 │ 对应架构 │ │
│ │ ──────────────┼──────────────────┼───────────────── │ │
│ │ 内存不够 │ 数据分片 │ Redis Cluster │ │
│ │ 单点故障 │ 主从+故障转移 │ Sentinel/Cluster│ │
│ │ 写不动 │ 多主分担写入 │ Redis Cluster │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1.2 从单机到集群的演进路径
┌─────────────────────────────────────────────────────────────┐
│ Redis 架构演进路径 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 阶段1:单机 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [Redis] │ │
│ │ 一台机器,无备份 │ │
│ │ 挂了就全挂 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ↓ 问题:单点故障 │
│ │
│ 阶段2:主从复制(Master-Slave) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [Master] ←──写 │ │
│ │ │ 异步复制 │ │
│ │ ├──→ [Slave1] ←──读 │ │
│ │ └──→ [Slave2] ←──读 │ │
│ │ │ │
│ │ 读写分离:主写从读 │ │
│ │ 问题:Master 挂了需要手动切换,有数据丢失窗口 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ↓ 问题:手动故障切换太慢 │
│ │
│ 阶段3:Sentinel(高可用) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [Sentinel1] [Sentinel2] [Sentinel3] ← 哨兵集群 │ │
│ │ │ │ │ │ │
│ │ └──────────┴──────────┘ │ │
│ │ │ │ │
│ │ 监控 + 自动故障转移 │ │
│ │ │ │ │
│ │ [Master] ←→ [Slave1] [Slave2] │ │
│ │ │ │
│ │ 解决了:自动故障转移,但所有数据还是在一台 Master上 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ↓ 问题:单机内存有限、单主写不动 │
│ │
│ 阶段4:Cluster(分片 + 高可用) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [Master1] [Master2] [Master3] │ │
│ │ Slots 0-5k Slots 5k-10k Slots 10k-16k │ │
│ │ │ │ │ │ │
│ │ [Slave1] [Slave2] [Slave3] │ │
│ │ │ │
│ │ 每个 Master 负责一部分数据,内置故障转移 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第2部分:Redis Sentinel 架构
2.1 Sentinel 是什么
Sentinel 是一个独立的进程,它的唯一使命是:替你盯着 Redis 实例,Master 挂了自动把 Slave 提上来。
Sentinel 本身也要部署多个(至少 3 个),组成哨兵集群,因为单个 Sentinel 自己也会挂。
┌─────────────────────────────────────────────────────────────┐
│ Redis Sentinel 完整拓扑 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Sentinel 集群 │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │Sentinel1 │ │Sentinel2 │ │Sentinel3 │ │ │
│ │ │ :26379 │ │ :26380 │ │ :26381 │ │ │
│ │ └─────┬────┘ └─────┬────┘ └─────┬────┘ │ │
│ │ └──────────────┼─────────────┘ │ │
│ │ │ 互相通信 (Pub/Sub) │ │
│ └───────────────────────┼──────────────────────────────┘ │
│ │ │
│ ┌──────────┴──────────┐ │
│ ↓ 监控 ↓ 监控 │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Redis Master │ │ Redis Slave │ │
│ │ :6379 │ │ :6380 │ │
│ │ role: master │ │ role: slave │ │
│ │ slaveof: - │ │ master_host: :6379 │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘2.2 Sentinel 的四个职责
┌─────────────────────────────────────────────────────────────┐
│ Sentinel 的四大职责 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 监控(Monitoring) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Sentinel 定期 PING Redis 实例(Master + Slave) │ │
│ │ 如果 Master 在指定时间内没有响应 → 标记为主观下线 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 2. 通知(Notification) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 故障转移完成后,Sentinel 通过 Pub/Sub 通知客户端 │ │
│ │ 客户端收到通知后更新连接目标为新的 Master │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 3. 自动故障转移(Automatic Failover) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 确认 Master 客观下线后: │ │
│ │ a. Sentinel 之间选举一个 Leader │ │
│ │ b. Leader 从 Slave 中选一个提升为新 Master │ │
│ │ c. 让其他 Slave 改为复制新 Master │ │
│ │ d. 旧 Master 恢复后自动降为 Slave │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 4. 配置提供者(Configuration Provider) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 客户端连接 Sentinel,询问"当前 Master 是谁?" │ │
│ │ Sentinel 返回 Master 地址 │ │
│ │ 这样客户端不需要硬编码 Master 地址 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘2.3 主观下线(SDOWN)vs 客观下线(ODOWN)
这是 Sentinel 最核心的判断机制:
┌─────────────────────────────────────────────────────────────┐
│ SDOWN vs ODOWN 判断流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ SDOWN(Subjectively Down,主观下线) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Sentinel1 PING Master │ │
│ │ 超时(down-after-milliseconds,默认30s) │ │
│ │ → Sentinel1 认为 Master 挂了 │ │
│ │ → 但这只是 Sentinel1 自己的判断,可能是网络抖动! │ │
│ │ → 标记: SDOWN │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ODOWN(Objectively Down,客观下线) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Sentinel1 发现 Master SDOWN 后: │ │
│ │ 1. 向其他 Sentinel 询问:"你们觉得 Master 挂了没?" │ │
│ │ 2. 收集投票结果 │ │
│ │ 3. 如果 ≥ quorum 个 Sentinel 都认为 Master SDOWN │ │
│ │ 4. → 标记: ODOWN │ │
│ │ 5. → 触发故障转移 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 关键参数: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ down-after-milliseconds: 认为无响应的超时时间 │ │
│ │ quorum: 需要多少个 Sentinel 同意才能标记 ODOWN │ │
│ │ │ │
│ │ 示例配置(3 个 Sentinel): │ │
│ │ quorum = 2 │ │
│ │ 含义:至少 2 个 Sentinel 认为 Master 挂了, │ │
│ │ 才算真正挂了,才触发故障转移 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ODOWN 的时序图: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ S1: PING Master ──超时──→ SDOWN │ │
│ │ │ │ │
│ │ ├──→ S2: "is-master-down?" ──→ 回复: yes (SDOWN) │ │
│ │ └──→ S3: "is-master-down?" ──→ 回复: yes (SDOWN) │ │
│ │ │ │
│ │ 投票结果: 2/2 >= quorum(2) │ │
│ │ → ODOWN! 开始故障转移 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘2.4 Leader 选举与故障转移流程
标记 ODOWN 后,Sentinel 之间要选举出一个 Leader 来执行故障转移:
┌─────────────────────────────────────────────────────────────┐
│ Sentinel Leader 选举与 Failover 流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 选举流程(类 Raft 协议): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 发现 ODOWN 的 Sentinel 发起选举 │ │
│ │ → 给自己投一票 │ │
│ │ → 向其他 Sentinel 发送投票请求 │ │
│ │ │ │
│ │ 2. 其他 Sentinel 响应规则(先到先得) │ │
│ │ → 如果还没投过票,投给请求者 │ │
│ │ → 如果已经投过了,拒绝 │ │
│ │ │ │
│ │ 3. 得票超过 max(quorum, N/2+1) → 成为 Leader │ │
│ │ N=Sentinel实例数 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Failover 流程(Leader 执行): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Step 1: 选新 Master │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ 从所有 Slave 中选最优的: │ │ │
│ │ │ 优先级1: slave-priority(数字越小越优先) │ │ │
│ │ │ 优先级2: 复制偏移量(谁复制的最新) │ │ │
│ │ │ 优先级3: run_id(字典序,兜底) │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Step 2: SLAVEOF NO ONE │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ 向选中的 Slave 发送命令,让它脱离主从关系 │ │ │
│ │ │ 它现在是新的 Master │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Step 3: 其他 Slave 重新指向 │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ 向其余 Slave 发送 SLAVEOF new-master │ │ │
│ │ │ 让它们开始复制新 Master │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Step 4: 旧 Master 恢复后降级 │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ 旧 Master 重连后,Sentinel 发 SLAVEOF │ │ │
│ │ │ 让它变成新 Master 的 Slave │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘2.5 Sentinel 配置实战
# sentinel.conf — 每个 Sentinel 实例一个配置文件
# 监控 Master: sentinel monitor <master-name> <ip> <port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2
# Master 无响应超过 30 秒则标记 SDOWN
sentinel down-after-milliseconds mymaster 30000
# Failover 超时时间(超过则认为 failover 失败)
sentinel failover-timeout mymaster 180000
# 同时有多少个 Slave 可以和新 Master 同步
sentinel parallel-syncs mymaster 1┌─────────────────────────────────────────────────────────────┐
│ Sentinel 关键参数说明 │
├─────────────────────────────────────────────────────────────┤
│ │
│ down-after-milliseconds │
│ ├── SDOWN 的超时判断依据 │
│ ├── 设太小:网络抖动就触发 SDOWN,产生误判 │
│ ├── 设太大:Master 真挂了很久才发现 │
│ └── 推荐:在云环境可设 30s;物理机房可设 10s │
│ │
│ parallel-syncs │
│ ├── 故障转移后,多少 Slave 同时从新 Master 全量同步 │
│ ├── 设太大:新 Master 的带宽/CPU 被同步占满 │
│ ├── 设太小:Slave 逐个同步,最后一个等很久 │
│ └── 推荐:1(逐个同步,避免 Master 过载) │
│ │
│ failover-timeout │
│ ├── 故障转移各阶段的超时时间 │
│ ├── 如果超时,这次 failover 失败,下次重新选举 Leader │
│ └── 推荐:180000(3分钟) │
│ │
└─────────────────────────────────────────────────────────────┘第3部分:Redis Cluster 架构
3.1 Cluster 的核心思想:Hash Slot
┌─────────────────────────────────────────────────────────────┐
│ 16384 个 Hash Slots 的分片模型 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 核心规则: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. Redis Cluster 共有 16384 个 slot(哈希槽) │ │
│ │ 2. 每个 key 通过 CRC16(key) % 16384 归属到一个 slot │ │
│ │ 3. 每个 Master 节点负责一部分 slot │ │
│ │ 4. 数据在哪个节点 = slot 在哪个节点 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 为什么是 16384? │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 心跳包大小:节点间 Gossip 消息携带 slot 位图 │ │
│ │ 16384 bits = 2KB,刚好塞进一个 TCP 包 │ │
│ │ 2. 节点数上限:Redis 建议集群不超过 1000 个节点 │ │
│ │ 16384 slots / 1000 nodes = 每个节点 ~16 slots │ │
│ │ 粒度足够细,迁移时可以逐个 slot 搬 │ │
│ │ 3. CRC16 输出 16 位,16384 = 2^14,不多不少 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 3 Master 的 Slot 分布示例: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Master1: Slots 0 ─── 5460 (5461 个 slot) │ │
│ │ Master2: Slots 5461 ─── 10922 (5462 个 slot) │ │
│ │ Master3: Slots 10923─── 16383 (5461 个 slot) │ │
│ │ │ │
│ │ key "user:1001" │ │
│ │ → CRC16("user:1001") = 12345 │ │
│ │ → 12345 % 16384 = 12345 │ │
│ │ → slot 12345 在 Master3 上 │ │
│ │ → 请求发到 Master3 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘3.2 Hash Tag:让多个 Key 落在同一个 Slot
┌─────────────────────────────────────────────────────────────┐
│ Hash Tag 机制 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 问题: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 事务/批处理/ Lua 脚本需要多个 key 在同一节点 │ │
│ │ 但 hash slot 计算会把它们分散到不同节点 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 解决:Hash Tag │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 规则:CRC16 只计算 { 和 } 之间的内容 │ │
│ │ │ │
│ │ user:{1001}:name ─┐ │ │
│ │ user:{1001}:email ├── 都算 CRC16("1001") │ │
│ │ user:{1001}:orders ─┘ → 保证在同一 slot │ │
│ │ │ │
│ │ 示例: │ │
│ │ MSET user:{1001}:name "Tom" user:{1001}:age 25 │ │
│ │ → 两个 key 同 slot → 同节点 → 原子执行 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 注意: │
│ ├── 不要滥用 hash tag 导致数据倾斜 │
│ ├── 如果 {1001} 的数据量巨大,该 slot 成为热点 │
│ └── 正常情况下让 key 自然分散到各节点 │
│ │
└─────────────────────────────────────────────────────────────┘3.3 Gossip 协议:节点间如何通信
Redis Cluster 的节点之间不通过中心化组件通信,而是通过 Gossip(流言)协议互相传递集群元数据:
┌─────────────────────────────────────────────────────────────┐
│ Gossip 协议通信机制 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 原理:每个节点定期随机选取几个其他节点,交换自己知道的元数据 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Node1 ──── Gossip ────→ Node2 │ │
│ │ "我知道: Node3 的 slots 是 0-5000" │ │
│ │ "我知道: Node4 还活着" │ │
│ │ │ │
│ │ Node2 收到后更新自己的路由表, │ │
│ │ 下次 Gossip 时再传给 Node3 和 Node4 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Gossip 消息类型: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ PING ──→ 发送心跳,附带自己知道的节点信息 │ │
│ │ PONG ←── 回复心跳,附带发送者知道的节点信息 │ │
│ │ MEET ──→ 邀请新节点加入集群 │ │
│ │ FAIL ──→ 广播某个节点已确认挂掉 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 节点故障判断(Cluster 内置,不依赖 Sentinel): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. Node1 PING Node2 → 超时 │ │
│ │ → Node1 标记 Node2 为 PFAIL(疑似下线) │ │
│ │ │ │
│ │ 2. Node1 通过 Gossip 传播 "Node2 可能是 PFAIL" │ │
│ │ │ │
│ │ 3. 当 ≥ N/2+1 个 Master 都认为 Node2 是 PFAIL │ │
│ │ → 所有节点标记 Node2 为 FAIL(确认下线) │ │
│ │ → 触发故障转移 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Gossip 的消息传播速度: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 假设 100 个节点,每个节点每秒 ping 1 个随机节点 │ │
│ │ 一条新消息在 ~log(100) ≈ 5 秒内传遍整个集群 │ │
│ │ │ │
│ │ 代价:每个节点每秒发 1 条 Gossip 消息 │ │
│ │ 100 节点全部 Gossip ≈ 100 条/秒 │ │
│ │ 足够低,不占多少带宽 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘3.4 Cluster 的故障转移
Redis Cluster 内置故障转移,不需要额外部署 Sentinel:
┌─────────────────────────────────────────────────────────────┐
│ Cluster 故障转移流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 拓扑: │ │
│ │ Master1 (slots 0-5000) ←→ Slave1 │ │
│ │ Master2 (slots 5001-10000) ←→ Slave2 │ │
│ │ Master3 (slots 10001-16383) ←→ Slave3 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Master1 宕机后: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. Master2 / Master3 通过 Gossip 发现 Master1 PFAIL │ │
│ │ 2. 足够多的 Master 确认 → Master1 标记 FAIL │ │
│ │ 3. Slave1 发现自己的 Master FAIL │ │
│ │ 4. Slave1 发起选举(向其他 Master 请求投票) │ │
│ │ 5. Slave1 获得 N/2+1 票 → 提升为新 Master │ │
│ │ 6. Slave1 接管 slots 0-5000 │ │
│ │ 7. 新 Master1 通过 Gossip 广播自己的新身份 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Cluster 选举的条件: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Slave 要参与竞选,需要满足: │ │
│ │ 1. 它的 Master 处于 FAIL 状态 │ │
│ │ 2. 它与 Master 的复制偏移量足够新 │ │
│ │ (epoch 时间内落后不超过一定量) │ │
│ │ 3. 集群半数以上 Master 给它投票 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第4部分:MOVED 与 ASK 重定向
4.1 问题的由来
┌─────────────────────────────────────────────────────────────┐
│ 客户端访问 Cluster 的问题 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 客户端 ──→ GET user:1001 ──→ Node1 │ │
│ │ │ │
│ │ 但 key "user:1001" 的 hash slot 在 Node2! │ │
│ │ │ │
│ │ Node1 怎么回复? │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘4.2 MOVED:永久重定向
┌─────────────────────────────────────────────────────────────┐
│ MOVED 重定向(永久) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 触发条件:key 所在的 slot 在另一个节点上,且没有迁移 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 客户端 ──→ GET user:1001 ──→ Node1 │ │
│ │ │ │ │
│ │ ←── MOVED 12345 192.168.1.2:6379 ──┘ │ │
│ │ │ │ │ │
│ │ │ └── Node2 的地址 │ │
│ │ └── slot 编号 │ │
│ │ │ │
│ │ 客户端收到 MOVED 后: │ │
│ │ 1. 更新本地 slot → 节点映射表 │ │
│ │ 2. 重新向 Node2 发送 GET user:1001 │ │
│ │ 3. 以后对 slot 12345 的请求直接发 Node2 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 特点: │
│ ├── 永久重定向:客户端应更新路由表 │
│ ├── 说明本次请求发错了节点 │
│ └── 下次同一 slot 的请求不会再 MOVED │
│ │
└─────────────────────────────────────────────────────────────┘4.3 ASK:临时重定向
┌─────────────────────────────────────────────────────────────┐
│ ASK 重定向(临时,槽迁移中) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 触发条件:key 所在的 slot 正在从 Node1 迁移到 Node2 │
│ │
│ 场景:Slot 12345 正在从 Node1 迁移到 Node2 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 迁移中的 slot 状态: │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ Node1 (源): slot 12345 → MIGRATING │ │ │
│ │ │ Node2 (目标): slot 12345 → IMPORTING │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 客户端 ──→ GET user:1001 ──→ Node1 │ │
│ │ │ │
│ │ Node1 查找 user:1001: │ │
│ │ ├── 找到了 → 直接返回(迁移中但数据还在就返回) │ │
│ │ └── 没找到(已迁移到 Node2)→ 返回: │ │
│ │ ASK 12345 192.168.1.2:6379 │ │
│ │ │ │
│ │ 客户端收到 ASK 后: │ │
│ │ 1. 向 Node2 发送 ASKING 命令 │ │
│ │ (告诉 Node2:虽然 slot 状态是 IMPORTING, │ │
│ │ 但我确认要操作你这个 slot) │ │
│ │ 2. 然后发送 GET user:1001 │ │
│ │ 3. 不更新本地路由表!(这是临时状态) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ASK vs MOVED 对比: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ MOVED │ ASK │ │
│ │ ────────────┼──────────────┼────────────────────── │ │
│ │ 场景 │ 正常路由 │ 槽迁移中 │ │
│ │ slot 状态 │ 稳定归属 │ MIGRATING/IMPORTING │ │
│ │ 重定向类型 │ 永久 │ 临时(仅本次请求) │ │
│ │ 是否更新路由│ 是 │ 否 │ │
│ │ 前置命令 │ 无 │ ASKING │ │
│ │ 效率 │ 高(更新后直连│ 低(每次可能都 ASK) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘4.4 智能客户端如何高效路由
┌─────────────────────────────────────────────────────────────┐
│ 智能客户端路由流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 客户端启动: │ │
│ │ 1. 连接种子节点 (seed nodes),发送 CLUSTER SLOTS │ │
│ │ 2. 获取完整的 slot → node 映射表 │ │
│ │ 3. 在本地缓存这份映射表 │ │
│ │ │ │
│ │ 每次请求: │ │
│ │ 1. CRC16(key) % 16384 → slot │ │
│ │ 2. 从本地映射表查 slot → node │ │
│ │ 3. 直接向目标 node 发送命令 │ │
│ │ │ │
│ │ 收到 MOVED: │ │
│ │ 1. 更新本地映射表 │ │
│ │ 2. 重试请求 │ │
│ │ │ │
│ │ 收到 ASK: │ │
│ │ 1. 向目标节点 ASKING + 原命令 │ │
│ │ 2. 不更新本地映射表 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 为什么不在服务端做路由转发? │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Redis Cluster 没有 Proxy 层 │ │
│ │ 每个节点都知道完整路由表,但不做转发 │ │
│ │ 转发会成为瓶颈、增加延迟、破坏无中心架构 │ │
│ │ 所以把路由责任交给客户端 │ │
│ │ │ │
│ │ 如果你想要 Proxy 层 → 用 Redis Cluster Proxy(补充件)│ │
│ │ 或 Codis / Twemproxy 等代理方案 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第5部分:客户端如何集成
5.1 ioredis Cluster 模式(Node.js / TypeScript)
import Redis from 'ioredis';
// 创建 Cluster 客户端
const cluster = new Redis.Cluster([
{ host: '192.168.1.1', port: 6379 },
{ host: '192.168.1.2', port: 6379 },
{ host: '192.168.1.3', port: 6379 },
], {
// 关键配置
redisOptions: {
password: 'your-password',
connectTimeout: 5000,
},
// 集群相关配置
clusterRetryStrategy: (times) => {
// 重试策略:指数退避
return Math.min(times * 100, 3000);
},
// 是否在 MOVED 时自动重定向(默认 true)
enableAutoPipelining: true,
// 节点宕机后多久从连接池移除
scaleReads: 'slave', // 'master' | 'slave' | 'all'
});
// 普通操作 —— 客户端自动路由
await cluster.set('user:1001', JSON.stringify({ name: 'Tom' }));
const user = await cluster.get('user:1001');
// Pipeline —— 自动按节点分组
const pipeline = cluster.pipeline();
pipeline.set('key1', 'val1');
pipeline.set('key2', 'val2');
pipeline.get('key1');
const results = await pipeline.exec();
// ioredis 自动把 pipeline 中的命令按 slot 分组,
// 向不同节点发送不同的 pipeline 子集
// 事务(需要 hash tag 保证同一 slot)
await cluster.multi()
.set('order:{1001}:id', '1001')
.set('order:{1001}:amount', '99')
.exec();
// 用 {1001} 确保两个 key 在同一个 slot5.2 Jedis Cluster(Java)
import redis.clients.jedis.*;
import java.util.HashSet;
import java.util.Set;
public class RedisClusterExample {
public static void main(String[] args) {
// 配置种子节点
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("192.168.1.1", 6379));
nodes.add(new HostAndPort("192.168.1.2", 6379));
nodes.add(new HostAndPort("192.168.1.3", 6379));
JedisCluster cluster = new JedisCluster(
nodes,
5000, // connectionTimeout
5000, // soTimeout
3, // maxAttempts (MOVED/ASK 重试次数)
"password",
new GenericObjectPoolConfig<>()
);
// 普通操作 —— 客户端自动路由
cluster.set("user:1001", "{\"name\":\"Tom\"}");
String user = cluster.get("user:1001");
// 注意:JedisCluster 没有 pipeline 支持
// 需要 pipeline 时考虑使用 Lettuce 客户端
// 关闭连接
cluster.close();
}
}5.3 Sentinel 客户端集成(ioredis)
import Redis from 'ioredis';
const redis = new Redis({
sentinels: [
{ host: '192.168.1.1', port: 26379 },
{ host: '192.168.1.2', port: 26379 },
{ host: '192.168.1.3', port: 26379 },
],
name: 'mymaster', // 对应 sentinel.conf 中的 master-name
password: 'your-password',
sentinelPassword: 'sentinel-password',
// 当 Sentinel 通知 Master 变更时自动切换
enableAutoPipelining: true,
});
// Sentinel 模式下,客户端:
// 1. 启动时向 Sentinel 询问 Master 地址
// 2. 连接 Master
// 3. 订阅 Sentinel 的 +switch-master 频道
// 4. Master 切换后自动重连新 Master第6部分:集群运维
6.1 扩容:加入新节点
┌─────────────────────────────────────────────────────────────┐
│ Cluster 扩容流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 初始状态:3 Masters, 3 Slaves │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ M1 (0-5460) M2 (5461-10922) M3 (10923-16383) │ │
│ │ S1 S2 S3 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 目标:加入 M4 + S4,重新平衡 slot │
│ │
│ Step 1: 启动新节点 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ redis-server --port 6380 --cluster-enabled yes │ │
│ │ redis-server --port 6381 --cluster-enabled yes │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Step 2: 加入集群 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ redis-cli --cluster add-node 192.168.1.4:6380 \ │ │
│ │ 192.168.1.1:6379 # 任意已有节点 │ │
│ │ │ │
│ │ redis-cli --cluster add-node 192.168.1.4:6381 \ │ │
│ │ 192.168.1.1:6379 \ │ │
│ │ --cluster-slave \ │ │
│ │ --cluster-master-id <M4的id> │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Step 3: 重新分配 slot(reshard) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ redis-cli --cluster reshard 192.168.1.1:6379 │ │
│ │ │ │
│ │ 交互式引导: │ │
│ │ How many slots do you want to move? 4096 │ │
│ │ What is the receiving node ID? <M4的id> │ │
│ │ Source node #1: <M1的id> │ │
│ │ Source node #2: <M2的id> │ │
│ │ Source node #3: <M3的id> │ │
│ │ Source node #4: done │ │
│ │ │ │
│ │ 从 3 个旧 Master 各搬 ~1365 个 slot 到 M4 │ │
│ │ 结果:每个 Master 约 4096 个 slot │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 最终状态:4 Masters, 4 Slaves │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ M1 (0-4095) M2 (4096-8191) │ │
│ │ M3 (8192-12287) M4 (12288-16383) │ │
│ │ S1 S2 S3 S4 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘6.2 缩容:移除节点
┌─────────────────────────────────────────────────────────────┐
│ Cluster 缩容流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Step 1: 迁移该节点的所有 slot 出去 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ # 将 M4 的 slot 迁移到其他节点 │ │
│ │ redis-cli --cluster reshard 192.168.1.1:6379 │ │
│ │ │ │
│ │ How many slots do you want to move? 4096 │ │
│ │ What is the receiving node ID? <M1的id> │ │
│ │ Source node #1: <M4的id> │ │
│ │ Source node #2: done │ │
│ │ │ │
│ │ 也可以平均分配到 M1/M2/M3 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Step 2: 确认 slot 已清空后再删除节点 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ # 检查 M4 是否还有 slot │ │
│ │ redis-cli -p 6380 CLUSTER NODES │ │
│ │ # 确保 M4 的 slot 列表为空 │ │
│ │ │ │
│ │ # 先从集群中移除 Slave │ │
│ │ redis-cli --cluster del-node 192.168.1.1:6379 \ │ │
│ │ <S4的id> │ │
│ │ │ │
│ │ # 再移除 Master │ │
│ │ redis-cli --cluster del-node 192.168.1.1:6379 \ │ │
│ │ <M4的id> │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘6.3 槽迁移的内部流程
┌─────────────────────────────────────────────────────────────┐
│ Slot 迁移的原子操作流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Redis Cluster 对每个 slot 的迁移是按 key 逐个进行的: │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 源节点 (Node1) 目标节点 (Node2) │ │
│ │ │ │
│ │ 1. Node2: CLUSTER SETSLOT <slot> IMPORTING <node1> │ │
│ │ → 告诉 Node2: 我要接收 slot 12345 了 │ │
│ │ │ │
│ │ 2. Node1: CLUSTER SETSLOT <slot> MIGRATING <node2> │ │
│ │ → 告诉 Node1: 我要迁移 slot 12345 了 │ │
│ │ │ │
│ │ 3. 获取 slot 中的所有 key │ │
│ │ CLUSTER GETKEYSINSLOT <slot> <count> │ │
│ │ │ │
│ │ 4. 逐个迁移 key(原子操作) │ │
│ │ MIGRATE <target_host> <target_port> <key> │ │
│ │ <db> <timeout> [COPY] [REPLACE] │ │
│ │ → 源节点把 key 序列化发给目标 │ │
│ │ → 目标节点接收并存储 │ │
│ │ → 源节点删除本地 key │ │
│ │ │ │
│ │ 5. 所有 key 迁移完成后 │ │
│ │ 双方向全集群广播 slot 归属变更 │ │
│ │ CLUSTER SETSLOT <slot> NODE <node2> │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 迁移期间的并发请求处理: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 客户端请求 key 在正在迁移的 slot 中: │ │
│ │ │ │
│ │ 请求到达 Node1(源): │ │
│ │ key 还在 → 直接返回 │ │
│ │ key 已迁走 → 返回 ASK 重定向到 Node2 │ │
│ │ │ │
│ │ 请求到达 Node2(目标): │ │
│ │ key 已迁来 → 正常处理 │ │
│ │ key 还在 Node1 → 返回 MOVED 到 Node1 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 注意: │
│ ├── 迁移过程中 slot 可以正常读写(数据不会丢) │
│ ├── ASK 重定向带来短暂延迟 │
│ └── 建议在低峰期进行槽迁移 │
│ │
└─────────────────────────────────────────────────────────────┘6.4 节点替换:Slave 升 Master 后重新部署
┌─────────────────────────────────────────────────────────────┐
│ 节点替换操作 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 场景1:Master 永久故障,Slave 已自动提升 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [旧M1 宕机] │ │
│ │ [S1 自动提升为新 M1] │ │
│ │ │ │
│ │ 操作: │ │
│ │ 1. 启动一个新的 Redis 实例(作为新 Slave) │ │
│ │ 2. 加入集群并设为新 M1 的 Slave │ │
│ │ redis-cli --cluster add-node <新节点IP:6379> \ │ │
│ │ <新M1的IP:6379> \ │ │
│ │ --cluster-slave │ │
│ │ 3. 修理/重置旧 M1(可选) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 场景2:计划内替换(升级硬件/Redis版本) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 目标:替换 M1 为新机器,零停机 │ │
│ │ │ │
│ │ 操作: │ │
│ │ 1. 在新机器上启动 Redis,作为 M1 的 Slave │ │
│ │ 2. 等待数据同步完成 │ │
│ │ 3. 手动触发故障转移 │ │
│ │ redis-cli -p <Slave端口> CLUSTER FAILOVER │ │
│ │ → Slave 提升为新 Master,旧 M1 降级 │ │
│ │ 4. 移除旧 M1 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第7部分:Sentinel vs Cluster 选型决策
7.1 架构对比
┌─────────────────────────────────────────────────────────────┐
│ Sentinel vs Cluster 全景对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 维度 │ Sentinel │ Cluster │
│ ───────────────┼──────────────────────┼────────────────── │
│ 数据分片 │ 无(所有数据在一起) │ 有(16384 slots) │
│ 写扩展 │ 单主(写QPS上限单机) │ 多主(横向扩展写)│
│ 读扩展 │ 多 Slaves 分担 │ 多 Slaves 分担 │
│ 内存扩展 │ 受单机限制 │ 横向扩展 │
│ 自动故障转移 │ 依赖 Sentinel 集群 │ 内置,不依赖外部 │
│ 客户端复杂度 │ 低(直连 Master) │ 高(需支持路由) │
│ 运维复杂度 │ 低(3~5 个 Sentinel)│ 中(多节点管理) │
│ 事务/Lua脚本 │ 支持完整功能 │ 受限于单 slot │
│ 跨 slot 操作 │ 支持 │ 需 hash tag │
│ Pub/Sub │ 全集群广播 │ 全集群广播(性能差)│
│ 最小节点数 │ 3 Sentinel + 2 Redis │ 6 Redis (3M+3S) │
│ │
└─────────────────────────────────────────────────────────────┘7.2 决策流程图
┌─────────────────────────────────────────────────────────────┐
│ Sentinel vs Cluster 选型决策 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 你的核心需求是什么? │
│ │ │
│ ├── 单机内存够用吗?(如 < 64GB) │
│ │ ├── 够用 │
│ │ │ ├── 需要高可用吗? │
│ │ │ │ ├── 不需要 → 单机 Redis │
│ │ │ │ └── 需要 │
│ │ │ │ ├── 主从 + Sentinel → 够了 │
│ │ │ │ │ (数据都在一个 Master 上) │
│ │ │ │ └── 特殊情况:需要多 Master 写 │
│ │ │ │ → Cluster(需要合理的 sharding key)│
│ │ │ │ │
│ │ │ └── 需要读写分离 + 自动故障转移 → Sentinel │
│ │ │ │
│ │ └── 不够用(数据 > 单机内存) │
│ │ └── Cluster(唯一解,数据分片) │
│ │ │
│ └── 写 QPS 超过单机上限了吗?(如 > 10万/s) │
│ ├── 没有 → Sentinel 够用 │
│ └── 超过了 → Cluster(多主分担写入) │
│ │
│ 总结: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Sentinel 解决:高可用 + 读扩展 │ │
│ │ Cluster 解决:数据分片 + 写扩展 + 高可用 │ │
│ │ │ │
│ │ 大多数中小项目:Sentinel 就够 │ │
│ │ 数据量大/写压力大:选 Cluster │ │
│ │ 最简单且够用时:不要集群化 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘7.3 典型应用场景举例
┌─────────────────────────────────────────────────────────────┐
│ 典型场景选型 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 场景1:中小型电商(日活 10 万,数据 < 20GB) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 架构:Sentinel(1 Master + 2 Slaves + 3 Sentinels) │ │
│ │ 理由:数据量小、写压力不大、主从+自动故障转移就够了 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 场景2:大型内容平台(日活 1000 万,数据 > 200GB) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 架构:Cluster(6 Master + 6 Slave,每对 40GB) │ │
│ │ 理由:数据量远超单机内存,必须分片 │ │
│ │ 按 userId hash,确保用户数据在同一节点 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 场景3:实时排行榜/计数器(写 QPS 20 万) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 架构:Cluster(多 Master) │ │
│ │ 理由:单 Master 写不动 20 万 QPS │ │
│ │ 多 Master 分担写入,每个 Master ~3 万 QPS │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 场景4:企业内部系统(日活 < 1000,数据 < 1GB) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 架构:单机 Redis │ │
│ │ 理由:没有数据量和可用性压力,集群化是过度设计 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘核心总结
总结1:从单机到集群的演进逻辑
单机 → 发现三个瓶颈:
内存不够 → 分片(Cluster)
单点故障 → 主从 + 自动故障转移(Sentinel)
写不动 → 多主分担(Cluster)
Sentinel 解决可用性,Cluster 同时解决可用性 + 扩展性。总结2:Sentinel 核心机制
SDOWN(主观下线):1 个 Sentinel 认为 Master 挂了
ODOWN(客观下线):≥ Quorum 个 Sentinel 都认为挂了
ODOWN → Leader 选举 → 选新 Master → 通知客户端 → 旧 Master 降级
核心参数:
down-after-milliseconds: SDOWN 超时
quorum: ODOWN 所需的票数
parallel-syncs: 故障转移后同时同步的 Slave 数总结3:Cluster 核心机制
16384 个 Hash Slots
→ CRC16(key) % 16384 = slot 编号
→ 按 slot 分配 key 到不同 Master
→ 每个 Master 负责一部分 slot
节点间通信:Gossip(PING / PONG / MEET / FAIL)
故障转移:内置,不依赖 Sentinel
客户端路由:
MOVED = 永久重定向(更新路由表)
ASK = 临时重定向(槽迁移中,不更新路由表)总结4:选型决策
| 需求 | 方案 |
|---|---|
| 高可用(小数据量) | Sentinel |
| 数据分片(大数据量) | Cluster |
| 高可用 + 读扩展 | Sentinel |
| 高可用 + 写扩展 | Cluster |
| 最简单的可靠方案 | 单机 + RDB/AOF 持久化 |
章节测试
测试1:SDOWN vs ODOWN
一个 Sentinel 集群有 5 个 Sentinel 实例,quorum 设为 2。Sentinel-1 发现 Master 超时无响应,标记为 SDOWN。此时需要多少个 Sentinel 同意才能标记 ODOWN 并触发故障转移?
测试2:Hash Slot
有一个 key 叫 "product:54321",请写出它在 Redis Cluster 中被路由到哪个 slot 的计算过程(写出公式和计算步骤)。
测试3:MOVED vs ASK
客户端收到 MOVED 重定向和 ASK 重定向后,行为上有什么区别?为什么有这种区别?
测试4:Hash Tag
为什么在 Redis Cluster 中执行事务(MULTI/EXEC)需要用 Hash Tag?写出一个正确的示例。
测试5:Gossip
Redis Cluster 为什么选择 Gossip 协议而不是使用中心化的配置中心(如 ZooKeeper/etcd)?
测试6:故障转移
Sentinel 模式下,Leader Sentinel 从多个 Slave 中选择新 Master 时的优先级顺序是什么?
测试7:选型
一个项目数据量约 200GB,写 QPS 约 5 万/s,读 QPS 约 50 万/s。请设计 Redis 架构并说明理由。
测试8:运维
你有 3 Master + 3 Slave 的 Cluster,想把一个 Master 安全下线(计划内维护),保证数据零丢失,步骤是什么?
参考答案
测试1答案
答案:至少 2 个 Sentinel 同意(≥ quorum=2)。5 个 Sentinel 中至少有 2 个(包括 Sentinel-1 自己)标记 SDOWN 即可达成 ODOWN。
测试2答案
答案:
- 计算 CRC16("product:54321") → 假设结果是 48021
- slot = 48021 % 16384 = 15253
- key 被路由到负责 slot 15253 的 Master 节点
测试3答案
答案:
- MOVED:永久重定向。客户端应更新本地 slot→node 映射表,后续同一 slot 的请求直接发到正确节点。
- ASK:临时重定向。slot 正在迁移中,客户端不应更新路由表。发送 ASKING 命令后向目标节点请求,下次同一 slot 的请求仍发原节点。
- 区别原因:MOVED 表示 slot 归属已确定变更;ASK 表示迁移未完成,归属尚未最终变更。
测试4答案
答案:因为 MULTI/EXEC 中的所有命令必须在同一个节点上原子执行。如果 key 分散在不同 slot,它们落在不同节点上,事务无法跨节点。
正确示例:
MULTI
SET order:{1001}:id 1001
SET order:{1001}:amount 99
INCR order:{1001}:version
EXEC花括号 {1001} 确保 3 个 key 的 hash 基于 "1001" 计算,落到同一个 slot。
测试5答案
答案:
- 无单点故障:没有中心化配置中心,任意节点挂掉不影响集群元数据传播
- 最终一致性:去中心化的 Gossip 传播满足 Redis 对 AP 的需求
- 运维简单:不需要额外部署 ZooKeeper/etcd 集群
- 代价可接受:100 节点规模下,Gossip 消息量很小(每秒约 100 条)
测试6答案
答案:优先级从高到低:
- slave-priority(配置值越小越优先,0 表示永远不提升)
- 复制偏移量(offset 越大 = 数据越新)
- run_id(字典序,作为最终兜底)
测试7答案
答案:推荐 Redis Cluster。
- 200GB 数据远超单机内存 → 需要数据分片 → Cluster
- 5 万写 QPS → 单 Master 可能扛不住 → Cluster 多 Master 分担
- 50 万读 QPS → 每个 Master 挂 2 个 Slave,6 Master 共 12 Slave 分担读
- 建议:6 Master + 12 Slave,每对 Master-Slave 负责约 200/6≈33GB 数据和约 8k 写 QPS
测试8答案
答案:
- 确认该 Master 的 Slave 数据同步正常(检查复制偏移量)
- 在 Slave 上执行
CLUSTER FAILOVER手动触发故障转移 - Slave 提升为新 Master,接管原 Master 的 slots
- 原 Master 自动降级为 Slave
- 等待客户端的路由表全部更新(几秒内)
- 安全关闭原 Master 进程
- 完成维护后可从集群中移除该节点
相关笔记
- [[/03-web/07-middleware/01-cache-layer/00-overview]] - 缓存架构全景
- [[/03-web/07-middleware/01-redis-deep]] - Redis 深入:缓存三大问题
- [[/03-web/07-middleware/05-cache-strategy]] - 缓存一致性策略
- [[/03-web/06-databases-and-data-access/05-redis]] - Redis 数据结构基础
下一步学习
- [ ] 阅读 消息队列基础
- [ ] 实践:搭建一个 3 Master + 3 Slave 的 Redis Cluster 并验证故障转移
学习状态:🟡 开始学习