Redis(三):主从复制、哨兵与集群
Redis(三):主从复制、哨兵与集群
导语:单机 Redis 有两个天花板:宕机就没数据(不可用)、内存装不下(不可扩展)。本篇按这个脉络展开:主从复制解决数据冗余、哨兵解决自动故障转移、Cluster 解决水平分片,并补齐 PSYNC 全量/增量同步、哨兵选主规则、MOVED/ASK 重定向、脑裂等高频追问题,共 14 题。
一、主从复制
1. 主从复制的原理?
答: Redis 复制分全量同步和增量同步两种,由 PSYNC 命令统一调度。
第 1 次连接(或无法增量时)——全量同步:
从库 主库
| PSYNC ? -1 -------------> (问:我是新人,不知道该续哪里)
| <---- FULLRESYNC <replid> <offset> (答:这是你的身份和起点)
| BGSAVE 生成 RDB(同时用复制缓冲区记录新写命令)
| <---- 传输 RDB 文件
| 清空本地数据、加载 RDB
| <---- 发送复制缓冲区中累积的写命令 (补齐 RDB 生成期间的增量)
| 执行到 offset 对齐,进入命令传播阶段之后的长连接——命令传播 + 增量同步:
- 主库每执行一条写命令,就异步发给所有从库,从库执行并维护自己的偏移量
offset; - 连接断开重连时,从库发
PSYNC <replid> <offset>; - 主库检查三件事都满足,才回复
+CONTINUE做增量同步(只补发缺失的那一段):replid与自己的(或历史replid2)一致;- 请求的
offset还在复制积压缓冲区(repl-backlog)范围内; - 没有被降级为「不认识的老从库」。
关键组件:
| 组件 | 作用 |
|---|---|
replid + offset | 标识「哪一份数据集」和「复制到哪」,相当于复制的坐标 |
复制积压缓冲区(repl-backlog-size,默认 1MB) | 环形缓冲区,保留最近一段写命令,供断线重连做增量同步 |
replid2 | 从库被提升为主库时保留旧 replid,让兄弟节点仍可增量同步(支持级联) |
加分点:全量同步是重操作(fork + RDB + 网络传输),一旦
repl-backlog太小、断连时间稍长就会退化成全量同步,引发 复制风暴。所以生产上要把repl-backlog-size调大(如 64~256MB),尤其在写入量大的实例上。
2. 主从复制延迟的原因与解决?
答: 复制是异步的,延迟天然存在。排查方向分四类:
| 原因 | 说明 | 解决 |
|---|---|---|
| 网络带宽/延迟 | 跨机房、大 value 写入、全量同步挤占带宽 | 同机房部署、repl-diskless-sync 或调大 repl-backlog-size、压缩 value |
| 从库被阻塞 | 从库执行慢命令(大 key 的 HGETALL、集合运算)、AOF always、内存不足触发淘汰 | 从库只读禁写、避免慢命令、replica-lazy-flush yes、no-appendfsync-on-rewrite yes |
| 主库写入压力过大 | 主库单线程忙不过来,命令堆积在复制缓冲区 | 拆分实例/分片、减少 bigkey、必要时加机器 |
| 从库数量过多(复制风暴) | 一个主挂多个从,每个从都消耗主库一份全量同步资源 | 用级联复制(从库再挂从库),或限制单主从节点数 |
监控指标:
INFO replication中的master_repl_offset与从库slave_repl_offset的差值(offset差就是积压的命令字节数);- 从库上的
master_link_status(up/down)、master_last_io_seconds_ago; - 主库
INFO stats里的sync_full/sync_partial_ok/sync_partial_err——sync_full持续增长说明总在全量同步,是重要警报。
业务侧兜底:关键读走主库、需要强一致时用
WAIT numreplicas timeout等待副本确认(注意它只是「等到足够副本确认」,不是分布式共识,仍可能丢数据)。
3. Redis 复制是同步还是异步?会丢数据吗?
答: 默认是异步复制——主库把命令写入本地内存并追加到输出缓冲区后就返回成功,不等从库确认。
因此存在丢失窗口:
- 主库执行成功但尚未发送/尚未被从库执行时主库宕机 → 这部分数据丢失;
- 哨兵把从库提升为新主时,新主可能缺少最后一段增量,这段数据就永久丢了;
min-replicas-to-write可以缓解但不能根治(见第 14 题)。
能做的三件事:
WAIT numreplicas timeout:写完之后显式等待 N 个副本确认(有限同步,提升可靠性但增加延迟);- 业务幂等 + 补偿:把 Redis 当缓存而非「唯一数据源」,丢了能从 DB 重建(最根本的方案);
- 金融级场景换方案:需要强一致时用 ZK/etcd 类的共识系统,或数据库本身来做权威存储。
一句话总结:Redis 主从复制是「最终一致」的,不是强一致。面试答「异步 + 会丢」,并补上业务兜底,就是完整答案。
二、哨兵(Sentinel)
4. 什么是哨兵?解决什么问题?
答: 主从复制只解决数据冗余,不解决自动故障转移——主库挂了需要人工把某个从库 REPLICAOF NO ONE 并通知所有客户端改地址。哨兵(Sentinel)就是把这个过程自动化。
哨兵是独立的进程(不存数据、只做协调),四个职责:
| 职责 | 说明 |
|---|---|
| 监控 | 持续 PING 主库、从库、其他哨兵 |
| 通知 | 节点异常时通过 API/脚本通知运维或告警系统 |
| 自动故障转移 | 主库客观下线后,选出一个从库提升为新主,让其他从库 REPLICAOF 新主 |
| 配置中心 | 对外提供「当前谁是主库」,客户端(Sentinel-aware client)通过它获取地址并订阅变更 |
下线判定的两级设计:
- 主观下线(SDOWN):单个哨兵在
down-after-milliseconds内没收到有效PING回复,就认为该实例「我认为它挂了」——仅是个人看法; - 客观下线(ODOWN):该哨兵询问其他哨兵,若同意的主库判定数达到 quorum,才认定主库真的下线,随后才进入故障转移。
为什么必须分两级:单哨兵会因自身网络抖动而误判,导致不必要的切主(切主本身有成本和数据丢失风险)。两级判定把「误判」变成了「多数共识」。
5. 哨兵为什么至少部署 3 个?
答: 因为哨兵的判定与选举都依赖多数派(majority),而多数派要求「超过一半」:
- 判定客观下线需要达到配置的
quorum(判定阈值); - 选举出执行故障转移的领导者哨兵需要多数哨兵(N/2 + 1)投票,且每个哨兵一轮只能投一票。
| 哨兵数量 | 多数派门槛 | 能容忍宕机数 |
|---|---|---|
| 1 | 1 | 0(单点,等于没做高可用) |
| 2 | 2 | 0(任一个挂就无法选举,比 3 个还不如) |
| 3 | 2 | 1 |
| 5 | 3 | 2 |
所以:
- 至少 3 个:才能容忍 1 个哨兵宕机同时还能选举;
- 推荐奇数个:避免平票;偶数不仅浪费一个节点,还可能因为「majority = N/2+1」反而更难达成一致;
- 哨兵要分布在不同物理机/可用区:否则一台物理机故障就同时干掉多个哨兵。
补充:
quorum只用于判定客观下线,选举领导者需要的是 majority(多数),不一定等于 quorum,这是常被混淆的点。生产建议quorum = N/2 + 1。
6. 哨兵如何判断下线、如何选出新主?
答: 完整流程分四步:
① 主观下线 SDOWN 单哨兵超时未收到 PING 回复
② 客观下线 ODOWN 询问其他哨兵,同意数 >= quorum
③ 选举领导者 Sentinel 该哨兵参选,需获得多数哨兵投票(Raft 风格的任期机制)
④ 选择新主并切换 由领导者哨兵从从库中挑一个,执行 REPLICAOF NO ONE + 通知其他从库第 ④ 步「挑哪个从库」的规则(高频追问):
先过滤,再排序:
过滤掉(候选淘汰):
- 处于主观下线状态的从库;
- 与主库断连时间过长的从库(超过
down-after-milliseconds × 10+ 主库下线时长); replica-priority设为 0 的从库(表示永不参与选主)。
排序(越靠前越优先):
| 优先级 | 字段 | 规则 |
|---|---|---|
| 1 | replica-priority | 越小越优先(默认 100) |
| 2 | 复制偏移量 offset | 越大越优先(数据最新,尽量少丢) |
| 3 | runid | 字典序越小越优先(前两项都平局时的兜底,保证结果确定) |
切换完成后: 领导者哨兵让被选中的从库 REPLICAOF NO ONE 升为主,其余从库 REPLICAOF 新主,并把旧主标记为从库(它恢复后会变成新主的从库),最后通过发布订阅广播新配置给客户端。
实战要点:可以把机房内/延迟低的从库
replica-priority调小,实现「就近选主」;把只读分析型从库设为0,避免它被选成主库。
7. 哨兵和集群有什么区别?如何选型?
答: 两者解决的问题根本不同,也常被拿来一起对比:
| 维度 | 主从 + 哨兵 | Redis Cluster |
|---|---|---|
| 核心能力 | 高可用(自动故障转移) | 高可用 + 水平扩展(分片) |
| 数据分布 | 每个节点都有全量数据 | 每个节点只存一部分槽的数据 |
| 容量上限 | 受单机内存限制 | 可横向扩展,突破单机内存 |
| 写入扩展 | 无法扩展(所有写都在主库) | 可扩展(多主分片) |
| 客户端 | 需 Sentinel-aware 客户端拿主库地址 | 需支持 MOVED/ASK 重定向的集群客户端 |
| 多 key 操作 | 支持(同一实例内) | 受限(必须同槽,见第 12 题) |
| 运维复杂度 | 较低 | 较高(槽迁移、集群状态、扩容缩容) |
| 适用场景 | 数据量 < 单机内存、QPS 可被单主承载 | 数据量大、写 QPS 高,需要水平扩展 |
选型建议:
- 单实例数据量能装下(如 16~32GB 以内)、瓶颈在可用性 → 主从 + 哨兵,简单可靠、运维成本低、功能不受限;
- 数据量或写流量已超出单机 → Cluster;
- 两者可组合:Cluster 中每个分片自己也是主从结构,槽的故障转移由集群内部完成(不依赖哨兵)。
注意:哨兵和 Cluster 不能叠加使用(Cluster 自带故障转移),这点常被问。
三、Cluster 集群
8. Redis Cluster 是什么?哈希槽机制?
答: Redis Cluster 是服务端分片的分布式方案,无中心代理(不是 Proxy 模式),把数据分布在多个主节点上。
哈希槽(Hash Slot)机制:
- 整个 key 空间被划分为固定的 16384 个槽;
- 槽位计算:
slot = CRC16(key) mod 16384; - 每个主节点负责一部分槽(可以手动分配或
--cluster rebalance); - 每个主节点可挂若干从节点,主挂则从顶。
客户端访问流程(重定向):
客户端 -> 任意节点:GET user:1
节点计算 slot = CRC16("user:1") mod 16384 = 7000
若 7000 不归我管 -> 返回 MOVED 7000 127.0.0.1:7002
客户端 -> 7002 节点:重新请求(并把槽映射缓存起来,下次直接命中)两种重定向要区分清楚(高频追问):
| 重定向 | 触发场景 | 客户端行为 |
|---|---|---|
MOVED <slot> <ip:port> | 槽已确定归属另一个节点(如扩缩容完成后) | 更新本地槽映射,之后请求直接发给新节点 |
ASK <slot> <ip:port> | 槽正在迁移中,该 key 已迁走但槽还没正式移交 | 只对本次请求临时转发到目标节点,不更新槽映射(下次仍按原映射请求,因为槽归属还没变) |
其他关键机制:
- 节点间通信:用二进制协议在 Cluster Bus 上做
PING/PONG(gossip),默认端口为服务端口 + 10000; - 槽的位图:每个节点维护一份 16384 位的位图,记录哪些槽归自己,心跳包中携带该位图做信息交换;
- 客户端需要支持集群协议(Jedis/Lettuce 的 Cluster 模式、
redis-cli -c),否则无法处理重定向。
9. 集群下某个节点挂了数据会丢吗?
答: 分三种情况:
| 情况 | 结果 |
|---|---|
| 主节点挂,且有从节点 | 集群自动故障转移,从节点升主,服务基本不中断;但可能丢最后一段未同步的增量(异步复制,见第 3 题) |
| 主节点及其所有从节点都挂 | 该主负责的槽整体不可用;默认 cluster-require-full-coverage yes 时,整个集群会拒绝所有查询(返回 CLUSTERDOWN),即使其他槽的数据完好 |
| 槽有主但从库不足 / 投票失败 | 无法完成故障转移,槽持续不可用 |
关键配置:
cluster-require-full-coverage yes(默认):只要有槽不可用,整个集群停止对外服务——可用性换一致性的思路。如果业务能接受「部分数据不可用」,可设为no,让健康槽继续服务;- 每主至少 1 从是 Cluster 的基本要求,否则主挂就等于数据不可用;
cluster-node-timeout(默认 15000ms):判定节点失联的阈值,调小切换更快但更容易误判。
实践建议:Cluster 至少要 3 主 3 从(共 6 节点),既满足「3 主才能达成多数派」的选举要求,又能容忍单机故障。
10. 为什么哈希槽是 16384 个?
答: 这是节点间通信开销与分片粒度之间的平衡,官方(antirez)给出的理由主要有三条:
- 心跳包体积:节点间每次心跳都要携带「我负责哪些槽」的信息。用位图表示时,16384 个槽 = 16384 bit = 2KB;如果改成 65536 个槽,就是 8KB,而心跳是高频消息,4 倍的带宽与解析开销不划算;
- 16384 足够用:Redis Cluster 建议的节点规模在 1000 个主节点以内。16384 / 1000 ≈ 16 个槽/节点,仍然够用,不会出现「槽不够分」的情况;
- 位图与消息大小可控:槽位图在 gossip 消息里传递,槽数越多消息越大,集群规模扩大时心跳开销会非线性增长。
顺带一提:槽数固定也是集群扩缩容的一个约束——不能像一致性哈希那样「加节点只影响相邻区间」,但固定槽位 + 位图交换让槽归属的表达与传播变得极其简单,这是工程上的权衡。(对比:Codis 用的是 1024 个槽。)
11. 集群的故障检测与自动故障转移?
答: 流程与哨兵类似但判定主体不同(哨兵是独立进程,集群是节点之间互相投票):
① 主观下线 PFAIL 某节点在 cluster-node-timeout 内没收到 PONG
② 客观下线 FAIL 该节点通过 gossip 传播 PFAIL,
当「认为它挂了的节点数」超过半数主节点时,标记为 FAIL
③ 从节点发起选举 从节点发起投票,向所有持有槽的主节点拉票
④ 获得多数票则晋升 拿到「超过半数主节点」的票 -> 提升为主节点
⑤ 接管槽并广播 接管故障主节点的槽,通过广播让集群更新槽映射三个关键细节:
- 有投票权的只有「持有槽的主节点」,从节点无投票权;
- 选举需要多数票(N/2 + 1),所以主节点数量最好是奇数;这也是「集群至少要 3 主」的原因——2 主时挂掉 1 个就无法达成多数派(1 票 < 2);
- 被隔离的旧主不会「复活」成主:当旧主网络恢复后,它发现自己的槽已被接管、且自己的配置纪元(
configEpoch)更旧,会自动降级为从节点并同步新主。
故障转移期间的客户端体验:
- 部分请求会收到
CLUSTERDOWN或重定向; - 客户端应配置合理的重试与超时,并在收到
MOVED后更新本地槽映射; - 转移的耗时主要取决于
cluster-node-timeout+ 选举轮次。
12. 集群有哪些使用限制?
答: 这些限制必须在架构设计阶段就考虑,否则改造代价很大:
| 限制 | 说明与应对 |
|---|---|
| 多 key 操作必须同槽 | MGET、SINTER、RENAME 等要求所有 key 在同一个槽。可用 hash tag 强制同槽:{user10086}:name 和 {user10086}:age 中的 {} 内容参与 CRC16,因此落在同一槽 |
| 跨槽事务与 Lua 不可用 | MULTI/EXEC 与 Lua 脚本涉及的所有 key 必须同槽,跨槽会报错 |
| 不支持多数据库 | 集群只有 db 0,SELECT 不可用(这是为分布式一致性的简化) |
| 不支持发布订阅的全局广播(Redis 7.0 前) | 普通 PUBLISH 只在本节点广播;Redis 7.0 起新增 SSUBSCRIBE/SPUBLISH(分片 Pub/Sub) 支持集群内按槽广播,但语义是「按分片」而非全局 |
KEYS、SCAN 只作用于单节点 | 要全集群扫描必须客户端遍历所有节点并聚合 |
| 槽不可用影响面大 | 默认 cluster-require-full-coverage yes 时,任一槽不可用则整体不可用 |
| 客户端必须支持集群协议 | 否则无法处理 MOVED/ASK;使用连接池时要注意「槽迁移期间的重试」 |
| 运维更复杂 | 槽迁移、configEpoch 冲突、扩容时的数据搬迁都需要工具(redis-cli --cluster)与演练 |
设计建议:在集群模式下尽量不要设计需要跨 key 原子操作的业务;确实需要时,用 hash tag 把相关 key 聚到同槽(但要注意别把热点全聚到一个槽上,形成新的倾斜)。
13. 集群如何扩容/缩容?
答: 核心动作就是槽的搬迁,命令层面由 redis-cli --cluster 封装:
| 操作 | 命令 |
|---|---|
| 加节点 | redis-cli --cluster add-node <新节点> <任一现有节点>(新节点初始槽数为 0) |
| 迁移槽 | redis-cli --cluster reshard <任一现有节点>,交互式指定迁多少槽、从哪些节点迁、给哪个节点 |
| 均衡槽 | redis-cli --cluster rebalance <任一现有节点> [--use-empty-masters] |
| 加从节点 | add-node --cluster-slave --cluster-master-id <主节点ID> |
| 删节点 | redis-cli --cluster del-node <节点> <节点ID>(必须先把它的槽全部迁走) |
迁移过程中的数据一致性(重点):
单个槽的迁移是逐个 key 进行的:
① CLUSTER SETSLOT <slot> MIGRATING <目标节点> # 源节点:标记槽为迁出中
② CLUSTER SETSLOT <slot> IMPORTING <源节点> # 目标节点:标记槽为迁入中
③ MIGRATE 逐个 key 迁移(期间源节点对已迁走的 key 返回 ASK 重定向)
④ CLUSTER SETSLOT <slot> NODE <目标节点> # 双方都执行,正式移交槽归属为什么迁移期间需要 ASK(见第 8 题):槽归属还没变(客户端缓存里仍是源节点),但被请求的 key 可能已经迁走了。所以源节点只能对这一个 key 做临时重定向,不能改整个槽的映射。
关键保障:
- 迁移不阻塞整体服务,只是被迁移的 key 在极短时间内会有一次额外跳转;
- 迁移前务必确认新节点容量与带宽,大 key 的
MIGRATE会长时间阻塞(先清理 bigkey); - 迁移期间监控
CLUSTER INFO的cluster_state与节点间MIGRATING/IMPORTING状态,异常时要能回退; - 低峰执行 + 演练,缩容前先确认目标节点的槽已全部迁出。
14. 什么是脑裂?如何避免?
答: 脑裂(Split-Brain)指网络分区导致出现两个「自认为的主库」,各自接受写入,恢复后其中一个的写入被丢弃。
Redis 中的发生过程:
① 主库 A 与哨兵/从库/客户端之间网络断开(但 A 自身还活着)
② 哨兵判定 A 客观下线,从从库中选出 B 提升为新主
③ 与此同时,客户端仍连着 A(网络对客户端是通的),继续往 A 写数据
④ 网络恢复,A 被降级为 B 的从库 -> 执行全量/增量同步
⑤ A 在「孤立期间」写入的数据被覆盖,永久丢失危害:客户端以为写入成功,实际数据丢了——静默丢数据比宕机更可怕。
解决办法(配置层面的兜底):
| 参数 | 作用 |
|---|---|
min-replicas-to-write <N> | 主库只有在至少有 N 个从库在线时才接受写请求 |
min-replicas-max-lag <秒> | 从库的「最后确认延迟」必须小于该值才算「在线」 |
两者配合(如 min-replicas-to-write 1 + min-replicas-max-lag 10) | 主库被隔离后,从库数量条件不满足,直接拒绝写入,从根上避免孤立主库继续收数据 |
其他相关防线:
- 哨兵侧:
quorum设为多数派,避免单哨兵误判;哨兵跨物理机部署; - 集群侧:Cluster 天然靠多数派保护——少数派分区中的主库无法获得多数主节点投票,会主动停止服务(
CLUSTERDOWN),因此集群下的脑裂风险比「主从 + 哨兵」小得多; - 业务侧:关键写入做幂等与对账,把 Redis 定位为缓存而非唯一数据源。
面试答法:先讲清「脑裂 = 网络分区 + 主库仍可写」,再给
min-replicas-to-write这个具体配置,最后补一句「Redis 是异步复制,脑裂本质上是异步复制的必然风险,只能通过限制写入来缓解,不能彻底消除」。
下一篇:《Redis(四)》讲缓存治理——穿透/击穿/雪崩的本质区别与解法、布隆过滤器的原理与局限,以及缓存与数据库一致性(Cache-Aside、延迟双删、binlog 订阅)的完整分析。
