高可用架构深度:主从复制、哨兵与 Cluster

概述

衔接前文段落

前文《持久化双雄》深入拆解了 Redis 在单机层面 通过 RDB 快照与 AOF 日志实现数据持久化的完整原理。然而,单机持久化无法解决物理宕机、机房故障导致的服务不可用 问题------当唯一的主机意外宕机时,所有依赖 Redis 的业务都会陷入瘫痪。为突破单点瓶颈,Redis 设计了一套层层递进的多机高可用体系:主从复制(数据冗余)→ 哨兵(自动故障转移)→ Cluster(水平扩展) 。本文将沿着这条演进路径,从 PSYNC 协议中的复制偏移量追踪,到哨兵的 SDOWN/ODOWN 判定流程,再到 Cluster 的 hash slot 路由与 Gossip 协议状态传播,系统剖析 Redis 分布式架构的内核设计。

总结性引言

当你用 Redis 存储了数亿个 Key,单机早已不够用。主从复制让数据有了冗余副本,哨兵让故障切换不再依赖人工,Cluster 让数据分散到多个节点实现水平扩展。然而,PSYNCrepl-backlog 设置过小会导致频繁全量同步,哨兵的 quorum 配置不当可能引发脑裂,Cluster 在 hash slot 迁移期间产生的 ASK 重定向也会对延迟敏感的业务造成影响。理解这些机制的底层协议、核心参数与配置权衡,是构建生产级 Redis 高可用架构的前提。

核心要点

  • 主从复制PSYNC + Replication ID + offset 的全量/部分同步;repl-backlog-size 积压缓冲区;repl-diskless-sync 无盘复制。
  • 哨兵机制 :SDOWN/ODOWN 判定;Leader Election(Raft 变体);故障转移执行;quorumparallel-syncs 调优。
  • Cluster 集群hash slot 分片(CRC16(key) % 16384);MOVED/ASK 重定向;Gossip 协议故障检测;cluster-node-timeoutcluster-require-full-coverage
  • 脑裂避免min-replicas-to-write + min-replicas-max-lag 在哨兵和 Cluster 模式下的写入安全保证。

文章组织架构图

flowchart TD A["1. 主从复制:全量同步与部分同步"] B["2. 主从复制的关键参数与无盘复制"] C["3. 哨兵机制:SDOWN、ODOWN 与故障转移"] D["4. 哨兵的 Leader Election 与配置"] E["5. Cluster 集群:hash slot 与分片路由"] F["6. Cluster 的 MOVED/ASK 与 Gossip 协议"] G["7. 脑裂成因与避免策略"] H["8. 面试高频专题"] A --> B --> C --> D --> E --> F --> G --> H

架构图说明

  • 总览说明 :全文 8 个模块从主从复制 的基础数据冗余出发,逐步深入到哨兵 的自动故障转移和 Cluster 的水平扩展,最后以脑裂避免面试高频专题收尾,形成从单点到分布式、从数据冗余到自动容灾的完整认知路径。
  • 逐模块说明
    • 模块 1-2 :建立数据冗余的基础认知。拆解全量与部分同步的协议细节、repl-backlog 的工作机制以及无盘复制和写入安全保证,为后续理解故障转移提供数据层基础。
    • 模块 3-4 :揭示自动故障转移的判定与执行。从 SDOWN/ODOWN 的量化判定,到基于 Raft 变体的 Leader Election,再到 SLAVEOF NO ONE 的执行细节,完整复盘哨兵自动切换的全过程。
    • 模块 5-6 :解析水平扩展的路由与状态传播。深入 hash slot 的分片模型,对比 MOVEDASK 重定向的场景差异,并通过 Gossip 协议的 PFAIL/FAIL 状态传播,呈现 Cluster 的去中心化自治设计。
    • 模块 7 :分析高可用架构中的最大风险------脑裂。指出网络分区下哨兵误判的根因,并给出 min-replicas-to-writecluster-require-full-coverage 的组合防御方案。
    • 模块 8:面试巩固。通过 12+ 道经典面试题(含系统设计题),以"一句话回答 + 详细解释 + 多角度追问 + 加分回答"的格式,将理论落地为可输出的面试竞争力。
  • 关键结论Redis 的高可用体系通过三层递进实现------主从复制通过 PSYNC + offset 实现数据冗余,哨兵通过 SDOWN/ODOWN + Raft 变体实现自动故障转移,Cluster 通过 hash slot + Gossip 实现水平扩展。理解 repl-backlogquorumcluster-node-timeout 等关键参数的调优,是构建稳定 Redis 分布式架构的前提。

0. 总纲:Redis 高可用架构的演进逻辑与总体设计

Redis 的高可用体系并非一次性设计完成,而是沿着数据冗余 → 自动故障转移 → 水平扩展的路径逐步演化。这一演进过程与业界的分布式系统发展规律高度吻合,同时也深刻反映了 Redis 作为内存数据库对性能、简洁性和可运维性的极致追求。在进入具体模块之前,需要先建立全局视角,理解三个阶段各自解决了什么核心问题,又遗留了什么新挑战,以及它们之间的衔接关系。

1. 从单机到主从:数据冗余是基石

单机 Redis 通过 RDB 和 AOF 保证了持久性(详见第 4 篇),但宕机必然导致服务中断。即使有持久化文件,重启恢复也需要分钟级时间,对于高并发业务不可接受。主从复制的引入,本质是将数据异步地镜像到多个副本节点,实现:

  • 读扩展:通过读写分离,将读流量分担到从库。
  • 数据冗余:一台节点宕机,其他节点仍有完整数据副本。
  • 故障接管的基础:允许管理员手动将一台从库提升为主库,恢复写入服务。

然而,主从复制架构仍然存在明显短板:故障时需要人工介入,切换时间长,且客户端需要感知主库地址变化。此时,高可用的下一个层次被提上日程:自动化故障转移

2. 从主从到哨兵:自动故障转移是质变

哨兵(Sentinel)本质上是一个分布式监控系统,它独立于数据节点部署,通过定期心跳检测主从节点的存活状态,并在主库故障时自动完成:故障发现(SDOWN→ODOWN)→ Leader 选举 → 新主库提升 → 从库重定向 → 通知客户端。这一过程的自动化,使得 Redis 真正具备了生产级的高可用能力。哨兵架构的关键设计在于:

  • 去中心化哨兵集群:避免单点哨兵本身成为故障点。
  • 主观/客观下线双层判定:兼顾检测灵敏度和防误判。
  • 基于 Raft 变体的 Leader 选举:保证故障转移的执行权唯一。

但哨兵只解决了"高可用"问题,并未解决"数据量"问题。随着业务增长,单台主库的内存容量和写入吞吐终究会到达硬件极限。此时,需要将数据分片 ,让多个主库共同承载数据,这就是集群(Cluster) 出现的背景。

3. 从哨兵到 Cluster:水平扩展是终极形态

Redis Cluster 通过将整个键空间划分为 16384 个哈希槽 ,并将槽分配给不同的主节点,实现了数据的自动分片。同时,每个主节点都可以挂载从节点,且从节点会在主节点故障时自动发起去中心化的选举,提升为新主节点,继续提供服务。Cluster 架构融合了高可用与水平扩展,其设计上的核心创新在于:

  • 无中心架构 :所有节点对等,通过 Gossip 协议 交换状态,没有代理层,客户端直接连接节点。
  • 智能客户端 :客户端缓存槽映射表,并在收到 MOVED/ASK 重定向时动态更新路由。
  • 自动故障转移:由从节点发起,主节点投票,完全去中心化执行。

4. 贯穿始终的核心挑战:脑裂与一致性权衡

从主从复制到哨兵再到 Cluster,网络分区(Split-Brain) 始终是分布式系统无法绕过的理论障碍。Redis 在不同架构下提供了不同的防护手段:

  • 主从复制层面 :通过 min-replicas-to-writemin-replicas-max-lag 让主库在从库数量不足时主动拒绝写入,这是最底层的防御。
  • 哨兵层面 :通过 quorum 和多数派 Leader 选举,保证切换决策的合法性,但无法完全避免分区时的双主写入。
  • Cluster 层面 :通过 cluster-require-full-coverage 和主节点投票机制,限制槽不可用时的服务范围。

理解了这一总体演进脉络,就能明白为何后文的每个技术细节------PSYNCrepl-backlog 大小、哨兵的 down-after-milliseconds 设置、Cluster 的 cluster-node-timeout 值------都会在系统的可靠性、可用性和性能之间产生微妙的牵制。接下来,我们进入各个模块的深度拆解。


第一部分:主从复制深度剖析

主从复制是 Redis 多机方案的根基。理解它,需要从复制协议的演进、同步过程的细节以及核心参数的调优三个层面展开。

1.1 复制协议的演进:SYNCPSYNC

