Skip to content

从主从到 Cluster:复制、哨兵与分片演进

本文是 Redis 系统学习系列的 L2 核心篇。前置:31. 事件循环与线程模型32. 持久化实践。 学完可以配合面试题食用:07-cluster-master-slave-sentinel16-sentinel-raft-election17-cluster-hash-slot-partitioning

单机 Redis 的瓶颈在哪

单机 Redis 全部读写压在一台机器上,遇到两个瓶颈:容量上限受限于单机内存,可用性靠单点保证。一台机器挂了,缓存层全体穿透,直接打穿数据库。

解决思路分两步走:先做主从复制解决读扩展和故障切换,再通过分片集群把数据水平拆分到多台机器。Redis 从单机到 Cluster 的演进,就是一条从"单个内存节点"到"分布式缓存集群"的路线。

主从复制:全量同步与增量同步

主从(Master-Replica)是 Redis 高可用的基础。一个主节点负责写,多个从节点同步数据并承担读流量。

全量同步流程

从节点首次连接主节点时触发全量同步:

  1. 从节点发送 PSYNC ? -1(无偏移量,表示全量)
  2. 主节点执行 BGSAVE 生成 RDB 快照
  3. 主节点将 RDB 快照发送给从节点,从节点加载到内存
  4. 主节点在 BGSAVE 期间缓冲的写命令,通过复制积压缓冲区(replication backlog) 发送给从节点
  5. 从节点应用缓冲命令,追上主节点状态
mermaid
sequenceDiagram
    participant Replica
    participant Master
    Replica->>Master: PSYNC ? -1
    Master->>Master: BGSAVE
    Master-->>Replica: RDB 文件
    Replica->>Replica: 加载 RDB
    Master->>Replica: backlog 缓冲写命令
    Replica->>Replica: 应用缓冲命令
    Note over Replica,Master: 全量同步完成

增量同步与断线重连

全量同步完成后,主节点将每条写命令发送给从节点(REPLCONF ACK 确认偏移量)。如果从节点短暂断线,重连后发送 PSYNC <master_run_id> <offset>,主节点检查偏移量是否还在 backlog 范围内——在就增量同步,不在就触发新一轮全量同步。

backlog 大小由 repl-backlog-size 控制(默认 1MB)。生产经验:在写流量大的实例上增大到 32-64MB,给断线重连留出更多缓冲空间。

级联复制

当从节点数量多(比如 10+)时,所有从节点都直接挂载到主节点会让主节点承受大量 RDB 传输带宽压力。replicaof 可以链式传递:让一个从节点挂载到主节点,其他从节点挂载到这个中间从节点。代价是中间从节点故障时下游会断连。

哨兵:从手动切主到自动故障转移

主从复制解决了读扩展,但主节点故障时需要人工操作:SLAVEOF NO ONE 提拔一个从节点做主节点,再把其他从节点指向新主节点。哨兵(Sentinel)把这个过程自动化了。

主观下线与客观下线

哨兵进程以 redis-sentinel 方式运行,每个 Sentinel 每秒向所有已知节点(主、从、其他 Sentinel)发送 PING。超过 down-after-milliseconds 未收到回复,Sentinel 将该节点标记为主观下线(SDOWN,Subjective Down)。

当 Sentinel 认为主节点主观下线,向其他 Sentinel 询问意见。如果超过 quorum 个 Sentinel 都认为主节点不可达,标记为客观下线(ODOWN,Objective Down)。

领导者选举(Raft 思想)

客观下线后,一个 Sentinel 发起故障转移。多个 Sentinel 之间通过类似 Raft 的算法选出一个领导者:

  • 每个 Sentinel 向其他节点发送投票请求
  • 每个 Sentinel 在一个 epoch 内只能投一票
  • 获得半数以上票的 Sentinel 成为领导者

故障转移步骤

领导者 Sentinel 执行以下操作:

  1. 从下属从节点中选一个做新主节点(优先级 slave-priority 最高 -> 复制偏移量最大 -> run_id 最小)
  2. 向新主节点发送 SLAVEOF NO ONE
  3. 等待新主节点完成切换(info replication 确认 role 变为 master)
  4. 向其他从节点发送 SLAVEOF <new_master_ip> <new_master_port>
  5. 修改原主节点配置,等它恢复后自动成为新主节点的从节点

客户端一般通过订阅 +switch-master 频道的消息来感知主节点变更,或者在连接失败时主动查询 Sentinel 获取最新主节点地址。

Cluster:16384 个槽与分片

主从+哨兵解决了可用性,但容量仍然受限于单机内存。Cluster 把数据水平拆分到多个节点,每个节点只负责一部分数据。

16384 个槽:常见误解澄清

Cluster 把整个 keyspace 划分为 16384 个哈希槽(hash slot)。每个 key 通过 CRC16(key) & 16383 计算所属槽,槽再通过 CLUSTER ADDSLOTS 分配到节点。

很多人直觉认为"槽数应该是 16384 是因为 CRC16 的输出范围是 0-65535,取 16384 等于除以 4"。实际上,16384 是设计选择,不是 CRC16 的数学计算结果。选择 16384 的原因是:

  • 墙门消息(gossip)中要携带每个节点的槽位信息,用大小 16384 的 bitmap 只需 2KB(16384 / 8 = 2048 字节)
  • 65536 槽的 bitmap 是 8KB,对大型集群的网络开销影响更大
  • 16384 对普通规模的集群(几十到几百个节点)分片粒度足够

请求路由:MOVED 与 ASK

