假设有一个“光光平台”,它把文章详情缓存、登录会话和实时阅读量放在 Redis 中:
article:8848 文章详情缓存
session:user:1001 用户登录会话
article:8848:view-count 实时阅读量
最初,所有请求都访问同一个 Redis 实例。这个实例性能很好,但只要进程退出、宿主机故障或网络中断,文章页就可能失去缓存,用户需要重新登录,阅读量更新也会暂停。即使磁盘里存在 RDB 或 AOF,应用仍然要等待原实例恢复,持久化文件本身不会把流量切换到另一台机器。
为了减少这个单点故障,团队增加了 Replica,又部署了 Sentinel。问题随之变成:
- Replica 怎样追上 Primary 不断变化的数据?
- 连接中断后,为什么有时只补一小段数据,有时却要重新复制完整数据集?
- Sentinel 如何区分短暂抖动和 Primary 真正故障?
- 自动切换成功后,已经向客户端返回成功的写入是否一定还在?
本文会沿着“光光平台的 Primary 突然故障”这一场景,依次解释 Primary-Replica 复制、全量与部分同步、异步复制边界、Sentinel 故障判定、选择新 Primary、客户端重连以及实际选型。
术语说明:本文在叙述角色时统一使用 Primary(主节点)和 Replica(副本)。Redis 的命令、配置项、协议字段和事件名为了兼容仍可能包含
master或slave,例如master_repl_offset、SENTINEL MASTER和+switch-master;这些原文与本文的 Primary、Replica 指向相同角色。
1. 高可用首先要解决什么问题
1.1 单节点故障会同时暴露哪些风险
光光平台最初的请求路径非常直接:
内容平台 API
|
v
Redis A
Primary
Redis A 既保存全部内存数据,又承担所有读写。一旦 A 不可达,应用面对的并不只是“数据有没有写入磁盘”,而是三个不同问题:
这里的故障域,是指可能因同一个故障而同时失效的一组资源,例如同一进程、主机、可用区或地域。
| 问题 | 关注点 | 对应机制 |
|---|---|---|
| 实例重启后能否重建数据 | 进程或机器恢复后加载什么 | RDB、AOF |
| 当前节点故障后能否继续服务 | 谁接管 Primary 角色,客户端连接到哪里 | 复制、Sentinel 或 Redis Cluster |
| 整台机器或故障域损坏后能否找回历史数据 | 是否存在独立恢复副本和历史时间点 | 异机、异地备份与恢复演练 |
持久化、复制和备份可以组合,却不能互相替代。RDB、AOF、备份与恢复的完整机制已经在《Redis 持久化原理:RDB、AOF、数据恢复与生产实践》中说明,本文只讨论它们与复制、高可用之间的边界。
例如,Redis A 已经把最新数据写入 AOF,但宿主机网卡损坏,应用仍然无法访问它。反过来,应用成功切换到 Replica B,也不代表故障前最后一条写入一定已经复制到 B,更不代表误执行的 FLUSHDB 能从 B 找回,因为错误命令也会进入复制流。
1.2 读扩展、高可用和数据安全不是同一个目标
Primary-Replica 复制首先建立了多份数据副本,但不同使用方式解决的问题不同:
- 读扩展:把能够容忍短暂旧数据的查询分配给 Replica,降低 Primary 的读取压力。
- 高可用:Primary 故障后,自动选择 Replica 接管写入,并让客户端找到新 Primary。
- 数据安全:控制故障时最多丢失多少数据,并确保存在可验证的恢复来源。
衡量故障结果时,经常使用两个指标:恢复点目标(Recovery Point Objective,RPO)表示最多允许丢失多长时间范围的数据,恢复时间目标(Recovery Time Objective,RTO)表示服务最多允许中断多久。
增加 Replica 可以改善读取能力和冗余,但复制默认是异步的,所以 RPO 不会自动变成零。Sentinel 可以降低人工切换时间,却仍需要经过故障检测、选举、提升和客户端重连,所以 RTO 也不会自动变成零。
还要注意,Primary-Replica 和 Sentinel 都没有对数据进行分片。所有完整数据仍然需要放进一个 Primary,写入也仍然由一个 Primary 承担。需要横向扩展总内存容量或写吞吐量时,应考虑Redis Cluster,而不是继续增加 Replica。