Redis 2.6 及之前版本只支持 SYNC 命令。无论从库是初次连接还是断线重连,都必须进行全量同步。其流程为:

  1. 从库向主库发送 SYNC
  2. 主库执行 BGSAVE 生成 RDB 文件,同时使用复制缓冲区replication buffer)暂存此期间的所有写命令。
  3. 主库将 RDB 文件发送给从库。
  4. 从库清空自身数据库,载入 RDB 文件。
  5. 主库再将缓冲区中的增量命令发送给从库,从库执行这些命令,最终达到与主库数据一致。

这种设计的致命缺陷是:如果从库因网络抖动断开几秒钟,重连后又要进行一次全量同步。对于内存上百 GB 的实例,BGSAVE 的 CPU、内存和磁盘开销极大,从库恢复时间漫长,且主库在 COW 期间的内存额外消耗会加剧系统压力。

Redis 2.8 引入了 PSYNC ,其核心思想是:让主从双方共同维护一个复制状态 ,使得断线重连后能够增量续传PSYNC 根据参数区分两种模式:

  • PSYNC ? -1:表示请求全量同步(首次同步或部分同步失败时)。
  • PSYNC <replication_id> <offset>:表示请求部分同步,即基于已知的复制 ID 和偏移量请求增量数据。

1.2 复制状态的数学描述:Replication ID + offset

Replication ID(主库复制 ID) 是一个 40 位十六进制随机字符串,用于唯一标识一个"复制数据集"的版本。虽然每个 Redis 节点都有全局唯一的 runid,但 replid 并非直接等于 runid。当节点成为主库时,会生成一个新的 replid。其作用是:当从库连接时,主库通过比对 replid 来判断从库先前的主库是否与自己拥有相同的数据集 。若从库携带的 replid 与主库当前 replid 不同,但等于主库的 replid2(即旧主库 ID),且偏移量在积压缓冲区范围内,则仍然允许部分同步。这在发生故障转移或主库重启后十分有用------新主库在提升时会保存旧主库的 replid 作为 replid2,从而为旧从库的续传提供了可能。

复制偏移量(offset) 是一个从 0 开始不断递增的字节计数。主库每向复制流发送一个字节,其 master_repl_offset 就增加相应数量;从库每接收并执行一个字节,其 slave_repl_offset 也同步增加。偏移量精确地标识了主库和各个从库在复制流中的位置。

当从库发起 PSYNC <replid> <offset> 时,主库执行以下判断逻辑(伪代码):

c 复制代码
if (从库的 replid == 我的 replid || 从库的 replid == 我的 replid2) {
    if (从库的 offset+1 在我的 repl_backlog 范围内) {
        // 部分同步:从 backlog 中取出从 offset+1 开始的数据发送
        返回 +CONTINUE;
    } else {
        // backlog 已不包含所需数据,必须全量同步
        返回 +FULLRESYNC <我的新 replid> <当前 offset>;
    }
} else {
    // replid 完全不匹配,全量同步
    返回 +FULLRESYNC <我的 replid> <当前 offset>;
}

1.3 全量同步与部分同步的详细过程

全量同步细节

当从库需要全量同步时,流程如下:

  1. 从库请求PSYNC ? -1
  2. 主库回复+FULLRESYNC <replid> <offset>。该 offset 通常是主库当前 master_repl_offset,作为该从库后续部分同步的起点。
  3. 主库 BGSAVE :主库 fork() 子进程执行 RDB 生成。此过程利用了操作系统的 Copy-On-Write(COW) 机制,主进程在此期间仍然可以正常处理写请求,新增修改的内存页会被复制一份供子进程读取(详见第 4 篇)。
  4. 累积复制缓冲区fork 完成后,主库将所有新写入命令同时发送到复制缓冲区 (该从库专属)和积压缓冲区(全局共享)。
  5. 传输 RDB:主库将 RDB 文件通过 Socket 直接发送给从库。
  6. 从库载入:从库收到 RDB 后,首先清空自身数据库,然后加载 RDB 数据。
  7. 发送增量命令:RDB 传输完毕后,主库将复制缓冲区中累积的增量命令发送给从库,从库逐条执行,最终追平与主库的偏移量。
  8. 进入命令传播阶段 :从库状态变为 online,此后主库的每一条写入命令都会实时发送给从库。

部分同步细节

部分同步仅在断线重连时发生:

  1. 从库重连 :从库向主库发送 PSYNC <replid> <slave_repl_offset>
  2. 主库校验 :调用 masterTryPartialResynchronization() 进行匹配检查。
  3. 若匹配成功 :主库回复 +CONTINUE,然后从积压缓冲区中定位到从库请求的 offset+1 位置,开始连续发送积压的写命令。
  4. 从库追齐:从库接收这些命令并执行,直至与主库最新偏移量一致,然后再次进入命令传播阶段。

关键参数:repl-backlog-sizerepl-backlog-ttl

积压缓冲区(replication backlog)是主库内存中的一个环形缓冲区 ,默认大小为 1MB。它独立于每个从库的复制缓冲区,是所有从库共享 的历史命令记录。当所有从库都与主库同步良好时,积压缓冲区中仍保留最近 1MB 的写命令历史。一旦某个从库断线,只要它的 slave_repl_offset 还在积压缓冲区覆盖的范围内,重连后就能实现部分同步。

窗口时间计算

scss 复制代码
最大断线时间(秒) ≈ repl_backlog_size / 主库平均写入速率(字节/秒)

例如主库写入带宽为 10MB/s,默认 1MB 的积压缓冲区仅能容忍 0.1 秒 的断线,这在生产环境中几乎不可能。必须调大。

repl-backlog-ttl(默认 3600 秒)指定:当所有从库都断开连接后,积压缓冲区在内存中保留的时间。超过此时间,积压缓冲区被释放,后续连接只能全量同步。

生产调优建议

  • 监控 total_partial_resync_need_full_sync 指标,若该值持续增长,说明部分同步频繁失败,应考虑增大 repl-backlog-size
  • 对于写入密集型应用(如计数器、排行榜),建议 repl-backlog-size 设置为 64MB ~ 256MB,以提供 60 秒以上的断线容忍窗口。
  • 内存紧张时可适当降低,但需接受全量同步频率增加。

主从复制的全量同步与部分同步对比序列图

sequenceDiagram participant S as 从库 participant M as 主库 Note over S,M: === 全量同步 PSYNC ? -1 === S->>M: PSYNC ? -1 M-->>S: +FULLRESYNC M->>M: fork 子进程 BGSAVE,期间新写入存入复制缓冲区 M->>S: 发送 RDB 文件 S->>S: 清空旧数据,加载 RDB M->>S: 发送复制缓冲区中的增量命令 S->>S: 执行增量命令,追齐 Note over S,M: 进入命令传播阶段 Note over S,M: === 部分同步(断线重连) === S-->>M: 网络中断 M->>M: 持续写入,积累至 repl_backlog S->>M: 网络恢复,PSYNC M->>M: 检查 replid 是否匹配,offset 是否在 backlog 范围 alt 条件满足 M-->>S: +CONTINUE M->>S: 发送 backlog 中从 offset+1 开始的增量命令 S->>S: 执行增量,追齐 Note over S,M: 部分同步完成 else 条件不满足 M-->>S: +FULLRESYNC Note over S,M: 退化为全量同步 end

图1 说明

  • 序列角色:从库是同步请求的发起方,主库是数据提供方和状态判定方。
  • 全量同步阶段 :主库在 fork 之后就将自己当前的 master_repl_offset 作为从库的起始偏移量返回。后续即使 RDB 生成耗时较长,从库也能通过增量命令补全期间的写入。
  • 部分同步阶段 :主库的判定是基于 replid 匹配性和 offset 的落点。+CONTINUE 返回后,主库直接从积压缓冲区中顺序发送命令,无需任何磁盘操作。
  • 关键设计repl_backlog 使用环形数组实现,通过 repl_backlog_off(缓冲区起始偏移量)和 repl_backlog_idx(当前写入位置)管理空间。如果从库请求的 offset 小于 repl_backlog_off,则说明历史命令已被覆盖,只能全量同步。

第二部分:无盘复制与写入安全机制

2.1 无盘复制(Diskless Sync)详解

传统全量同步在将 RDB 传给从库之前,需要先将 RDB 内容写入主库的磁盘文件。当主库磁盘性能较差(例如使用网络存储或 HDD)时,磁盘写入可能成为整个同步过程的瓶颈,拖长从库上线时间,并且占用额外磁盘空间。

