主从复制、哨兵和 Cluster 有什么区别?

进阶高频对比约 7 分钟读完

一句话回答

三者解决的问题层层递进。主从复制把主节点的数据异步复制到从节点,提供数据冗余和读扩展,但主节点挂了要手动切换;哨兵在主从的基础上监控节点,主节点故障时自动选出新的主节点并通知客户端,解决了高可用,但写入和容量仍受限于一个主节点;Cluster 把数据按 16384 个哈希槽分片到多个主节点,每个主节点再带从节点,同时解决了容量、写扩展和高可用,客户端访问到错误的节点时会收到 MOVED 重定向。

详细解析

主从复制

从节点执行 REPLICAOF <主节点 IP> <端口> 后开始同步:

  1. 全量同步:第一次连接时,主节点执行 BGSAVE 生成 RDB 发给从节点,同时把这期间的新写命令缓存起来;从节点清空旧数据、加载 RDB,再执行缓存的命令
  2. 命令传播:之后主节点把每条写命令异步发给从节点,从节点定期汇报自己的复制偏移量(offset)
  3. 断线后的增量复制:从节点带着主节点的复制 ID 和自己的 offset 请求续传(PSYNC)。主节点有一个环形的复制积压缓冲区(repl-backlog-size,默认 1MB),缺失的部分还在缓冲区里,就只补发这一部分;否则重新全量同步

从节点默认只读,可以分担读流量,也可以在从节点上做持久化和备份,减轻主节点的压力。RDB 的原理见 RDB 和 AOF。复制是异步的,从节点可能读到旧数据;主节点宕机后,需要人工把某个从节点提升为主节点(REPLICAOF NO ONE),再让其他从节点和客户端改连新的主节点。

哨兵(Sentinel)

哨兵是独立运行的进程,一般部署 3 个及以上的奇数个:

  1. 监控:每个哨兵定期 PING 主从节点,超过 down-after-milliseconds 没有有效回复,就认为它主观下线
  2. 确认:认为主节点下线的哨兵数达到配置的 quorum,主节点被判定为客观下线
  3. 选举:哨兵之间选出一个领导者来执行故障转移,需要过半哨兵投票同意
  4. 故障转移:领导者按优先级、复制偏移量(数据越新越好)、运行 ID 从从节点里选出新的主节点并提升它,再让其他从节点复制新主节点
  5. 通知:客户端先向哨兵询问当前主节点的地址,再直接连接主节点读写;切换后哨兵会发布新主节点的地址,客户端据此重连

哨兵解决了自动故障转移,但仍然只有一个主节点负责写入,数据量也受限于单机内存。

Cluster

文本
             客户端(缓存了"槽 → 节点"的映射)
          /               |                \
  主 A:0 ~ 5460   主 B:5461 ~ 10922   主 C:10923 ~ 16383
       |                  |                   |
     从 A1              从 B1               从 C1
  • 分片:slot = CRC16(key) % 16384,每个主节点负责一部分槽,扩容缩容就是在节点之间迁移槽
  • 高可用:每个主节点带从节点,主节点故障时由其他主节点投票,把它的从节点提升上来,不需要哨兵
  • 重定向:命令发到了不负责这个槽的节点,会收到 -MOVED 3999 10.0.0.2:6379,客户端更新本地的映射后重新发送;槽正在迁移时会收到 -ASK,只对这一次请求转向目标节点,不更新映射
  • 多 key 操作:MGET、事务、Lua 脚本涉及的 key 必须在同一个槽里,否则报 CROSSSLOT 错误。可以用哈希标签 {user:1001}:profile、{user:1001}:orders,只有花括号里的部分参与计算,保证它们落在同一个槽
  • Cluster 只支持 0 号数据库

对比和选择

主从复制 哨兵 Cluster
解决的问题 数据冗余、读扩展 自动故障转移 数据分片、写扩展、自动故障转移
写入节点 1 个主节点 1 个主节点 多个主节点
容量上限 单机内存 单机内存 所有主节点内存之和
故障转移 手动 哨兵自动完成 节点之间投票自动完成
客户端 直接连主从节点 先向哨兵查询主节点地址 需要支持槽路由和重定向
多 key 操作 不受限制 不受限制 只能在同一个槽内
  • 数据量小、能接受人工切换:一主一从或一主多从
  • 数据能放进单机内存,需要自动故障转移:哨兵,比如一主两从加三个哨兵
  • 数据量或写入量超出单机的能力:Cluster

代码示例

用 ioredis 连接哨兵和 Cluster:

JavaScript
import Redis from 'ioredis'

// 哨兵:传入哨兵地址和主节点名称,ioredis 会找到当前的主节点,切换后自动重连
const redis = new Redis({
  sentinels: [
    { host: '10.0.0.11', port: 26379 },
    { host: '10.0.0.12', port: 26379 },
    { host: '10.0.0.13', port: 26379 },
  ],
  name: 'mymaster',
})

// Cluster:只需提供部分节点,ioredis 会拉取槽的分布,并自动处理 MOVED 和 ASK
const cluster = new Redis.Cluster([
  { host: '10.0.0.21', port: 6379 },
  { host: '10.0.0.22', port: 6379 },
])

面试官可能追问

主从切换会丢数据吗?什么是脑裂?

会。复制是异步的,主节点已经确认的写入,可能还没同步到从节点就宕机了。脑裂是指主节点和哨兵之间的网络断开了,但主节点本身还活着:哨兵选出了新的主节点,旧主节点仍在接收部分客户端的写入;网络恢复后,旧主节点变成从节点,清空数据去同步新主节点,这期间写入的数据就丢了。可以配置 min-replicas-to-write 1 和 min-replicas-max-lag 10,主节点发现能正常复制的从节点不够时拒绝写入,缩小丢失的窗口。

Cluster 为什么是 16384 个槽?

节点之间通过 Gossip 协议交换心跳,每条心跳消息都带着发送者负责哪些槽的位图。16384 个槽的位图是 2KB,如果用 65536 个槽就是 8KB,心跳的网络开销太大;而 Redis 作者建议的集群规模不超过 1000 个主节点,16384 个槽已经足够分配。

全量同步代价很大,怎么避免频繁发生?

全量同步要 fork 生成 RDB、占用网络带宽传输,从节点加载 RDB 期间通常也无法正常提供服务。避免频繁全量同步:调大 repl-backlog-size,让短暂断线后能增量续传;控制单个实例的内存大小;从节点很多时可以级联复制(从节点下面再挂从节点),减轻主节点的压力。

易错点

  • 哨兵只负责监控和故障转移,不存数据,也不做分片
  • Cluster 自带故障转移,不需要再部署哨兵
  • MOVED 会让客户端更新槽的映射,ASK 只是迁移期间的一次性转向
  • 主从复制是异步的,刚写完就从从节点读,可能读不到

AI 模拟面试官

用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮

登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录

这道题你掌握了吗?

选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。

学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。