2. Primary-Replica 如何持续复制数据
2.1 写入为什么只进入 Primary
光光平台把拓扑扩展为一个 Primary 和两个 Replica:
+------------------+
写请求 ----------------> | Redis A |
| Primary |
+---------+--------+
|
replication stream
+---------+---------+
| |
v v
+----------------+ +----------------+
读请求可选 ----> | Redis B | | Redis C |
| Replica | | Replica |
+----------------+ +----------------+
Replica 可以在配置文件中使用:
replicaof redis-a.example.internal 6379
也可以在运行时执行:
REPLICAOF redis-a.example.internal 6379
常规写请求仍然发送到 Primary。Primary 执行 SET、INCR、HSET 等命令后,把改变数据集的效果放入复制流并发送给 Replica。键过期、内存淘汰等由 Primary 决定的数据变化,也会以能够让 Replica 得到一致结果的方式传播。
Replica 默认以只读角色对外服务。这样可以避免应用误把两个节点当作可以独立写入的 Primary。即使手工允许 Replica 接收本地写入,这些写入也不会反向合并到 Primary,并且可能在下一次同步时被覆盖,因此不能用它实现多主写入。
2.2 replication ID 和 offset 如何标识数据版本
Redis 不会只用“最后同步时间”判断两个节点是否一致。每个 Primary 都维护复制标识(replication ID)和复制偏移量(replication offset)。
可以把它们理解为:
replication ID = 这一条数据历史属于谁
offset = Replica 已经处理到这条历史的哪个字节位置
Primary 每产生一段复制流,offset 就会向前推进。Replica 处理复制数据后,会周期性向 Primary 回报自己已经处理到的 offset。即使 Primary 不等待每条写入的确认,它也能知道不同 Replica 大致追到了哪里。
例如:
Primary A: replid = r1, offset = 128000
Replica B: replid = r1, offset = 128000
Replica C: replid = r1, offset = 127420
这表示 B 已经处理到 A 当前的复制位置,C 还缺少后面的 580 字节复制流。offset 比较的是复制流位置,不是业务命令条数;一条大命令和一条小命令推进的字节数并不相同。
运行时可以通过下面的命令观察角色、连接状态和复制位置:
redis-cli INFO replication
不同角色和 Redis 版本暴露的字段会有所差异,常见字段包括 role、master_replid、master_repl_offset、slave_repl_offset、master_link_status,以及复制积压缓冲区(replication backlog)的大小和历史范围等状态。监控程序应根据目标版本解析,而不是假设所有字段永远存在。
2.3 Replica 能扩展读取,但会引入旧数据窗口
内容平台可以把文章详情等允许短暂陈旧的查询路由到 B、C:
写文章缓存、更新会话、增加计数 -> Primary A
读取公开文章详情 -> Replica B / C
写后立即读取、权限相关判断 -> Primary A
由于复制默认异步,Primary 返回写入成功时,Replica 可能还没有处理到对应 offset。用户刚修改文章标题,随后从 Replica 读取,可能短暂看到旧标题。因此,读写分离不是简单地把全部 GET 随机发给 Replica,而要先判断业务是否接受旧数据。
增加 Replica 还会增加 Primary 的复制连接、网络发送和缓冲成本。Replica 保存的是完整数据集,所以两个 Replica 提供了两份冗余和更多读取能力,却没有把一份超出单机内存的数据拆开,也没有提升单个 Primary 的写命令执行能力。
只有 Primary-Replica 而没有 Sentinel 时,Primary 故障后仍然需要人工判断哪个 Replica 最新、执行提升、重配其他 Replica,并修改客户端连接地址。复制是自动故障转移的基础,但它本身还不是一套完整的自动高可用方案。
3. 全量同步与部分同步如何选择
3.1 全量同步如何建立一份完整副本
Replica 第一次连接 Primary,或者无法从旧位置继续复制时,会尝试进行全量同步。大致过程如下:
Replica B Primary A
| |
|------ PSYNC -----------> |
| <---- FULLRESYNC ------- |
| | 创建数据集快照
| <==== 传输快照 ========= |
| | 同时缓存快照后的新写入
| 加载完整数据集 |
| <---- 发送增量命令流 ----- |
| 应用增量,进入在线复制 |
Primary 会在后台生成能够表示当前数据集的 RDB 快照。根据复制配置,它可以先生成文件再发送,也可以使用无盘复制把快照数据直接写入网络连接。这个 RDB 用于传输完整数据集,即使实例没有把 RDB 当作长期持久化策略,全量同步仍可能使用快照过程。
生成快照期间,Primary 继续接收新的写入,并保留快照时间点之后的复制流。Replica 收到快照后,需要丢弃或替换自己的旧数据集、加载完整快照,再应用后续增量,最终追到 Primary 的当前 offset。
复制对 Primary 大部分时间是非阻塞的,但创建子进程、写时复制(Copy-on-Write,COW)、快照编码和网络传输都会消耗 CPU、内存与带宽。Replica 加载大数据集时也可能短暂阻塞客户端。因此,多个 Replica 同时触发全量同步可能形成明显的资源峰值。
3.2 部分同步为什么只需要补缺失的数据
如果 B 只是短暂断网,它已经拥有绝大部分数据,重新传输完整数据集会很浪费。Redis 为此在 Primary 侧维护复制 backlog,保存最近一段复制流。
B 重连时通过 PSYNC 提交自己记住的 replication ID 和 offset:
Replica B: 我属于历史 r1,已经处理到 offset 127000
Primary A: 当前仍是历史 r1,backlog 还保留 127001 之后的数据
Primary A: 只发送缺失区间 127001 ... 128000
这就是部分同步。它避免了重新创建、传输和加载完整快照,通常能显著降低短暂网络故障后的恢复成本。
部分同步成立需要满足两个核心条件:
- Replica 请求的复制历史仍然能被 Primary 识别。
- Replica 缺失的 offset 区间仍然保存在 Primary 的 backlog 中。
repl-backlog-size 越大,越有机会覆盖更长的中断窗口,但也会占用更多内存。容量规划应结合高峰期复制流增长速度、允许的最长网络中断和安全余量,而不是只按数据集总大小设置。