无盘复制(Redis 2.8.18+)改变了这一流程:主库不再将 RDB 落盘,而是直接在子进程中通过 Socket 将 RDB 数据流式传输给从库。其内部步骤为:

  1. 主库 fork() 子进程。
  2. 子进程创建与从库的 Socket 连接(复用从库发起的复制连接),然后开始遍历整个数据集,将键值对按照 RDB 协议编码后直接写入 Socket。
  3. 与此同时,主进程继续处理命令,将新写入暂存在复制缓冲区中。
  4. 子进程传输完所有数据后退出。主进程检测到子进程结束,随后将复制缓冲区中的增量命令发送给从库,完成数据对齐。

配置方式

bash 复制代码
repl-diskless-sync yes
repl-diskless-sync-delay 5   # 等待 5 秒,以便多个从库可以共享同一轮传输

repl-diskless-sync-delay 设为 5 秒时,第一个从库到达后主库不会立即开始无盘传输,而是等待最多 5 秒。若在此窗口内有其他从库也请求全量同步,它们会被挂入同一批传输队列中,主库仅 fork 一次子进程,同时向多个从库发送 RDB 流。这极大地节约了 CPU 和内存开销(避免多次 fork 和 COW 副本)。

适用场景与限制

  • 推荐使用:主库磁盘为低速网络存储、云盘,而网络带宽充裕(如内网万兆)。大数据量(>100GB)实例需要频繁全量同步时。
  • 谨慎使用:跨地域公网传输或带宽有限的环境。多个从库同时接收会成倍增加网络出口流量。此外,无盘传输过程中如果从库断线,必须重新开始整个同步流程,没有断点续传机制(与 AOF 重写不同)。

Redis 7 的增强:Redis 7.0 优化了无盘复制子进程的 Socket 缓冲区管理,使用了更大的发送缓冲区并改写了事件循环,减少了因 Socket 缓冲区满而导致的子进程停顿,提升了传输效率。

2.2 写入安全保证:min-replicas-to-writemin-replicas-max-lag

Redis 的主从复制是异步的,这意味着主库在执行完写命令后不会等待从库确认即返回给客户端。这种设计获得了极高的性能,但代价是如果主库在命令传播到从库之前宕机,或者发生网络分区导致主库孤立,那么这段时间的写入将永久丢失,甚至在哨兵切换后导致数据不一致(脑裂)。为了在性能和安全之间取得平衡,Redis 提供了主库侧的自我保护机制:

arduino 复制代码
min-replicas-to-write 1
min-replicas-max-lag 10
  • min-replicas-to-write <N>:主库必须至少有 N 个从库处于"健康"状态,才接受写入。
  • min-replicas-max-lag <M>:上述"健康"状态的从库,其与主库的延迟(lag)必须小于 M 秒。延迟是指从库最后一次成功接收复制数据的时间与当前时间之差。

实现原理 : 在每条写命令执行前,主库会调用 checkGoodReplicasStatus() 统计当前在线且延迟小于 min-replicas-max-lag 的从库数量。若数量不足 N,主库拒绝命令,并返回错误:

lua 复制代码
(error) NOREPLICAS Not enough good replicas to write.

生产意义

  • 脑裂防护:当网络分区导致主库与所有从库(或哨兵)断开时,主库很快会发现从库延迟飙升或连接断开,从而停止接受写入,避免了"分区主库"孤立写入产生的数据冲突。
  • 数据安全:即使没有脑裂,该机制也能保证写入至少有 N 个副本在短时间内被同步,降低了主库崩溃导致的数据丢失风险。

注意事项

  • 这两个参数不适用于多级复制的中间从库(即从库的从库),仅在顶级主库上配置有效。
  • 设置过高会导致可用性降低:如果某时段从库因网络原因延迟增加,主库直接拒绝写入,可能造成业务中断。因此需要根据业务对一致性的要求谨慎取值,常见配置为 min-replicas-to-write 1min-replicas-max-lag 10

第三部分:哨兵机制深度拆解

哨兵是 Redis 官方提供的面向主从复制架构的高可用解决方案。它由一组哨兵进程组成,它们相互协作,完成监控、通知、自动故障转移和配置提供四大职责。

3.1 哨兵集群的自动发现与连接拓扑

每个哨兵节点与主库、从库之间维护两条连接:

  • 命令连接 :用于定时 PINGINFO,以及执行故障转移命令。
  • 发布订阅连接 :每个哨兵会订阅所有被监控节点的 __sentinel__:hello 频道(默认每 2 秒发布一次自身信息)。通过这个频道,哨兵可以自动发现其他监控同一主库的哨兵,无需在配置文件中手动列出所有哨兵地址,大幅简化了部署。

哨兵集群通过定期执行 INFO 命令从主库获取从库列表,因此整个拓扑的发现完全自动化。当新从库加入或移除,哨兵会在几个周期内更新自己的内部状态。

3.2 主观下线(SDOWN)判定

SDOWN 是单个哨兵对节点不可达的独立判断。其判定流程如下:

  1. 哨兵每秒向所有监控的主库、从库、其他哨兵发送 PING 命令。
  2. 若在 down-after-milliseconds(配置参数,默认 30000 毫秒)内未收到有效回复,则将该节点标记为 SDOWN
  3. 有效回复包括:+PONG+LOADING(加载数据中)、+MASTERDOWN(主库下线但不影响从库响应)。

源码体现 :在 sentinel.csentinelCheckSubjectivelyDown() 中,计算当前时间与 last_avail_time 的差值,若超过 down_after_period,则设置 SRI_S_DOWN 标志。

参数调优down-after-milliseconds 是哨兵系统中最重要的延迟参数。值越小,故障发现越快,但网络抖动造成误判的几率也越大。一般建议内网设为 5000~10000 毫秒,跨机房可适当放大但不宜超过 30000。

3.3 客观下线(ODOWN)判定与触发

当某个哨兵将主库标记为 SDOWN 后,需要向其他哨兵确认,以避免单点网络故障导致的误切换。此过程通过 SENTINEL IS-MASTER-DOWN-BY-ADDR 命令实现。该命令兼具两个功能:

  1. 质询是否 ODOWN:返回其他哨兵对该主库的状态(SDOWN 与否)。
  2. 拉票(Leader Election) :返回自己的 runid 作为投票凭证。

ODOWN 的条件是:至少 quorum 个哨兵(包括发起质询的哨兵自己)都认为该主库 SDOWNquorum 是在 sentinel monitor 中配置的,例如:

yaml 复制代码
sentinel monitor mymaster 192.168.1.100 6379 2

表示 quorum=2

关键点

  • quorum 只用于 ODOWN 判定,不用于 Leader 选举的票数计算。Leader 选举要求获得半数以上哨兵的投票(>N/2)。
  • 即使 ODOWN 达成,如果 Leader 选举未能选出执行者,故障转移也无法执行。
  • 最佳实践:哨兵数量设为 3、5 等奇数quorum 设为 N/2+1。例如 3 哨兵 quorum=2,5 哨兵 quorum=3

3.4 Leader Election(基于 Raft 变体的投票)

ODOWN 发生后,并非所有哨兵都能执行故障转移,必须选举出一个 Leader,由它负责后续操作。Redis Sentinel 采用了一种简化版的 Raft 算法:

  1. 每个想要成为 Leader 的哨兵,在检测到 ODOWN 后,向其他所有哨兵发送 SENTINEL IS-MASTER-DOWN-BY-ADDR,其中携带自己的 runid
  2. 接收方哨兵在该配置纪元(epoch)内遵循先到先得 原则:如果自己尚未投票给任何人,就把票投给请求者(返回请求者的 runid);如果已经投过,则返回已投的 runid
  3. 请求者统计自己获得的票数,当 票数 ≥ (总哨兵数/2) + 1 时,成为 Leader。
  4. 如果没有人获得足够票数,则等待一定时间(SENTINEL_ELECTION_TIMEOUT),然后纪元自增,重新投票。

与 Raft 的区别

  • 哨兵没有日志复制,Leader 角色是临时的,仅用于执行一次故障转移。
  • 选举期间如果发现原主库恢复或 ODOWN 条件消除,选举会立即终止。

3.5 故障转移的执行流程

Leader 产生后,执行以下步骤:

  1. 选择新主库 :从原主库的所有从库中,按以下规则筛选:
    • 排除不健康的从库(断线、SDOWN)。
    • replica-priority 升序排序(值越小越优先,0 表示永不提升)。
    • 优先级相同则比较复制偏移量,偏移量越大数据越新。
    • 偏移量相同则比较 runid 字典序,取最小者。
  2. 提升新主库 :向选中的从库发送 SLAVEOF NO ONE,使其变为主库。
  3. 重定向其他从库 :向剩余从库发送 SLAVEOF <new-master-ip> <new-master-port>
  4. 监控旧主库:旧主库恢复后,哨兵会将其降级为从库,使其成为新主库的从库。
  5. 更新哨兵元数据:更新所有哨兵的配置纪元,广播新主库信息。

哨兵 SDOWN/ODOWN 判定与故障转移序列图