客户端发送请求时,如果目标节点不负责该 key 的槽,节点返回 MOVED <slot> <ip>:<port> 错误,客户端拿到新地址重发请求。Smart Client(如 JedisCluster、Lettuce)会缓存槽映射表,首次请求后直接路由到正确节点,不经过 MOVED 重定向。

mermaid
flowchart LR
    Client["Smart Client\n(缓存槽映射)"]
    N1["Node 1\n0-5460 槽"]
    N2["Node 2\n5461-10922 槽"]
    N3["Node 3\n10923-16383 槽"]
    Client -- "key1 → CRC16=1389 → 槽 1389" --> N1
    Client -- "key2 → CRC16=7890 → 槽 7890" --> N2
    Client -- "key3 → CRC16=15000 → 槽 15000" --> N3

槽迁移期间会出现 ASK 重定向:MOVED 表示"槽永久归属已变",客户端应更新缓存;ASK 表示"槽正在迁移中,这次先来我这边查一下",客户端不更新缓存,下次直接去目的节点。

Gossip 通信与故障检测

Cluster 节点之间通过 gossip 协议交换状态信息。每个节点向其他节点发送 PING 包,包含本节点已知的其他节点状态(在线/下线/疑似下线)。收到 PONG 回复后更新自己维护的集群视图。

如果一个节点标记另一个节点为 PFAIL(疑似下线),并将此信息广播给其他节点。如果超过半数节点确认该节点不可达,状态升级为 FAIL,集群将故障节点负责的槽重新分配给其他节点(前提是故障节点有副本)。

三种架构选型表

场景推荐架构理由
单机读多写少,数据量 < 10GB主从(1主1从)低成本,读流量由从节点分担
需要自动故障切换主从 + 哨兵(3节点哨兵集群)自动切主,客户端感知切换
数据量 > 10GB 或 QPS > 10万Cluster(至少 3 主 3 从)水平扩展,容量和吞吐不受单机限制
读多写少且数据量大主从哨兵 + 本地缓存层减少 Redis 读压力,hotkey 兜底,但需处理一致性问题

动手实操:3 节点 Cluster 搭建与故障转移演示

以下用 docker-compose 在本地启动 3 主 3 从的 Redis Cluster(Redis 7.x)。

yaml
# docker-compose.yml
version: '3.8'
services:
  redis-cluster-1:
    image: redis:7-alpine
    ports:
      - "7001:7001"
      - "17001:17001"
    command: redis-server /usr/local/etc/redis/redis.conf
    volumes:
      - ./redis-7001.conf:/usr/local/etc/redis/redis.conf

  redis-cluster-2:
    image: redis:7-alpine
    ports:
      - "7002:7002"
      - "17002:17002"
    command: redis-server /usr/local/etc/redis/redis.conf
    volumes:
      - ./redis-7002.conf:/usr/local/etc/redis/redis.conf

  # ... 类似定义 3-6 节点,每个端口递增

每个节点的配置文件(以 7001 为例):

redis
# redis-7001.conf
port 7001
cluster-enabled yes
cluster-config-file nodes-7001.conf
cluster-node-timeout 5000
appendonly yes

启动集群:

bash
# 启动 6 个容器
docker-compose up -d

# 获取容器 IP(假设在同一网络)
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker-compose ps -q)

# 创建集群(3 主 3 从)
redis-cli --cluster create \
  172.19.0.2:7001 172.19.0.3:7002 172.19.0.4:7003 \
  172.19.0.5:7004 172.19.0.6:7005 172.19.0.7:7006 \
  --cluster-replicas 1

查看集群状态并模拟故障转移:

bash
# 查看集群信息
redis-cli -p 7001 cluster info
redis-cli -p 7001 cluster nodes

# 模拟主节点宕机(kill 7001 的容器)
docker-compose stop redis-cluster-1

# 等待 cluster-node-timeout(5 秒)后,查看集群状态
redis-cli -p 7002 cluster nodes
# 应该看到 7001 标记为 fail,其从节点 7004 被提升为主节点

# 恢复故障节点
docker-compose start redis-cluster-1
# 恢复后自动成为新主节点的从节点

常见误区与小结

  • 主从复制是异步的,不能保证强一致性:主节点写入成功后立刻返回,从节点可能尚未同步。如果主节点崩溃,已确认的写入可能丢失。
  • 哨兵不是 Redis 集群,不解决容量问题:哨兵只负责故障切换,所有数据仍在同一台机器上。
  • Cluster 不支持多 key 事务/管道跨槽MULTI/EXECMSETRPOPLPUSH 等操作要求所有 key 在同一槽。跨槽需要 hash tags{user:123}.name{user:123}.age 会落到同一槽)。这是应用层设计时必须考虑的约束。
  • MOVED 和 ASK 的区别经常被混淆:MOVED 是永久重定向,ASK 是临时迁移中重定向。客户端实现上,前者更新缓存,后者不更新。
  • Cluster 最小节点数不是 3:3 主 3 从是生产推荐,但理论上 3 主 0 从也能运行——只是没有副本可用,一旦某个主节点故障,其负责的槽全部不可用。

小结

从主从复制到哨兵再到 Cluster,Redis 的高可用方案沿着"读扩展 -> 自动故障切换 -> 水平分片"的路径演进。选型时先问三个问题:数据量是否超过单机内存?是否需要自动故障切换?QPS 是否超过单机处理能力?答案决定选哪种架构。

下一篇进入 L3 实战篇:34. 生产运维:部署规格、监控与 bigkey/hotkey 治理

参考

参考:Redis 官方文档 ReplicationSentinelCluster Specification;《Redis 设计与实现》第 11-14 章。

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1