3.3 哪些情况会退回全量同步
下面几种情况通常需要全量同步:
| 场景 | 能否部分同步 | 原因 |
|---|---|---|
| 新 Replica 第一次加入 | 不能 | 没有可继续的 replication ID 和 offset |
| 短暂断网,缺失数据仍在 backlog | 通常可以 | 历史一致,缺失区间仍存在 |
| 断网太久,请求的 offset 已经不在 backlog 中 | 不能 | Primary 无法补出完整缺口 |
| Replica 保存的复制历史与当前 Primary 不兼容 | 不能 | 不能证明两边属于同一条数据历史 |
| Primary 进程重启 | 通常不能 | 内存中的 replication backlog 已丢失,Replica 通常需要全量同步 |
| Replica 使用 RDB 正常关闭后重启 | 可能可以 | Replica 可以保存复制 ID 和 offset,并尝试部分同步;AOF 重启不提供这项续传能力 |
内容平台不能把“Replica 已重新连上”当作恢复完成。应继续观察 Replica 是否处于在线状态、复制 offset 是否收敛、是否发生了全量同步,以及同步期间 CPU、内存、磁盘和网络是否出现异常峰值。
为了降低多个 Replica 同时全量同步的风险,可以让 Replica 错开启动,并为复制 backlog、COW 和传输流量保留容量。级联复制可以减少 Primary 直接连接大量 Replica 时的发送压力,但中间 Replica 故障会影响下游节点,拓扑也会更复杂,应按真实规模决定,而不是默认引入。
4. 异步复制带来了哪些边界,又能怎样缩小风险
4.1 Primary 返回成功不代表 Replica 已经收到写入
Redis 默认使用异步复制。Primary 不会在每条命令完成后等待所有 Replica 确认,而是尽快向客户端返回结果,Replica 再异步处理复制流。这样能够保持较低的写入延迟,却留下了一个明确的数据窗口。
光光平台执行:
T1 Client -> Primary A: INCR article:8848:view-count
T2 Primary A: 内存中的阅读量从 100 变为 101
T3 Primary A -> Client: 101
T4 Primary A 在写入到达 Replica B、C 前故障
T5 Replica B 被提升为新 Primary,阅读量仍然是 100
从客户端角度,T3 已经成功;从新 Primary 的数据看,这条写入却不存在。Sentinel 会先排除不合格的 Replica,再从剩余候选中选择新 Primary,具体顺序将在第 6.2 节说明。但无论怎样排序,Sentinel 都无法选择一份尚未到达任何 Replica 的数据。
网络分区还可能造成更长的数据分叉:
网络分区
Client X -> Primary A Replica B + Replica C
旧 Primary + Sentinel 多数派
继续收到写入 将 B 提升为新 Primary
如果 A 在少数侧仍然接受 Client X 的写入,多数侧可以完成故障转移并让其他客户端写入 B。网络恢复后,A 会被重配为 B 的 Replica,A 在分区期间独有的数据将被新 Primary 的数据集覆盖。
因此,Redis Primary-Replica 加 Sentinel 是最终一致的高可用系统,不是依靠共识提交每一条写入的强一致系统。