sequenceDiagram participant S1 as 哨兵1 (发起) participant S2 as 哨兵2 participant S3 as 哨兵3 participant M as 主库 participant R1 as 从库1 participant R2 as 从库2 Note over S1,M: 周期性 PING S1->>M: PING M-->>S1: (超时无响应) S1->>S1: 标记 SDOWN Note over S1: 质询 ODOWN S1->>S2: IS-MASTER-DOWN-BY-ADDR S2-->>S1: 主库 SDOWN,投 leader_runid=S1_runid S1->>S3: IS-MASTER-DOWN-BY-ADDR S3-->>S1: 主库 SDOWN,投 leader_runid=S1_runid S1->>S1: 达到 quorum=2(含自己=3),标记 ODOWN S1->>S1: 得票≥2,成为 Leader Note over S1,R2: 故障转移执行 S1->>R1: SLAVEOF NO ONE R1-->>R1: 提升为新主库 S1->>R2: SLAVEOF <新主库 IP:PORT> R2->>R1: 开始复制新主库 S1->>S2: 更新元数据 S1->>S3: 更新元数据 Note over S1,S3: 故障转移完成

图2 说明

  • 角色与职责:哨兵集群通过质询投票机制达成一致,主从节点是被管理者。
  • 判定时序 :从 SDOWN 到 ODOWN,再到 Leader 选出,每一步都有明确的条件。网络故障时,可能只有部分哨兵能看到主库 SDOWN,因此 quorum 必须代表多数派的意见。
  • 故障转移关键动作SLAVEOF NO ONE 是原子提升;重定向从库是批量操作,受 parallel-syncs 控制并发。
  • 元数据一致性 :哨兵通过发布/订阅和定期 INFO 保持对新主库信息的同步,客户端可通过 SENTINEL GET-MASTER-ADDR-BY-NAME 实时获取。

3.6 关键配置详解

yaml 复制代码
sentinel monitor mymaster 192.168.1.100 6379 2
sentinel down-after-milliseconds mymaster 10000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000
  • parallel-syncs:默认 1。故障转移完成后,向新主库发起全量同步的从库个数。若从库较多,可设为 2~3 以加速集群恢复,但注意新主库在同步期间的 CPU 和 I/O 压力。
  • failover-timeout:故障转移各阶段的总超时。若超时未完成,则本次故障转移失败,进入下一轮选举。
  • Redis 7 新增sentinel deny-scripts-reconfig yes 加强了安全性,禁止在运行时动态修改通知脚本路径,防止恶意篡改。

第四部分:Cluster 集群深度解析

当数据量突破单机内存,或写入吞吐超过单主库极限时,Redis Cluster 成为必然选择。它通过哈希槽 实现数据分片,利用 Gossip 协议 进行去中心化的状态管理,并内置了自动故障转移。

4.1 哈希槽分片模型

Redis Cluster 将整个键空间划分为 16384 个槽(slot)。每个 Key 通过 CRC16(key) % 16384 计算得出所属的槽。每个主节点负责一部分槽,例如 3 主节点的集群:

  • 主节点 A:槽 0-5460
  • 主节点 B:槽 5461-10922
  • 主节点 C:槽 10923-16383

为何是 16384?

  • CRC16 的结果是 16 位,理论上可以有 65536 个槽。但 Redis 作者选择了 16384,使得在 Gossip 心跳消息中,节点的槽位图占用 2048 字节(16384 bits / 8),这样心跳包不至于过大。65536 个槽需要 8KB 位图,对于定期发送的心跳来说带宽开销过高。
  • 16384 对于绝大多数集群来说足够,槽迁移的粒度也适中。

槽的分配与管理

  • 创建集群时,可以使用 redis-cli --cluster create 自动分配槽。
  • 也可手动通过 CLUSTER ADDSLOTS <slot> [slot ...] 命令指定。
  • 查看当前槽分布:CLUSTER SLOTS 命令返回每个槽范围的负责节点信息。

4.2 客户端路由与 MOVED 重定向

客户端(如 JedisCluster、Lettuce)在连接集群时,会缓存整个槽映射表。对每个请求,客户端计算 slot = CRC16(key) % 16384,然后直接请求对应的主节点。如果集群发生伸缩(增加/移除节点),槽映射会变化,客户端的请求可能被重定向。

MOVED 错误 :当客户端请求的 Key 所属槽已永久 迁移到另一节点时,当前节点返回 MOVED <slot> <target_ip>:<target_port>。客户端收到后,必须更新本地路由表,后续对该槽的请求直接发往新节点。

重定向过程示例

diff 复制代码
GET mykey
-MOVED 3999 192.168.1.11:6379

客户端解析该错误,将槽 3999 映射到 192.168.1.11:6379,然后重新发送请求。

4.3 槽迁移与 ASK 重定向

槽迁移是 Cluster 进行水平伸缩的核心操作。迁移过程中,槽的状态会在源节点和目标节点间发生变更,为保证迁移期间服务不中断,Redis 设计了 ASK 重定向。

迁移步骤

  1. 源节点执行:CLUSTER SETSLOT <slot> MIGRATING <target-node-id>
  2. 目标节点执行:CLUSTER SETSLOT <slot> IMPORTING <source-node-id>
  3. 使用 MIGRATE 命令逐个将槽内的 Key 从源节点迁移到目标节点。
  4. 迁移完成后,向所有节点广播新的槽所有权信息(CLUSTER SETSLOT <slot> NODE <target-node-id>)。

ASK 错误场景 :在迁移过程中,如果客户端向源节点请求一个已经迁移到目标节点的 Key,源节点会返回 ASK <slot> <target_ip>:<target_port>。客户端收到后,需要先向目标节点发送 ASKING 命令,然后再发送原请求。ASKING 命令会在目标节点的客户端会话中设置 CLIENT_ASKING 标志,允许其临时处理一个尚处于 IMPORTING 状态的槽的请求。

MOVEDASK 的核心区别

  • MOVED 是永久重定向,表示槽所有权已变更,客户端必须更新路由表。
  • ASK 是临时重定向,仅在迁移中的单次请求有效,路由表不变。这样设计是为了保证迁移的原子性:如果一个 Key 尚在源节点,客户端仍可直接获取,不会因提前更新路由而请求到尚未有数据的目标节点。

Cluster hash slot 路由与 MOVED/ASK 重定向序列图

sequenceDiagram participant C as 客户端 participant A as 节点A (源) participant B as 节点B (目标) Note over C,B: 场景一:slot 已永久迁移 C->>A: GET key1 (slot 3999) A-->>C: MOVED 3999 192.168.1.11:6379 C->>C: 更新路由表 C->>B: GET key1 B-->>C: 返回值 Note over C,B: 场景二:slot 正在迁移 A->>B: 通过 MIGRATE 迁移部分 key C->>A: GET key2 (slot 3999, key 已迁移) A-->>C: ASK 3999 192.168.1.11:6379 C->>B: ASKING B-->>B: 设置 CLIENT_ASKING 标志 C->>B: GET key2 B-->>C: 返回值 Note over B: 请求处理完后清除 ASKING 标志

图3 说明

  • 场景一:展示已完成迁移后的永久路由变更。客户端需自行实现路由表缓存更新逻辑。
  • 场景二 :展示迁移中的临时重定向。ASKING 命令是 Redis Cluster 客户端实现中的关键一环,缺失会导致迁移期间请求失败。
  • 设计考量:通过区分永久和临时重定向,Redis Cluster 在不使用分布式事务的情况下,实现了槽迁移期间的一致性对外服务。客户端库(如 Jedis、Lettuce)都已内置对此逻辑的支持。

4.4 Gossip 协议与故障检测

Redis Cluster 没有中心元数据服务器,所有节点通过 Gossip(流言)协议 交换信息。每个节点每秒会随机挑选几个节点发送 PING 消息,并期望收到 PONG 回复。消息体包含:

  • 自身节点信息(ID、地址、flags、主从关系)。
  • 随机携带其他若干节点(通常 3 个)的信息。
  • 必要时携带槽分布信息。

故障检测的两阶段判定

  1. PFAIL(疑似下线) :若节点 A 超过 cluster-node-timeout 时间未收到节点 B 的 PONG 回复,A 将 B 标记为 PFAIL。这个判定仅基于 A 与 B 之间的点对点网络状况。
  2. Gossip 传播与 FAIL(客观下线) :A 会在心跳消息中报告"B 处于 PFAIL"。其他节点收到后记录下来。当集群中半数以上的主节点N/2+1)都认为某个主节点处于 PFAIL 时,某个主节点(通常是报告者或最先发现者)会将该节点标记为 FAIL ,并在心跳中广播 FAIL 标志。一旦节点被标记为 FAIL,其从节点就会发起选举,尝试替代它。