4.2 WAIT、WAITAOF 和最少 Replica 配置能保护到什么程度
对少量更重要的写入,客户端可以在同一连接执行写命令后调用 WAIT:
SET session:user:1001 "<session-data>"
WAIT 1 1000
WAIT 1 1000 表示:等待这个连接此前的写入至少被 1 个 Replica 确认,最多等待 1000 毫秒。命令返回实际完成确认的 Replica 数量。
如果返回 0,此前的 SET 不会自动回滚;它可能仍然只存在于 Primary。应用必须明确决定是接受降级结果、重试确认、拒绝后续业务,还是把状态写入权威存储。WAIT 能显著降低故障时丢失写入的概率,但它只确认复制进度,不等同于所有节点都已完成持久化,也不会把 Redis 变成强一致系统。
即使 WAIT 得到了足够确认,也不能保证确认该写入的 Replica 在故障时仍然可达,或一定会被 Sentinel 选为新 Primary。Replica 的故障域布局、候选 priority 和故障发生时的复制状态仍会影响实际 RPO;第 6.2 节会继续说明 Sentinel 的候选排序顺序。
Redis 7.2+ 还提供 WAITAOF numlocal numreplicas timeout,用于等待同一连接此前的写入在本地和/或指定数量 Replica 的 AOF 上完成 fsync。调用方必须检查返回的本地与 Replica 确认数是否满足要求,参与确认的实例也需要启用 AOF。它与 WAIT 一样,超时不会回滚此前的写入,也不能保证已经确认的 Replica 在故障时仍然可达或最终被选为新 Primary,更不会把 Redis 变成强一致系统。因此,WAITAOF 加强的是持久化确认,不能替代共识提交或备份。
服务器还可以限制 Primary 在 Replica 状态过差时继续接受写入:
min-replicas-to-write 1
min-replicas-max-lag 10
这表示至少需要 1 个延迟不超过约 10 秒的 Replica,Primary 才接受写请求。网络分区中的旧 Primary 失去合格 Replica 后,会在相应窗口后停止写入,从而限制分叉继续扩大。
这个机制根据 Replica 的周期性确认和估算延迟做尽力而为(best effort)的判断,不能证明某一条具体写入已经安全落在 Replica 上。它还会牺牲可用性:所有 Replica 故障或延迟过大时,即使 Primary 自身健康,也会拒绝写入。
| 机制 | 作用位置 | 主要收益 | 主要代价与边界 |
|---|---|---|---|
WAIT |
客户端按写入调用 | 等待指定数量 Replica 确认某个连接此前的写入 | 增加延迟;超时不回滚;不是强一致或磁盘持久化证明 |
WAITAOF(Redis 7.2+) |
客户端按写入调用 | 等待本地和/或指定数量 Replica 将当前连接此前的写入 fsync 到 AOF | 参与确认的实例需要启用 AOF;增加延迟;超时不回滚;仍非强一致 |
min-replicas-to-write |
Primary 全局写入门槛 | Replica 不足时阻止继续扩大数据分叉 | Replica 故障时牺牲写可用性;仍是尽力而为的判断 |
| RDB/AOF | 单个 Redis 实例 | 支持实例重启后重建数据 | 不负责自动切换,也不能替代备份 |
4.3 持久化和备份为什么仍要独立设计
第 1 节已经区分了复制、持久化和备份的职责。放到异步复制故障中,还要特别注意两个容易被忽略的结果。
Redis 官方强烈建议在重视数据安全的复制拓扑中,为 Primary 和 Replica 启用合适的持久化。如果 Primary 关闭持久化却被进程管理器自动重启,它可能以空数据集重新上线;Replica 随后向这个空 Primary 同步,可能把原有 Replica 上的数据也清空。Sentinel 场景中,Primary 重启得足够快时,甚至可能在 Sentinel 完成故障判定前重新出现。
另一方面,持久化和 Replica 都不是历史备份。DEL article:8848、FLUSHDB 或应用写入的错误值会进入复制流,也可能进入新的 RDB/AOF。重要数据仍需要跨故障域备份和恢复演练;订单、余额、付费权益等不能接受已确认写入丢失的权威状态,通常不应只保存在这套 Redis 拓扑中。
5. Sentinel 如何判断 Primary 真的故障
5.1 Sentinel 在拓扑中承担什么角色
Sentinel 不保存业务数据,也不是转发 Redis 请求的代理。它主要提供四项能力:
- 持续监控 Primary 和 Replica。
- 记录并发布节点异常事件。
- Primary 故障时协调自动故障转移。
- 作为配置提供者,告诉支持 Sentinel 的客户端当前 Primary 地址。
光光平台采用三个独立故障域:
+-------------------+ +-------------------+ +-------------------+
| Host / AZ 1 | | Host / AZ 2 | | Host / AZ 3 |
| Redis A Primary | | Redis B Replica | | Redis C Replica |
| Sentinel S1 | | Sentinel S2 | | Sentinel S3 |
+-------------------+ +-------------------+ +-------------------+
sentinel resolve-hostnames yes
sentinel monitor content-redis redis-a.example.internal 6379 2
content-redis 是这组 Primary-Replica 的服务名,最后的 2 是 quorum,也就是形成 ODOWN 所需的故障判断门槛。示例使用了主机名,因此 Redis 6.2 及以上版本还要显式开启 resolve-hostnames;这个能力默认关闭,早期版本应改用可达 IP。Sentinel 只需要配置要监控的 Primary;它会从 Redis 状态中发现 Replica,并通过 __sentinel__:hello Pub/Sub 频道发现监控同一服务的其他 Sentinel。
稳健部署至少需要三个 Sentinel,并把它们放在故障相互独立的主机或可用区。只有两个 Sentinel 时,如果承载 A 和 S1 的主机一起故障,只剩 S2,系统无法获得两个 Sentinel 中的多数授权,自动故障转移就无法进行。允许单个 Sentinel 独自切换又会放大网络分区中产生两个 Primary 的风险。
Sentinel 配置文件必须可写,因为它会把发现的节点、配置 epoch(拓扑配置版本号)和切换后的新 Primary 地址写回配置。Sentinel 之间默认还需要能够通过端口 26379 通信。Docker、NAT 或端口映射环境需要特别核对节点实际通告的 IP 和端口,否则自动发现可能得到其他节点无法访问的地址。
5.2 SDOWN 和 ODOWN 有什么区别
每个 Sentinel 都会独立检查已知实例。对某个 Sentinel 来说,如果一个实例在 down-after-milliseconds 指定的时间内没有返回可接受的 PING 响应,它会被标记为主观下线(Subjectively Down,SDOWN)。
sentinel down-after-milliseconds content-redis 5000
这里的 5000 表示单个 Sentinel 连续约 5 秒无法获得有效响应后,可以形成自己的 SDOWN 判断。它不表示系统一定在 5 秒后完成切换:调度暂停、网络抖动、实例繁忙和 Sentinel 自身停顿都可能影响观察结果,后面还需要客观下线判断、选举、提升和客户端重连。
SDOWN 只是一个 Sentinel 的本地观点:
S1: 我访问不到 A -> A is SDOWN from S1
S2: 我仍能访问 A -> A is healthy from S2
S3: 我仍能访问 A -> A is healthy from S3
只有足够多 Sentinel 同意 Primary 不可达,Primary 才会进入客观下线(Objectively Down,ODOWN)。ODOWN 使用 sentinel monitor 中配置的 quorum。上面的 quorum 为 2,所以至少两个 Sentinel 需要形成一致的故障观点。
ODOWN 只用于需要触发故障转移的 Primary。Replica 或其他 Sentinel 可以被单个 Sentinel 标记为 SDOWN,但不会因为 ODOWN 自动执行同样的角色切换;处于 SDOWN 或长期断连的 Replica 也不会成为合格的新 Primary 候选。
5.3 quorum 为什么不等于执行切换所需的多数派
quorum 决定“多少 Sentinel 同意,才能把 Primary 标记为 ODOWN”。真正开始故障转移时,还需要选出一个负责本次切换的 Sentinel leader,并获得 Sentinel 多数派授权。
假设有 5 个 Sentinel、quorum 配置为 2:两个故障观点足以形成 ODOWN,但选出的 leader 仍至少需要 3 个 Sentinel 的多数派授权。下面的图把这两道门槛放在同一条决策链上。
如果 quorum 配置得高于多数派,本轮 leader 也需要取得至少 quorum 票;否则至少取得多数派票。因此,leader 的授权门槛是 max(quorum, Sentinel 多数派)。这不表示两道门槛是同一个概念:ODOWN 统计的是故障观点,leader 授权统计的是本轮选票,故障转移必须依次通过两者。只能联系到少数 Sentinel 的网络分区无法获得授权,也就不能实际执行新的故障转移。
quorum 越小,ODOWN 对局部网络故障越敏感;quorum 越大,要求越多观察者同时确认故障。down-after-milliseconds、quorum 和 Sentinel 分布需要结合故障域与网络路径设置,并通过演练测量真实 RTO,不能把示例值直接当作所有环境的最佳配置。

6. Sentinel 如何选出新 Primary 并完成切换
6.1 leader 选举和配置 epoch 解决什么问题
Primary 进入 ODOWN 后,多个 Sentinel 可能几乎同时尝试发起切换。Sentinel 会在一个配置 epoch 中投票,只有获得所需授权的 Sentinel 才能成为本次故障转移 leader。
配置 epoch 是一次拓扑配置的版本号。leader 获得一个新的 epoch,并用它发布“谁是新 Primary”。更高 epoch 的配置会覆盖旧配置,使重新连通的 Sentinel 最终收敛到同一拓扑。
epoch 7: Primary = Redis A
epoch 8: Primary = Redis B
网络恢复后,epoch 8 胜过 epoch 7
如果第一个 leader 在切换过程中失败,其他 Sentinel 不会在同一个时刻无序地同时改节点角色,而是在相关超时规则允许后发起新的尝试。多数派投票和配置 epoch 解决的是“谁有权发布下一版拓扑”,并不把每条业务写入变成共识提交。
6.2 Sentinel 按什么顺序选择 Replica
获得授权的 leader 会先排除不适合提升的 Replica,例如已被判断为不可达或与 Primary 断开太久的节点。具体而言,如果 INFO 显示 Replica 的断连时间超过 down-after-milliseconds * 10 + 当前 Sentinel 观察到 Primary 已处于 SDOWN 的时长,Sentinel 会认为它不可靠并将其淘汰。因此,down-after-milliseconds 也会参与候选资格判断,但它并不等于端到端故障转移时间。
对剩余候选,主要按以下顺序选择:
replica-priority:数值越小越优先;设置为0的 Replica 永远不会被 Sentinel 提升。- 已处理的 replication offset:优先选择复制数据更多的 Replica。
- Redis 进程运行标识(run ID):如果前两项仍相同,使用 run ID 的字典序打破平局,使选择具有确定性。
假设 B 和 C 都可达:
| Replica | replica-priority |
replication offset | 结果 |
|---|---|---|---|
| B | 100 | 128000 | 候选 |
| C | 100 | 127420 | B 的 offset 更靠前,优先 B |
如果 C 的 priority 被设置为 50,它会在候选排序中优先于 priority 为 100 的 B,即使 B 的 offset 略靠前。因此,priority 表达的是运维人员明确指定的故障转移偏好,可能影响最终的数据新旧程度。只有确实需要按硬件、网络或故障域指定优先级时才应调整,并应在所有可能变成 Replica 的节点上保持一致配置。
6.3 从提升 Replica 到所有节点收敛
选定 B 后,一次故障转移大致经历:
S1、S2 判断 A 为 ODOWN
|
v
多数 Sentinel 选出 leader
|
v
leader 选择 Replica B
|
v
向 B 发送 REPLICAOF NO ONE
|
v
观察 INFO,确认 B 已成为 Primary
|
v
leader 发布 failover epoch,进入 RECONF_SLAVES
|
+----> leader 的 GET-MASTER-ADDR-BY-NAME 返回 B
|
+----> 让可达 Replica C 改为复制 B
|
v
C 完成重配或等待 failover-timeout
|
v
leader 本地:+failover-end
(超时会先产生 +failover-end-for-timeout)
|
v
leader 本地:+switch-master
|
v
A 恢复后再改为复制 B
leader Sentinel 成功发送 REPLICAOF NO ONE,并从 INFO 观察到 B 已成为 Primary 后,会为这次切换写入 failover epoch 并进入 RECONF_SLAVES。从这个阶段开始,这个 leader 的 SENTINEL get-master-addr-by-name 会使用已提升的 B 地址,因此查询它的客户端已经可以重新发现新 Primary;leader 同时主动传播包含更高 config epoch 和 B 地址的 HELLO,其他 Sentinel 接收后才陆续更新查询结果。不同 Sentinel 在这段传播窗口内可能返回不同地址。
leader 还会让其他可达 Replica 改为复制 B。对这个 leader 的本地状态机,所有可达 Replica 完成重配时会产生 +failover-end;如果等待超过 failover-timeout,则先产生 +failover-end-for-timeout,随后仍会产生 +failover-end。接着 leader 进入 UPDATE_CONFIG,产生 +switch-master,并把受监控 Primary 的保存地址切换到 B。observer Sentinel 不必等待 leader 走完这条路径:它收到更高 config epoch 的 HELLO 后可以先产生 +config-update-from 和自己的 +switch-master。因此,跨 Sentinel 汇总的事件日志没有统一的全局先后;不可达或没有及时完成重配的 Redis 节点也可以在此后继续被修正。
自 Redis 4.0 起,被提升的 Replica 会把原 Primary 的 replication ID 和切换位置保留为次级复制历史,同时为新的数据历史生成自己的 replication ID。其他原 Replica 连接 B 时,可以在安全的 offset 范围内继续使用旧 ID 请求部分同步,不必因为 Primary 角色变化就一定执行全量同步。这个机制只帮助共同历史上的 Replica 续传,并不会合并旧 Primary 在网络分区中独自接受的写入。
parallel-syncs 控制故障转移后可以同时重新同步新 Primary 的 Replica 数量。较小的值能够避免多个提供读取的 Replica 同时进入加载数据的不可用窗口,代价是整个拓扑收敛更慢。
旧 Primary A 恢复后不会与 B 合并两边的数据。Sentinel 会把 A 重配为 B 的 Replica,使它丢弃不属于当前高 epoch 拓扑的数据历史并重新同步。这正是分区期间写入旧 Primary 可能永久丢失的原因。
7. 客户端如何恢复,以及 Sentinel 什么时候适用
7.1 支持 Sentinel 的客户端如何找到新 Primary
Sentinel 不代理业务请求。客户端不能把 Sentinel 端口当作普通 Redis 数据端口,而要使用支持 Sentinel 的客户端库,并配置:
- 一组已知 Sentinel 的地址,而不是只配置一个 Sentinel。
- 被监控服务名,例如
content-redis。 - Redis 与 Sentinel 各自需要的认证信息。
客户端发现 Primary 的基本流程是:
1. 依次连接已知 Sentinel,跳过超时或不可达节点
2. 所有 Sentinel 均无法连接时,返回“Redis Sentinel 不可达”错误
3. 对可连接的 Sentinel 执行 SENTINEL get-master-addr-by-name content-redis
4. 返回 null 时尝试下一个 Sentinel;所有有效查询都返回 null 时,返回“未识别 master name”错误
5. 连接返回的 Redis 地址
6. 使用 ROLE 验证目标节点本地报告的角色是 Primary
7. ROLE 返回非 Primary 时,短暂等待并从步骤 1 重新发现
8. 发生超时、断线或连接被关闭时,重新从步骤 1 解析地址
Sentinel 更改某个 Redis 实例的角色或复制目标时,会关闭该实例上的普通客户端连接,促使客户端重新发现当前 Primary。连接池不能永久保留旧地址;发现 Primary 已变化时,应关闭旧连接并在新地址建立连接。
ROLE 只能校验目标节点当前报告的本地角色,不能证明它持有最新的 Sentinel 配置。在陈旧 Sentinel 返回旧地址、旧 Primary 尚未被降级的短暂窗口里,旧节点也可能仍然报告自己是 Primary。Sentinel 获得新配置后会把旧 Primary 重配为 Replica 并关闭其普通客户端连接,客户端随后必须重新发现地址。因此,ROLE 能拦截许多陈旧地址,却不能消除网络分区中的旧 Primary 窗口。
应用还需要区分“请求明确失败”和“请求结果未知”。例如客户端发送 INCR article:8848:view-count 后连接超时,这条命令可能从未到达,也可能已经执行但响应丢失。盲目重试可能重复计数。重要写入应使用业务操作 ID、幂等记录或权威数据库约束处理重试,而不是把自动重连误认为端到端恰好一次。
下图把故障转移、客户端恢复和拓扑收敛放在同一条时序中。B 被确认并进入 RECONF_SLAVES 后,leader Sentinel 的查询已经可以返回 B,其他 Sentinel 在收到新 config epoch 后也会陆续返回 B,客户端因而可以重新连接;与此同时,leader 继续让可达的 C 改为复制 B。旧 Primary A 在故障期间仍不可达,只有它恢复并重新被 Sentinel 联系到后,才会被重配为 B 的 Replica。因此,业务恢复可能早于 leader 内部状态机结束,也通常早于 A、C 全部追平。