cluster-node-timeout 参数:默认 15000 毫秒。此参数直接影响 PFAIL 的判定时间,以及后续 FAIL 的传播和故障转移总耗时。调优建议:

  • 内网环境可设为 10000~15000 毫秒,以加速故障恢复。
  • 跨地域或网络质量不稳定时,应适当放大(如 20000~30000 毫秒),防止因链路抖动导致频繁误判。
  • 结合监控观察 cluster_stats_messages_ping_sentcluster_stats_messages_pong_recv 确保 Gossip 通信正常。

Gossip 协议节点状态传播图

sequenceDiagram participant A as 节点 A participant B as 节点 B (主) participant C as 节点 C (主) participant D as 节点 D (主) Note over A,D: A 检测到 B 无响应 A-xB: PING 超时 A->>A: 标记 B 为 PFAIL A->>C: PING 消息中携带 B:PFAIL C->>C: 记录 B 的 PFAIL 报告 A->>D: PING 消息中携带 B:PFAIL D->>D: 记录 B 的 PFAIL 报告 Note over C,D: 多数主节点确认 C->>C: 统计:≥ N/2+1 主节点报告 B PFAIL C->>C: 标记 B 为 FAIL C->>A: PONG 消息中携带 B:FAIL C->>D: PING 消息中携带 B:FAIL Note over A,D: 全集群知晓 B FAIL,从节点发起选举

图4 说明

  • 角色:节点 A 为首次发现超时的节点,节点 B 为目标故障节点,C 和 D 为其他主节点。
  • PFAIL → FAIL 转换:只统计主节点的意见,从节点的报告不参与 FAIL 判定。这避免了在大量从节点时少数主节点被错误 FAIL。
  • 广播范围:FAIL 消息会通过 Gossip 迅速传播至整个集群,所有节点更新自身视图。此时 B 的从节点会立即检测到并准备接管。

4.5 cluster-require-full-coverage 的权衡

该参数默认 yes。当集群中有任何一个槽不可用(即负责该槽的主节点及其所有从节点都下线),整个集群停止对外服务。这是典型的 CP(一致性优先) 策略:保证数据完整性,避免因部分数据丢失而返回不一致的结果。如果设置为 no,则集群在部分槽不可用时仍可为其他槽提供服务,是 AP(可用性优先) ,但那些缺失槽的请求会返回错误。选择哪种取决于业务对一致性的要求,金融、订单类系统建议保持 yes

4.6 Redis 7 中 Cluster 的增强

Redis 7.0 引入了 cluster-allow-replica-migration 配置(默认 yes),允许从节点在集群拓扑不均衡时自动迁移到其他主节点下,以平衡复制负载。这一特性在早期版本中需要外部工具辅助,现在已内置。


第五部分:脑裂成因与系统性防御

5.1 脑裂的经典场景与根因

脑裂是指分布式系统中因网络分区导致产生两个或多个各自为政的子系统,各自认为自己是唯一合法的系统,从而造成数据冲突。在 Redis 哨兵架构中,典型场景如下:

  1. 主库 M 与哨兵集群、所有从库之间发生网络分区。哨兵集群与从库连通正常,但与 M 不通。
  2. 哨兵将 M 标记为 ODOWN,从从库中选出 S1 提升为新主库。客户端通过哨兵发现新主库并写入数据。
  3. 与此同时,旧主库 M 仍存活,且如果旧客户端直连 M,或者分区内还有业务流量,M 继续接受写入。
  4. 网络恢复后,哨兵发现旧主库 M,将其降级为从库,执行 SLAVEOF S1。M 清空自身数据并全量同步 S1,在分区期间写入 M 的所有数据永久丢失

根因:主库在失去从库和哨兵联系后,没有及时停止写入。

5.2 防御策略详解

策略一:min-replicas-to-write + min-replicas-max-lag

这是最直接有效的防御。配置示例:

arduino 复制代码
min-replicas-to-write 1
min-replicas-max-lag 10

当网络分区导致 M 与所有从库失联时,M 会在 10 秒内发现从库延迟超标,随后拒绝所有写入。这样即使旧客户端直连也无法写数据,分区期间无数据积压,网络恢复后 M 被降级为从库时不会丢失已提交的写入。代价是在从库故障期间写入可用性降低。

策略二:调整哨兵部署与 quorum

确保哨兵节点部署在多个物理机或可用区,使得哨兵集群与主库同时被隔离的概率降至最低。quorum 设置为 N/2+1,保证 ODOWN 判定必须获得多数哨兵同意。

策略三:客户端配合哨兵探活

智能客户端(如 Lettuce)支持从哨兵动态获取主库地址,当主库发生切换时能自动刷新连接。此外,客户端可配置定期与哨兵通信,一旦发现哨兵无法连接或主库地址变更,立即暂停写入,减少脑裂窗口。

5.3 Cluster 模式下的脑裂防护

Cluster 本身的主节点选举由从节点发起,且需要半数以上主节点投票 ,这在一定程度上避免了网络分区时的脑裂(分区的少数派无法选出新主)。但若分区恰好将集群对半分开,且两侧都有主节点,仍可能出现双主。此时可结合 cluster-require-full-coverage yes 和每个主库的 min-replicas-to-write 共同防护。


Redis 高可用架构演进总览图

flowchart TB A["1. 单机 Redis"] -- "解决:数据冗余与读扩展" --> B["2. 主从复制"] B -- "解决:自动故障转移" --> C["3. 哨兵 Sentinel"] C -- "解决:水平扩展与超大容量" --> D["4. Redis Cluster"] A -. "痛点:单点故障,服务不可用" .-> B B -. "痛点:故障人工切换慢,易出错" .-> C C -. "痛点:单机内存/性能瓶颈" .-> D classDef default fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a

图5 说明

  • 阶段演进:从单机到主从,增加了数据冗余和手动切换能力;从主从到哨兵,实现了自动化故障转移;从哨兵到 Cluster,完成了水平分片,突破了单机极限。
  • 核心问题:每一阶段都解决了前一阶段的根本痛点,但也引入了新的复杂性(如哨兵的选举机制、Cluster 的路由重定向)。
  • 脑裂:是所有分布式高可用架构必须直面的风险,防护措施需贯穿多个层面。

第六部分:面试高频专题

6.1 Redis 主从复制的全量同步和部分同步是如何工作的?PSYNCReplication IDoffset 起什么作用?

一句话回答 :全量同步通过 BGSAVE 生成 RDB 快照和复制缓冲区实现初始对齐;部分同步依赖于 Replication ID 验证数据集版本,offset 定位增量断点,从积压缓冲区续传命令。

详细机制解析

全量同步由从库发起 PSYNC ? -1,主库返回 +FULLRESYNC <replid> <offset>。主库 fork 子进程生成 RDB 的同时,将新写入命令暂存在复制缓冲区。RDB 传输完毕后,主库立即发送缓冲区的命令,从库执行后达到数据一致。部分同步发生在从库断线重连时,从库发送 PSYNC <replid> <slave_repl_offset>,主库检查该 replid 与当前 replidreplid2 是否匹配,且 offset+1 是否在 repl_backlog 范围内。若满足,则返回 +CONTINUE 并发送积压命令;否则返回 +FULLRESYNC 退化全量同步。Replication ID 用于标识数据集的血缘,offset 记录复制进度。

多角度追问

  1. 如果主库重启后 replid 改变了,从库还能部分同步吗?
    → 可以。新主库将旧 replid 保存为 replid2。从库携带旧 replid 前来,主库会检查 replid2 匹配性,若 offset 仍在积压缓冲区范围内则允许部分同步。
  2. 积压缓冲区是每个从库独立还是共享?其大小如何影响多个从库?
    → 共享一个环形缓冲区,因此任一从库断线窗口的大小都受限于同一个缓冲区的大小。增大 repl-backlog-size 会同时提升所有从库的部分同步命中率,但占用更多主库内存。
  3. 复制缓冲区和积压缓冲区的区别是什么?
    → 复制缓冲区(replication buffer)每个从库一个,用于实时命令传播;积压缓冲区(repl_backlog)全局共享,保留历史命令用于断线续传。
  4. 如何监控部分同步的命中率?
    → 通过 INFO stats 中的 total_partial_resync_need_full_sync 指标,如果该值持续增长,说明部分同步失败频繁,应检查积压缓冲区大小或网络质量。

加分回答

在 Redis 7 中,INFO Replication 还可以看到 master_replid2second_repl_offset,这两个字段精确描述了故障转移后的过渡期状态,有助于判断是否处于部分同步的安全窗口。结合 MEMORY STATS 可观察积压缓冲区的实际占用。

6.2 repl-backlog-size 如何调优?它如何影响部分同步的命中率?

一句话回答repl-backlog-size 应大于 主库写入带宽 × 期望容忍的最大断线时间;太小会导致部分同步退化为全量同步,增大系统压力。

详细机制解析

部分同步能否成功,取决于从库断线期间的写入数据量是否超过了积压缓冲区的大小。例如主库写入带宽稳定在 8MB/s,希望容忍 60 秒的网络中断,则所需的最小积压缓冲区为 8MB/s × 60s = 480MB。当积压缓冲区被新数据覆盖后,从库请求的偏移量会小于 repl_backlog_off,此时只能全量同步。调优时应基于 instantaneous_ops_per_sec 和平均 value 大小估算实际写入字节速率,并结合业务对恢复时间的容忍度设定。动态调整可通过 CONFIG SET repl-backlog-size <newsize> 完成,Redis 会自动扩展并保留已有数据。

多角度追问

  1. 如果内存不足以设置很大的积压缓冲区怎么办?
    → 需要在"内存开销"和"全量同步成本"之间权衡。可适当降低容忍时间,或在网络较稳定环境下接受较小的值。也可通过增加从库数量来分散全量同步的时机。
  2. repl-backlog-ttl 设为 0 有什么后果?
    → 当最后一个从库断开时立即释放积压缓冲区,意味着任何重连都必须全量同步。这对于频繁重连的场景是灾难性的。
  3. 部分同步失败会带来哪些连锁反应?
    → 全量同步会触发 forkBGSAVE,消耗 CPU 和内存(COW 开销),可能引发主库卡顿;同时网络流量激增,影响其他服务。
  4. 在多级从库架构中,积压缓冲区如何表现?
    → 每个主库(包括中间从库)都有自己的 repl_backlog。中间从库也需要配置合适的积压大小,以保证其下挂的从库能部分同步。

加分回答

Redis 7 支持通过 LATENCY DOCTOR 诊断 fork 延迟,若发现全量同步频繁触发延迟问题,可倒推积压缓冲区不足。结合 PROMETHEUS 等监控工具绘制 total_partial_resync 占比,是容量规划的依据。

6.3 repl-diskless-sync 无盘复制是什么?适用什么场景?

一句话回答:无盘复制跳过磁盘,主库直接通过 Socket 将 RDB 流式传输给从库,适用于磁盘性能差但网络带宽充裕的环境。

详细机制解析

传统全量同步中,主库 fork 子进程将 RDB 写入磁盘,再读取磁盘文件发送给从库。无盘复制模式下,子进程直接将 RDB 数据写入到从库的 Socket。通过 repl-diskless-sync-delay 可设置一个等待时间,以便将多个从库的请求合并到同一轮传输,主库只 fork 一次。这对磁盘 I/O 慢的云主机特别有效,能大幅缩短从库上线时间。但需要注意的是,传输期间如果 Socket 中断,整个同步必须重头开始,且多个从库并发接收时会消耗大量网络带宽。

多角度追问

  1. 无盘复制是否支持部分同步?
    → 支持。无盘复制只改变全量同步阶段的 RDB 传输方式,不影响后续增量命令的积压与部分同步。
  2. 如果设置 repl-diskless-sync-delay 10,第一个从库要等 10 秒才开始同步吗?
    → 是的。主库会等待最多 10 秒,看是否有其他从库也发起全量同步请求,然后一起传输。但这会增加第一个从库的上线延迟。
  3. 在哪些场景下不应使用无盘复制?
    → 网络带宽紧张、跨地域传输或主库出口带宽有限时。因为无盘复制会将 RDB 数据全量推送,可能占满带宽影响正常请求。
  4. Redis 7 对无盘复制有什么改进?
    → 优化了子进程 Socket 缓冲区管理,减少了因缓冲区满造成的阻塞,提升了传输稳定性。

加分回答

可以使用 redis-cli --stat 结合 iostat 在开启前后对比磁盘利用率。对于使用云盘且配置了高网络带宽的实例,无盘复制往往能将全量同步时间缩短 30% 以上。

6.4 哨兵的 SDOWN 和 ODOWN 有什么区别?quorum 如何设置?

一句话回答 :SDOWN 是单个哨兵基于心跳超时的本地判断;ODOWN 是多个哨兵共同确认后的客观下线结论。quorum 即为达成 ODOWN 所需的最少哨兵数。

详细机制解析

SDOWN 由每个哨兵独立判定,当 PING 响应超过 down-after-milliseconds 即标记。为避免单点误判,需要其他哨兵共同确认。哨兵通过 SENTINEL IS-MASTER-DOWN-BY-ADDR 质询,若至少 quorum 个哨兵(包含自己)都认为主库 SDOWN,则标记 ODOWN。quorum 是在 sentinel monitor 中配置的,它与 Leader 选举所需的票数(>N/2)是两个独立的概念。合理设置 quorum 需保证:1) 大于 1,防止单哨兵误判;2) 不超过哨兵总数;3) 通常设为 N/2+1,与选举多数派保持一致。

多角度追问

  1. 如果 quorum 设为 1,哨兵会表现如何?
    → 任何一个哨兵的网络抖动就会立即触发 ODOWN 和故障转移,系统将极不稳定。
  2. ODOWN 后如果 Leader 选举失败,会怎样?
    → 故障转移无法执行,哨兵会持续重试选举直到成功或 ODOWN 条件消失。
  3. 从库的 SDOWN 会触发 ODOWN 吗?
    → 不会。哨兵只会对主库发起 ODOWN 质询,从库下线仅标记 SDOWN,影响故障转移时的新主库选择。
  4. down-after-milliseconds 是否影响 ODOWN 的速度?
    → 间接影响。如果设置很小,SDOWN 更快产生,从而更快发起 ODOWN 质询,但网络抖动时误报也增多。

加分回答

Redis 7 支持 SENTINEL SIMULATE-FAILURE 命令,可以在非生产环境模拟故障切换,验证 quorum 和选举参数的实际效果。通过 SENTINEL MASTERS 可以实时查看当前主库的 flagsquorum 配置。

6.5 哨兵的 Leader Election 是如何进行的?故障转移过程中有哪些关键步骤?

一句话回答:基于 Raft 变体,哨兵在 ODOWN 后拉票,获得半数以上选票者成为 Leader,然后按优先级、偏移量选择新主库并执行提升。

详细机制解析

想要成为 Leader 的哨兵在得知 ODOWN 后,立即向其他哨兵发送 SENTINEL IS-MASTER-DOWN-BY-ADDR(携带自身 runid)。每个哨兵在每个 epoch 内遵循先到先得原则投票,如果自己尚未投票,就投票给请求者并记录。请求者统计票数,若 票数 >= (总哨兵数/2)+1,则当选为 Leader。选举成功后,Leader 执行:1) 选出 replica-priority 最小、offset 最大的从库;2) 发送 SLAVEOF NO ONE 使其成为新主库;3) 向其他从库发送 SLAVEOF 指向新主库;4) 更新所有哨兵的元数据,并持续监控旧主库直到其恢复并降级。

多角度追问

  1. 如果原主库在选举过程中突然恢复,会发生什么?
    → 哨兵会立即中断选举过程,ODOWN 条件消除,故障转移取消。
  2. 从库的 replica-priority 为 0 是什么意思?
    → 该从库永远不会被选举为新主库,常用于异地灾备从库。
  3. parallel-syncs 对故障转移有何影响?
    → 它控制同时向新主库发起全量复制的从库数量。值越大,所有从库恢复越快,但新主库可能过载。
  4. 如果哨兵集群本身发生分区,会出现两个 Leader 吗?
    → 在严格多数派规则下,不会在同一 epoch 出现两个 Leader。但如果分区导致哨兵集群分裂,且两侧都满足 ODOWN 和选举条件(极少见),则可能产生双 Leader。此时配合 min-replicas-to-write 可防止双主写入。

加分回答

Leader 选举中的 epoch 机制类似于 Raft 的 term,每轮选举失败都会自增 epoch。通过 SENTINEL MASTERS 可以观察到 config-epoch 的变化,它反映了当前主库的配置版本。

6.6 Cluster 的 hash slot 是什么?MOVEDASK 重定向有什么区别?

一句话回答hash slotCRC16(key) % 16384 计算出的逻辑分片单元;MOVED 表示槽已永久迁移,ASK 是迁移过程中的临时重定向。

详细机制解析

Cluster 将键空间分为 16384 个槽,每个主节点负责部分槽。客户端本地缓存槽映射,当请求的 Key 对应的槽不属于当前节点时,节点返回 MOVED 错误,客户端需更新路由表。当槽正在迁移时(源节点 MIGRATING,目标节点 IMPORTING),若请求的 Key 已被迁走,源节点返回 ASK 错误。客户端必须向目标节点先发 ASKING 命令(设置 CLIENT_ASKING 标志),然后再发请求,从而确保在迁移期间的服务正确性。