7.2 Standalone、Primary-Replica 和 Sentinel 怎么选
光光平台可以根据数据价值和服务目标选择不同拓扑:
| 拓扑 | 适合场景 | 主要能力 | 主要边界与成本 |
|---|---|---|---|
| Standalone | 开发测试、可快速重建且允许停机的低重要性缓存 | 部署最简单,资源成本最低 | 单点故障;无读扩展和自动接管 |
| Primary-Replica | 读多写少、接受短暂旧读,并有人工切换操作手册(runbook)的内部系统 | 读扩展、数据冗余 | Primary 故障后仍需人工提升和改客户端地址 |
| Primary-Replica + Sentinel | 单 Primary 容量和写吞吐仍足够,但生产业务要求自动切换 | 自动故障检测、选择新 Primary、重配和服务发现 | 至少三个独立 Sentinel;客户端必须支持;异步复制仍可能丢数据 |
| Redis Cluster | 数据量或写吞吐需要分布到多个 Primary | 分片、横向容量与写扩展、分片故障转移 | 键设计、客户端、迁槽、备份和运维更复杂 |
对光光平台来说:
- 文章详情缓存可以从数据库重建,但首页不能长时间失去缓存,因此生产环境可以使用 Sentinel 降低中断时间。
- 阅读量是派生统计,可以接受一个明确的小 RPO,并定期把聚合结果写回权威存储。
- 登录会话需要结合业务安全策略选择持久化、
WAIT或数据库兜底;故障后让少量用户重新登录,可能比牺牲全部写可用性更合理。 - 付费订阅、结算和权益变更应以具备事务、约束和审计能力的数据库为事实来源,Redis 只保存缓存或可重建投影。
选择 Sentinel 的前提,是完整数据和写入压力仍能由一个 Primary 承担、业务接受异步复制的数据边界、客户端库支持 Sentinel,并且团队能够提供至少三个独立故障域、持续监控与故障演练。若这些条件不成立,增加 Sentinel 进程数量也不会自动得到可靠系统。
8. 常见误区与总结
8.1 七个常见误区及其边界
| 误区 | 正确边界 |
|---|---|
| 有 Replica 就已经自动高可用 | Replica 提供候选数据副本;没有 Sentinel、Cluster 或其他编排时,提升和客户端切换仍需人工处理 |
| Sentinel 能保证不丢任何已确认写入 | Sentinel 自动协调角色切换,但底层仍是异步复制 |
| quorum 等于执行切换所需票数 | quorum 用于 ODOWN;真正切换还需要 Sentinel 多数派授权 leader |
WAIT 让 Redis 变成强一致系统 |
WAIT 等待复制确认并降低风险,但不提供共识提交、自动回滚或绝对持久性 |
| 两个 Sentinel 已经足够稳健 | 丢失一个 Sentinel 后无法形成两个节点中的多数派;官方建议至少三个并分布在独立故障域 |
| 增加 Replica 能扩展总容量和写吞吐 | 每个 Replica 保存完整数据,常规写入仍进入单个 Primary |
| 故障转移完成代表所有 Replica 都已追平 | 新 Primary 被确认后即可发布新配置,其他 Replica 可能仍在重新同步 |
8.2 从复制到故障转移的完整链路
这套架构可以归纳为一条连续机制链:
Primary 执行写入
|
v
异步复制流 + replication ID / offset
|
+---- 短暂断线、backlog 可覆盖 ----> 部分同步
|
+---- 新节点或历史缺口过大 ------> 全量同步
|
v
Sentinel: SDOWN -> quorum 达成 ODOWN
|
v
多数派选举 leader -> 选择 Replica -> REPLICAOF NO ONE
|
v
客户端重新发现 Primary -> 业务恢复
最终需要记住:
- 复制是读扩展和自动故障转移的基础,但默认异步,不能保证 RPO 为零。
- 部分同步依赖 replication ID、offset 和 backlog;缺失历史无法补齐时只能全量同步。
- SDOWN 是单个 Sentinel 的观点,ODOWN 需要 quorum,同意故障与授权 leader 是两道不同门槛。
- Sentinel 选择合格 Replica 时依次考虑 priority、复制 offset 和 run ID,提升后再让拓扑收敛。
- 支持 Sentinel 的客户端必须重新发现并验证新 Primary,应用还要处理结果未知和幂等重试。
WAIT、WAITAOF与最少 Replica 配置只能降低或限制数据损失窗口,代价分别包括复制等待、AOF fsync 等待和更低的写可用性。- Sentinel 适合“不需要分片但需要自动故障转移”的 Redis;强一致权威数据和横向写扩展需要其他架构承担。
高可用不是“永远不出故障”,而是:
系统提前定义哪些故障可以自动接管、切换期间可能丢失什么、客户端如何恢复,以及为了缩小风险愿意牺牲多少延迟和可用性。