多角度追问

  1. 为什么需要 ASKING 命令,不能直接请求目标节点吗?
    → 如果不先发送 ASKING,目标节点会因为该槽处于 IMPORTING 状态且请求未带有特殊标志而直接返回 MOVED 错误,导致请求失败。ASKING 是临时授权。
  2. 客户端库如何处理 MOVEDASK
    → 高级客户端(如 JedisCluster)将 MOVED 触发槽映射更新并自动重试;ASK 则触发一次临时重定向,之后槽映射不变。
  3. 如果迁移过程中客户端向源节点请求一个尚未迁移的 Key,会发生什么?
    → 源节点直接正常处理,不会返回任何重定向,因为 Key 还在本地。
  4. Cluster 为什么选择 16384 个槽而不是更多?
    → 主要是为了控制心跳消息的大小(2KB 位图),同时 16384 个槽足够满足绝大多数集群的伸缩需求。

加分回答

可以通过 CLUSTER SETSLOT <slot> MIGRATING/IMPORTING 手动模拟迁移,再使用 redis-cli --cluster reshard 进行实际迁移。观察 CLUSTER SLOTSCLUSTER NODES 输出可以看到槽状态的变化。

6.7 Cluster 的 Gossip 协议是如何传播集群状态的?PFAIL 和 FAIL 的区别?

一句话回答:节点间通过周期性 Ping/Pong 消息携带随机节点信息和槽位图;PFAIL 是单节点主观超时判定,FAIL 需多数主节点确认后广播。

详细机制解析

Gossip 协议中,每个节点每秒向几个随机节点发送 PING,消息包含自身信息和随机选取的其他节点信息。这保证了集群状态最终一致。PFAIL 标志由节点在 cluster-node-timeout 超时后设置,仅代表本节点无法连接该节点。随后通过 Gossip 传播,当半数以上主节点都报告某主节点 PFAIL 时,某个主节点将其标记为 FAIL 并广播。FAIL 状态下的主节点会触发其从节点发起选举。

多角度追问

  1. 为什么 FAIL 判定只计主节点的票数?
    → 防止从节点数量膨胀导致的不公平投票,确保故障判定由集群的核心成员(主节点)决定。
  2. 如果某个主节点被标记为 FAIL,但它的从节点也挂了,会怎样?
    → 该主节点负责的槽将不可用,如果 cluster-require-full-coverage 为 yes,则整个集群拒绝服务。
  3. cluster-node-timeout 过大会有什么风险?
    → 故障发现和恢复时间变长,可能长时间存在不可用槽。
  4. Gossip 消息量会随着节点数增加而爆炸吗?
    → 不会。消息大小固定,且每个节点只随机同步少量节点,通信开销与节点数成 O(N) 而非 O(N^2)。

加分回答

可以通过 CLUSTER INFO 查看 cluster_stats_messages_ping_sentcluster_stats_messages_pong_recv 监控 Gossip 通信量。Redis 7 对 Gossip 消息的合并做了优化,减少了不必要的传播延迟。

6.8 cluster-node-timeout 如何设置?它影响哪些行为?

一句话回答:默认 15 秒,影响 PFAIL 判定、FAIL 传播、故障转移启动等所有与超时相关的行为。

详细机制解析

该参数是 Cluster 故障检测的基石。当节点 A 超过 cluster-node-timeout 未收到节点 B 的 PONG,则标记 PFAIL。后续 FAIL 确认和选举的时间都基于此。因此,它决定了故障检测的延迟。如果设置过小,网络偶发延迟将导致频繁 PFAIL,引发不必要的选举和状态广播;设置过大,则故障恢复时间变长。一般内网推荐 1015 秒,跨机房 2030 秒。

多角度追问

  1. 该参数可以动态调整吗?
    → 可以,使用 CONFIG SET cluster-node-timeout <ms>,所有节点应保持一致。
  2. 它和哨兵的 down-after-milliseconds 有何异同?
    → 功能相似,都是超时检测的基础阈值。但 Cluster 的超时还联动 Gossip 传播和选举。
  3. 调整 cluster-node-timeout 后,是否影响已经标记 PFAIL 的节点?
    → 会影响后续判定,但已标记 PFAIL 的节点需要等待新的超时条件才能清除。
  4. 如果集群中有节点时间不同步,会有问题吗?
    → 一般没有,因为超时基于本地时间差,不依赖全局时钟。

加分回答

可结合 redis-cli --cluster check 检查节点延迟,用 CLUSTER NODES 查看 ping-sentpong-recv 时间戳,确保延迟在合理范围内。

6.9 什么是 Redis 脑裂?如何通过配置避免?

一句话回答 :脑裂是网络分区导致出现多个可写主库的现象;通过 min-replicas-to-writemin-replicas-max-lag 让孤立主库拒绝写入,是主要防御手段。

详细机制解析

在哨兵架构中,若主库与哨兵/从库的网络断开,哨兵会选举新主库,而旧主库仍在分区内接受写入,导致数据冲突。当网络恢复后,旧主库被降级为从库,其分区期间写入的数据会被覆盖丢失。防御的核心思想是:让主库在发现从库不足时主动停止写入。配置 min-replicas-to-write 1min-replicas-max-lag 10 后,一旦主库失去所有健康从库,10 秒内将开始拒绝写入,从而避免了孤立写入的数据积压。

多角度追问

  1. 如果网络分区并未导致从库完全断开,只是延迟变高,会发生什么?
    → 如果延迟超过 min-replicas-max-lag,同样会触发拒绝写入,保证数据安全。
  2. Cluster 模式下会脑裂吗?
    → 也有可能。如果网络将集群分成两半且两边都有主节点,但通过多数派选举机制,少数派一侧的从节点无法成功选举为主节点,因此脑裂窗口较小。配合 cluster-require-full-coverage yes 进一步防护。
  3. min-replicas-to-write 设为 2 是否更好?
    → 理论上更安全,但在只有 2 个从库的情况下,如果 1 个从库故障,主库将拒绝所有写入,可用性下降。需根据从库数量和业务SLA设定。
  4. 除了配置参数,客户端层面能做什么?
    → 客户端可定期从哨兵查询主库地址,一旦发现地址变化立即断开旧连接,减少写入旧主库的概率。

加分回答

可以使用 Redis 的 CLIENT KILLCLIENT PAUSE 命令在脑裂恢复期间暂停旧主库的流量,作为一种应急手段。

6.10 min-replicas-to-writemin-replicas-max-lag 的作用是什么?

一句话回答:保证主库至少有 N 个延迟不超过 M 秒的从库时才接受写入,用于防止孤立写入和数据丢失。

详细机制解析

这两个参数由主库在每次写入前检查。若健康从库数小于 min-replicas-to-write,写入命令会被拒绝并返回错误。它们本质上是牺牲一部分写入可用性来换取数据一致性,是 CAP 中 CP 的倾向。在哨兵和 Cluster 模式均可使用,是预防脑裂的最后一道防线。但需注意,它们不能替代哨兵的高可用切换,只能减少切换时的数据丢失量。

多角度追问

  1. 如果临时需要维护,可以动态关闭这个限制吗?
    → 可以,使用 CONFIG SET min-replicas-to-write 0 即可立即关闭检查。
  2. 这两个参数对主库性能有影响吗?
    → 极小,仅仅是每次写入前检查从库列表状态,O(1) 操作。
  3. 当主库拒绝写入后,客户端应该怎么做?
    → 应该重试或等待,并触发告警。如果长时间持续,可能需要介入排查从库或网络问题。
  4. 在 Cluster 中,这个配置是针对整个集群还是每个主节点?
    → 每个主节点独立配置并生效,只限制该主节点的写入。

加分回答

通过 INFO Replication 可以实时查看 connected_slaves 和每个从库的 lag 值,结合监控可以预警是否接近阈值。

6.11 sentinel parallel-syncs 如何影响故障转移后的恢复速度?

一句话回答:控制同时向新主库进行全量同步的从库数量,值越大恢复越快,但新主库压力也越大。

详细机制解析

故障转移完成后,Leader 哨兵会向剩余从库发送 SLAVEOF 命令,使它们指向新主库。如果 parallel-syncs 设为 1,则哨兵会等待一个从库完成全量同步后,再对下一个发送 SLAVEOF,整个过程是串行的。如果设为 N,则哨兵同时命令 N 个从库开始同步。在大规模部署(如 10+ 从库)下,parallel-syncs=1 可能导致最后一个从库在故障转移后数十分钟才完成同步,延长整体恢复时间;但若设置过大,所有从库同时拉取 RDB 会瞬间耗光新主库的 CPU 和网络带宽,导致正常读写受影响。一般建议设为 1~3。

多角度追问

  1. 设为 0 表示什么?
    → 在 Redis 中,0 代表无限制,即所有从库同时同步,应避免使用。
  2. 是否影响哨兵选举过程?
    → 不影响,它只在故障转移最后的重定向阶段生效。
  3. 如果部分从库在同步时失败怎么办?
    → 哨兵会在后续周期检测到该从库状态异常,并重新发送 SLAVEOF 命令。
  4. 这个参数可以动态调整吗?
    → 可以,通过 SENTINEL SET mymaster parallel-syncs 2 实时修改。

加分回答

可在故障转移后通过 INFO Replication 查看新主库的 connected_slaves 和各个从库的 statesend_bulk 表示正在全量同步),评估恢复进度。

6.12 系统设计题:设计一个支持千万级用户的三地 Redis 高可用架构

要求:同城双机房半同步复制、异地灾备异步复制,给出哨兵部署和脑裂防护策略。

设计方案

架构总览

  • 同城双机房 :A 机房和 B 机房物理距离 < 10km,网络延迟 < 1ms。部署一套 Redis 哨兵集群,主库在 A 机房,B 机房部署一个高优先级从库(replica-priority 10)。
  • 异地灾备 :C 机房距离数千公里,网络延迟 > 20ms。部署一个低优先级从库(replica-priority 0),仅作异步复制,不参与自动选举。
  • 哨兵部署:3 个哨兵节点,分别位于 A 机房、B 机房和 C 机房(或第三方监控节点),保证任意单一机房故障时仍有 2 个哨兵存活。

复制与一致性配置

python 复制代码
# A 机房主库 redis.conf
min-replicas-to-write 1
min-replicas-max-lag 5
repl-backlog-size 256mb

# B 机房从库(高优先级)
replicaof A-master 6379
replica-priority 10

# C 机房从库(灾备,永不提升)
replicaof A-master 6379
replica-priority 0

同城半同步通过 min-replicas-to-write 1 和低延迟(5s)保证:A 机房的每条写入,必须在 5 秒内被 B 机房的从库确认(表现为延迟 ≤5s),否则主库拒绝写入。这样即使 A 机房整体宕机,B 机房的从库也拥有所有已确认写入的数据。C 机房异步复制,延迟可能较高,但不影响同城一致性。

哨兵配置

yaml 复制代码
sentinel monitor mymaster A-IP 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 120000

quorum=2,3 哨兵部署保证任一机房故障时仍能 ODOWN 判定和选举。

脑裂防护

  • A-B 机房链路中断:A 机房的从库(B 机房)断开,A 主库在 5 秒后因 min-replicas-to-write 条件不满足而拒绝写入,不再接受新数据,避免孤立写入。
  • 哨兵在 B 机房(及可能 C 机房哨兵)将 A 主库标记为 ODOWN,选举 B 机房从库为新主库,客户端通过哨兵切换至新主库。此时旧主库已停止写入,无数据冲突。
  • 网络恢复后,旧主库被降级为从库,通过全量同步恢复数据。

灾备切换

如果同城双机房均不可用(罕见),可手动将 C 机房从库的 replica-priority 调整为非 0,并干预哨兵或直接使用 SLAVEOF NO ONE 提升为主库,承载业务,实现异地灾备恢复。由于异步复制,可能会有少量数据丢失。

多角度追问

  1. 如何保证同城双机房间的网络延迟满足要求?
    → 通过专线或私有网络连接,监控延迟在 1ms 以内,丢包率 <0.01%。
  2. 如果 C 机房的哨兵也参与了选举,会否导致异地链路抖动引发误切?
    → 由于 quorum=2,单哨兵抖动不会导致 ODOWN。C 机房哨兵主要起投票和监控作用,即使与 A/B 断开,A/B 两个哨兵仍可正常工作。
  3. 异地从库延迟较高,如何监控数据同步差距?
    → 使用 INFO Replication 监控 lag 值,结合 redis-cli --latency-history 观察网络延迟。
  4. 是否可以使用 Cluster 替代?
    → 可以,但跨机房的 Cluster 需要更复杂的网络配置和 Gossip 调优,且不推荐跨高延迟地域部署同一集群。

加分回答

可使用 RedisShake 或 RedisGears 实现异地数据校验和补偿,定时对比 A 机房和 C 机房的数据一致性。同时,客户端接入层应通过哨兵动态获取主库地址,结合断路器模式防止写入旧主库。


核心命令输出与配置速查

INFO Replication 主库输出示例与解读

ini 复制代码
# Replication
role:master
connected_slaves:2
master_replid:837144b6e5c2c3e1a4b1f5d7c6e8a9b0d1f2a3c4
master_repl_offset:5000000
slave0:ip=10.0.0.2,port=6379,state=online,offset=4999800,lag=0
slave1:ip=10.0.0.3,port=6379,state=online,offset=4998000,lag=1
master_replid2:0000000000000000000000000000000000000000
second_repl_offset:-1
  • master_replid / master_repl_offset:主库当前复制 ID 和偏移量。
  • slave0/1:从库状态,lag 为延迟秒数,0 表示完全同步。
  • master_replid2:若不为全 0,表示发生过故障转移或重启,保存了旧主库 ID。

CLUSTER NODES 输出示例与解读

arduino 复制代码
07c37dfeb235213a872192d90877d0cd55635b91 127.0.0.1:30001@40001 myself,master - 0 1623730000000 2 connected 0-5460
67ed2db8d677e59ec4a4cefb06858cf2a1a89fa1 127.0.0.1:30002@40002 master - 0 1623730001000 3 connected 5461-10922
  • node-id:节点唯一 ID。
  • flagsmyself,master 表示当前是主节点。
  • config-epoch:配置纪元。
  • connected:连接状态。
  • 0-5460:负责的槽范围。

SENTINEL MASTERS 输出示例

erlang 复制代码
1)  1) "name"
    2) "mymaster"
    3) "ip"
    4) "192.168.1.100"
    5) "port"
    6) "6379"
    7) "flags"
    8) "master"
    9) "num-slaves"
   10) "2"
   11) "quorum"
   12) "2"
   ...

可查看当前哨兵监控的主库信息、quorum 等。


高可用机制速查表

机制 核心参数 关键诊断命令 故障排查思路
主从复制 repl-backlog-size, repl-diskless-sync, min-replicas-to-write/min-replicas-max-lag INFO replication, MEMORY STATS 检查 master_repl_offsetslave_repl_offset 差值;观察 sync_full 指标是否增长;检查从库 state
哨兵 sentinel monitor (quorum), down-after-milliseconds, parallel-syncs SENTINEL MASTERS, SENTINEL SENTINELS, SENTINEL GET-MASTER-ADDR-BY-NAME 查看哨兵日志,确认 ODOWN 和选举过程;检查各哨兵时钟同步;测试网络连通性
Cluster cluster-node-timeout, cluster-require-full-coverage CLUSTER NODES, CLUSTER SLOTS, CLUSTER INFO 检查是否有槽处于 FAIL 状态;查看 cluster_stats_messages_ping_sent/pong_recv;观察节点间延迟
脑裂防护 min-replicas-to-write, min-replicas-max-lag INFO replication 中的 connected_slaveslag 当出现 NOREPLICAS 错误时,立即检查从库健康状态和主库网络;查看哨兵日志是否发生切换

总结

Redis 的高可用架构从主从复制出发,以 PSYNC 协议和积压缓冲区实现了高效的数据冗余与断线续传;哨兵通过 SDOWN/ODOWN 双层判定和 Raft 变体选举,将故障转移自动化;Cluster 进一步引入哈希槽和 Gossip 协议,实现了去中心化的水平扩展与自愈能力。而贯穿这三种架构的脑裂风险 ,则需要通过 min-replicas-to-write 等配置在一致性和可用性之间做出精确权衡。掌握这些原理和调优手段,是构建大规模、高可靠 Redis 服务的基石。


延伸阅读

  • 《Redis 设计与实现》黄健宏著,第 17、18、19 章
  • 《Redis 深度历险:核心原理与应用实践》钱文品著
  • Redis 官方文档:Replication / Sentinel / Cluster Specification
相关推荐
IronMurphy1 小时前
Redis拷打第三讲
数据库·redis·mybatis
楠枬1 小时前
Redis 哨兵
数据库·redis
橙子圆1232 小时前
Redis知识6之事务
数据库·redis·缓存
zkkkkkkkkkkkkk3 小时前
python使用celery实现异步任务
redis·python·rabbitmq·rocketmq
环流_5 小时前
Redis ZSet
数据库·redis·缓存
bandaoyu7 小时前
【CUDA】store/load普通访存 vs 非临时(Non-Temporal)访存
java·开发语言·redis
爱编程的小新☆8 小时前
redis缓存
redis·分布式·缓存
环流_8 小时前
Redis中set类型以及应用场景
数据库·redis·缓存
明月_清风8 小时前
Redis 数据类型全景解析:从基础到高阶,一文掌握九大核心结构与应用场景
redis·后端