数据库基础体系 · 第 37/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

Redis 复制、Sentinel 与 Cluster:槽位、故障转移和一致性

Redis 的高可用方案常被概括为三组名词:

  • 复制(Replication):把一个主节点的数据流发送给一个或多个副本节点。
  • Sentinel:监控一组主节点及其副本,在主节点故障时自动选举并完成故障转移。
  • Cluster:把整个键空间划分为槽位,由多个主节点共同承载数据,并为每个主节点配置副本。

它们解决的问题并不相同。复制本身主要解决数据副本和读扩展,Sentinel 解决非分片架构中的自动故障转移,Cluster 同时解决数据分片和分片集群内的故障转移。三者都以异步复制为基础,因此“节点可用”不等于“所有已确认写入都不会丢失”。


一、先区分三个维度:复制、故障转移和分片

可以把 Redis 高可用问题拆成三个独立维度:

  1. 数据是否有副本

    • 由主从复制提供。
    • 没有副本时,主节点故障通常只能依赖持久化恢复。
  2. 主节点故障后是否能自动选出新主节点

    • 单独使用复制时不会自动完成。
    • Sentinel 或 Cluster 的故障转移逻辑负责完成。
  3. 多个主节点是否共同承载不同键

    • 复制架构中通常只有一个主节点承载写入,多个副本承载复制数据。
    • Cluster 中,多个主节点分别负责不同槽位。

因此:

主从复制 ≠ 自动故障转移
Sentinel ≠ 数据分片
Cluster ≠ 强一致事务集群

Sentinel 和 Cluster 都会使用复制,但它们管理拓扑、发现故障和执行切换的方式不同。


二、Redis 复制:主节点如何把写入传播给副本

2.1 复制的基本角色

Redis 复制采用主节点(primary)和副本节点(replica)的关系:

客户端写入
    |
    v
主节点 A
    |
    | 复制流
    +------> 副本 A1
    |
    +------> 副本 A2

主节点处理写命令,并把会改变数据集的命令传播给副本。副本通常配置为只读,以避免客户端直接修改副本数据。

可以使用如下命令查看复制状态:

redis-cli -h 127.0.0.1 -p 6379 INFO replication

主节点上常见的结果类似:

role:master
connected_slaves:2
slave0:ip=10.0.0.11,port=6379,state=online,offset=123456
slave1:ip=10.0.0.12,port=6379,state=online,offset=123420
master_replid:abc...
master_repl_offset:123456

副本节点上则可能看到:

role:slave
master_host:10.0.0.10
master_port:6379
master_link_status:up
master_replid:abc...
master_repl_offset:123450
slave_repl_offset:123450

这里的 offset 可以理解为复制流中的位置。主节点持续推进复制偏移量,副本通过确认自己已经处理到的位置,向主节点反馈复制进度。


2.2 复制不是“把内存实时镜像过去”

Redis 复制传播的是复制协议中的命令流,而不是把主节点的内存页逐页同步。

例如客户端依次执行:

SET user:1 name
INCR counter
EXPIRE user:1 60

主节点会把对应的写操作纳入复制流。副本按照顺序执行这些命令,最终得到相同的数据状态。

但是,复制流并不天然意味着以下保证:

  • 主节点执行成功后,副本已经收到;
  • 副本收到后,已经完成持久化;
  • 客户端从主节点读到的数据,副本立刻可读到;
  • 主节点故障后,所有副本都拥有主节点的最后状态。

这些保证需要分别讨论网络传输、执行确认和持久化。


2.3 初次同步:全量同步

当副本第一次连接主节点,或者无法利用已有复制历史时,通常需要全量同步。

过程可以抽象为:

1. 副本连接主节点
2. 副本发送复制身份和当前偏移量
3. 主节点判断是否可以继续增量同步
4. 如果不能,主节点生成 RDB 快照
5. 主节点把快照传给副本
6. 副本加载快照
7. 主节点把快照生成期间积累的写命令继续发送给副本
8. 副本追上主节点复制流

这里存在一个关键并发问题:生成 RDB 期间,主节点仍然可能接收写请求。于是主节点不能只发送某个时间点的快照,还必须保存快照期间产生的写命令,待副本加载完快照后继续发送。

这段暂存区域就是复制积压缓冲区(replication backlog)发挥作用的地方之一。


2.4 部分重同步:PSYNC、复制偏移量和复制 ID

Redis 支持部分重同步。副本短暂断线后重新连接时,会告诉主节点:

  • 自己认得的复制 ID;
  • 自己处理到的复制偏移量。

如果主节点仍保存着对应复制 ID 的历史数据,并且副本缺失的那一段仍在 backlog 中,主节点就可以只发送缺失部分,而不必重新传输完整 RDB。

形式化地说,设:

  • 主节点复制流当前偏移量为 M
  • 副本最后确认偏移量为 R
  • 复制积压区保存的最早偏移量为 B

若满足:

R < M
且
R >= B
且
副本使用的复制 ID 与主节点历史复制 ID 匹配

则主节点可能发送区间:

(R, M]

这就是部分重同步。

如果 R < B,说明副本缺失的数据已经从 backlog 中淘汰;如果复制 ID 不匹配,也不能直接把两个复制历史拼在一起。此时需要全量同步。

复制 ID 很重要,因为主节点发生故障转移后,新主节点可能继承旧主节点的数据,却拥有新的复制历史。复制 ID 用来区分“这些偏移量属于哪一条复制历史”,不能只看一个数字判断是否可以增量同步。


2.5 网络断开时,副本会发生什么

假设主节点已经执行:

SET order:100 status paid

随后主节点与副本网络断开。

可能出现以下状态:

主节点:status = paid
副本:status = pending

主节点恢复与副本的连接后:

  • 如果断线期间的复制命令仍在 backlog 中,执行部分重同步;
  • 否则执行全量同步。

在断线期间,副本通常仍可响应读请求,但读到的是断线前的数据。生产环境若允许从副本读,应用必须接受这种读取延迟,或者根据业务要求禁止在副本断联时继续读。


2.6 副本是否可以写入

副本默认是只读的。常见配置是:

replica-read-only yes

这不是简单的权限控制,而是为了保持复制拓扑语义。若允许客户端在副本上写入,可能出现:

主节点数据:user:1 = A
副本本地写入:user:1 = B
主节点继续发送复制流:user:1 = C

副本本地写入可能被之后的复制操作覆盖,而且这些写入不会自动反向传播给主节点。Redis 复制不是双向复制,也不是多主冲突合并系统。


三、复制的一致性边界:异步复制意味着什么

3.1 已确认写入不必然已经复制

默认情况下,客户端执行:

SET account:1 balance

Redis 主节点返回 OK 的条件主要是:主节点本地已经执行该命令。它不要求指定数量的副本已经确认。

因此,故障路径可能是:

t1:主节点执行 SET,返回 OK
t2:复制命令尚未到达任何副本
t3:主节点宕机
t4:故障转移选择一个副本成为新主节点

如果被选中的副本没有收到该写入,客户端虽然已经看到成功,但数据可能丢失。

这不是 Redis 命令执行错误,而是异步复制的正常语义。


3.2 WAIT 能提高复制确认程度,但不是强一致

客户端可以使用:

SET order:100 status paid
WAIT 1 1000

含义是:等待至少一个副本确认当前连接此前的写入已经达到副本,最多等待 1000 毫秒。返回值是实际确认的副本数量,可能是:

(integer) 1

也可能是:

(integer) 0

应用必须检查返回值,而不能只因为 SET 成功就认为已经有副本确认。

WAIT 改善的是复制确认语义,但它不等于严格一致性,原因包括:

  1. 副本确认复制进度不等于副本已经持久化到磁盘;
  2. 随后仍可能发生网络分区和故障转移;
  3. Redis 的故障转移不是围绕每个客户端操作建立线性一致性证明;
  4. 读请求如果发往其他副本,仍可能读到旧数据。

如果版本支持 WAITAOF,还可以等待本地或副本对写入完成 AOF 相关的持久化确认。但它同样不能把整个 Redis 部署变成严格线性一致的分布式事务系统,且具体可用参数和行为应以所运行版本的命令文档为准。


3.3 一个更准确的成功模型

对一次写入,可以区分四个时刻:

W0:客户端把命令发送给主节点
W1:主节点执行命令并返回 OK
W2:一个或多个副本收到并处理复制流
W3:主节点或副本把相关数据持久化

默认写入通常只需要到达 W1

使用 WAIT 后,可以要求尽量到达 W2

使用持久化相关等待命令或配置,可以进一步关注 W3

但这四个时刻之间不是不可逆的全局提交协议。故障转移可能选择一个落后副本,网络隔离也可能让客户端暂时看见不同视图。


四、Sentinel:非分片主从架构的自动故障转移

4.1 Sentinel 解决什么问题

Sentinel 适用于“一个主节点、一个或多个副本”的非 Cluster 架构。它主要提供:

  • 监控主节点和副本;
  • 判断主节点是否主观下线或客观下线;
  • 选出 Sentinel 领导者;
  • 从副本中选择新主节点;
  • 让其他副本改为复制新主节点;
  • 向客户端发布当前主节点地址。

典型拓扑如下:

Sentinel S1 ----\
Sentinel S2 -----+---- 监控 ----> 主节点 M
Sentinel S3 ----/                  |
                                  +--> 副本 R1
                                  +--> 副本 R2

Sentinel 本身不保存业务键空间的副本,也不负责把一个键映射到某个槽位。它是控制面组件,不是数据分片层。


4.2 主观下线与客观下线

Sentinel 周期性向被监控实例发送探测请求。如果某个 Sentinel 在超时时间内无法得到有效响应,它可以把该实例标记为:

主观下线(subjectively down,SDOWN)

这只表示“这个 Sentinel 认为目标不可达”,可能是:

  • 目标 Redis 确实宕机;
  • 该 Sentinel 自己网络异常;
  • Sentinel 到 Redis 的链路拥塞;
  • Redis 正在阻塞或负载过高。

因此,单个 Sentinel 的判断不能直接等价于整个系统确认故障。

当足够数量的 Sentinel 都认为同一主节点下线,并达到配置的 quorum 时,主节点才被标记为:

客观下线(objectively down,ODOWN)

quorum 的作用主要是故障认定阈值,不等同于“故障转移已经获得全部授权”。


4.3 quorum 与多数派不是一回事

Sentinel 的故障转移包含两个不同判断:

  1. 是否把主节点判定为 ODOWN

    • 需要达到配置的 quorum。
  2. 是否允许某个 Sentinel 发起故障转移

    • 通常还需要 Sentinel 之间通过领导者选举获得足够票数,即达到多数派条件。

例如有 5 个 Sentinel:

sentinel monitor mymaster 10.0.0.10 6379 2

这里的 2 表示至少两个 Sentinel 同意后可以把主节点标记为 ODOWN,并不表示只要有两个 Sentinel 就一定能安全完成故障转移。故障转移授权仍涉及 Sentinel 多数派。

如果 5 个 Sentinel 被网络分成 2 + 3 两个分区:

  • 2 个 Sentinel 可能无法获得多数派授权;
  • 3 个 Sentinel 组成的分区更可能完成故障转移;
  • 客户端是否访问到正确主节点,还取决于客户端如何发现 Sentinel 以及网络隔离状态。

因此,Sentinel 数量、quorum、网络分区和客户端连接路径必须一起分析。


4.4 Sentinel 故障转移的主要步骤

假设主节点 M 故障,副本为 R1R2

第一步:多个 Sentinel 探测到异常

S1 -> M:超时
S2 -> M:超时
S3 -> M:正常

此时只有 S1、S2 可能认为 M 是 SDOWN,尚不能说明 M 全局不可用。

第二步:达到 quorum,标记 ODOWN

如果 quorum 为 2,S1 和 S2 通过 Sentinel 之间的通信确认后,M 进入 ODOWN。

第三步:Sentinel 选举故障转移领导者

Sentinel 通过投票选择一个 Sentinel 执行本轮故障转移。故障转移过程需要配置纪元(configuration epoch),用来区分不同轮次的拓扑变更,避免旧消息覆盖新状态。

第四步:选择副本

候选副本通常会按照多个因素筛选和排序,包括:

  • 副本是否在线;
  • 副本是否最近与主节点保持连接;
  • 复制偏移量是否更靠前;
  • 副本优先级配置;
  • 副本 ID 等稳定排序因素。

复制偏移量更靠前,通常意味着它拥有更多主节点数据,但这不是“最后一条写入必然存在”的强一致保证。

第五步:提升副本

Sentinel 向被选中的副本发送切换相关命令,使其脱离原主节点复制关系并成为新主节点。

第六步:重新配置其他副本

其他副本改为复制新主节点:

M 故障
 |
 +--> R1 晋升为新主节点
 +--> R2 复制 R1

第七步:客户端发现新主节点

客户端可以通过 Sentinel 查询主节点:

redis-cli -h 10.0.0.21 -p 26379 \
  SENTINEL get-master-addr-by-name mymaster

可能得到:

1) "10.0.0.11"
2) "6379"

Sentinel 客户端库通常还会订阅 Sentinel 的切换事件,并在连接池中更新主节点地址。


4.5 Sentinel 故障转移期间的写入风险

在故障转移期间,客户端可能遇到:

  • 连接主节点失败;
  • 连接仍存活但写入超时;
  • 写入成功后客户端未收到响应;
  • 客户端重试导致命令执行一次或多次;
  • 旧主节点恢复后仍认为自己是主节点。

最后一种情况尤其危险。Sentinel 会尽量通过重新配置、下线或重配置旧主节点来避免双主,但在网络分区期间,旧主节点可能暂时继续接受写入。

例如:

客户端 A ---> 旧主节点 M
客户端 B ---> 新主节点 R1

如果 M 和 R1 之间互不可达,就可能出现两个写入视图。Redis 不会自动对两个主节点上的冲突写入进行通用合并。

所以应用层需要:

  • 正确处理连接切换;
  • 对超时后的写命令设计幂等语义;
  • 不把客户端收到超时简单等价为“命令一定失败”;
  • 对关键业务使用唯一请求 ID、状态机或去重表。

五、Sentinel 与普通复制的配置示例

假设使用三个 Redis 实例:

  • 主节点:6379
  • 副本:6380
  • 副本:6381

副本配置可以类似:

port 6380
replicaof 127.0.0.1 6379
replica-read-only yes

Sentinel 配置可以类似:

port 26379

sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1

启动 Sentinel 的具体命令取决于安装方式。常见形式是:

redis-server sentinel.conf --sentinel

查看 Sentinel 对主节点的判断:

redis-cli -p 26379 SENTINEL master mymaster

查看副本:

redis-cli -p 26379 SENTINEL replicas mymaster

验证时不能只看 Sentinel 是否输出了新地址,还应检查:

redis-cli -p 6380 INFO replication
redis-cli -p 6381 INFO replication

确认:

  • 谁是 role:master
  • 其他节点的 master_hostmaster_port 是否指向新主节点;
  • 新主节点是否拥有预期数据;
  • 客户端是否已经切换连接;
  • 旧主节点恢复后是否被重新配置为副本。

测试故障转移前,应确认这是隔离环境。直接杀死生产主节点进程可能造成未复制写入丢失,也可能触发大量客户端重连。


六、Redis Cluster:16384 个槽位如何分配键

6.1 Cluster 的核心不是“多个 Redis 拼在一起”

Redis Cluster 将逻辑键空间划分为固定数量的槽位:

0 到 16383,共 16384 个槽位

每个主节点负责一部分槽位。副本复制对应的主节点,而不是复制全部键空间。

例如:

主节点 M1:槽位 0       ~ 5460
主节点 M2:槽位 5461    ~ 10922
主节点 M3:槽位 10923   ~ 16383

副本 R1:复制 M1
副本 R2:复制 M2
副本 R3:复制 M3

因此 Cluster 的主节点是分片承载者:

键 user:1   -> 某个槽位 -> M1
键 order:9  -> 某个槽位 -> M3

Cluster 不提供一个“所有主节点都拥有完整数据集”的全量副本。某个主节点故障后,主要由它的副本接管它负责的槽位。


6.2 键如何映射到槽位

对于普通键,Cluster 使用:

slot = CRC16(key) mod 16384

这里的 CRC16 是对键进行计算得到的校验值,取模后落入 0 到 16383 的范围。

但是,如果键中包含哈希标签(hash tag),即一对 {},Redis Cluster 通常只对大括号之间的内容计算槽位:

{user:42}:profile
{user:42}:orders

这两个键使用的哈希部分都是:

user:42

因此会落在同一个槽位。

这使得一部分跨键操作可以在单个节点内执行。例如:

MULTI
HSET {user:42}:profile name Alice
SADD {user:42}:roles admin
EXEC

前提是两个键确实经过相同的哈希标签计算,并且客户端已经连接到负责该槽位的节点。

哈希标签不能解决所有跨键问题。若业务一次操作涉及:

{user:42}:profile
{user:43}:profile

它们通常属于不同槽位,不能作为普通单节点事务一起执行。


6.3 槽位表与客户端路由

Cluster 客户端通常维护一个槽位到节点的映射:

slot 0      -> 10.0.0.1:6379
slot 1      -> 10.0.0.1:6379
...
slot 5461   -> 10.0.0.2:6379

客户端计算键的槽位后,把命令发送给对应节点。

如果客户端路由过期,节点会返回重定向错误。

MOVED

-MOVED 3999 10.0.0.2:6379

含义是:槽位 3999 已经稳定地属于另一个节点。客户端应更新槽位表,并把命令发送到目标节点。

ASK

-ASK 3999 10.0.0.3:6379

含义是:槽位正在迁移,当前命令应临时发送到目标节点。客户端应先向目标节点发送:

ASKING

再发送原命令,但不应因此永久把槽位归属更新为目标节点。槽位迁移完成后,通常会得到稳定的 MOVED 结果。

直接使用普通 redis-cli 访问 Cluster 节点时,如果不加集群模式,客户端不会自动替你跨节点路由。常见测试方式是:

redis-cli -c -h 127.0.0.1 -p 6379

其中 -c 让命令行客户端跟随 MOVED 和相关集群路由。


七、Cluster 中的多键命令、事务和 Lua

7.1 为什么多键命令要求同槽

Redis Cluster 不能把普通单节点事务自动拆成分布式事务。

例如:

MGET user:1 user:2

如果两个键位于不同槽位,节点无法在本地完成该命令,客户端通常会收到类似:

CROSSSLOT Keys in request don't hash to the same slot

原因不是 Redis 不知道如何向两个节点发送请求,而是 MGET 的语义要求一个命令操作一个一致的数据视图。若自动拆分,客户端还必须处理部分成功、返回顺序和故障重试等分布式问题。

以下写法可以让键落到同一个槽位:

MGET {user:1}:name {user:1}:email

但这只在业务上允许共享同一个哈希标签时成立。为了消除错误而把所有键都放入同一个标签,会造成热点槽位,最终失去分片收益。


7.2 事务的边界

在单节点 Redis 中:

MULTI
SET {user:42}:name Alice
SET {user:42}:level 3
EXEC

两个键属于同一槽位,因此事务可以在负责该槽位的节点上执行。

在 Cluster 中,事务、Lua 脚本和函数涉及的键必须满足集群路由要求,通常是同槽位。脚本使用的键还应通过 KEYS 参数显式传入,而不是在脚本中动态构造无法被客户端预先分析的键。

一个可运行的示例:

redis-cli -c -h 127.0.0.1 -p 6379 \
  EVAL "return redis.call('INCRBY', KEYS[1], ARGV[1])" \
  1 "{user:42}:points" 10

这里:

  • 1 表示脚本有一个键;
  • "{user:42}:points"KEYS[1]
  • 10ARGV[1]
  • 脚本只访问一个槽位中的键。

不能把跨槽位的两个键直接放入同一个脚本,期待 Cluster 自动提供跨节点原子性。


八、Cluster 的故障检测与故障转移

8.1 Cluster 节点之间的通信

Cluster 节点不仅接受客户端请求,还会使用集群总线端口进行节点间通信。默认情况下,集群总线端口通常是客户端端口加上一个固定偏移量;部署时必须同时开放客户端端口和集群总线端口。

节点之间通过通信传播:

  • 节点身份;
  • 地址和端口;
  • 槽位归属;
  • 主从关系;
  • 节点状态;
  • 故障检测信息;
  • 配置纪元。

Cluster 节点通常通过 CLUSTER NODES 查看本节点已知的拓扑:

redis-cli -h 127.0.0.1 -p 6379 CLUSTER NODES

输出中会包含:

  • 节点 ID;
  • 地址;
  • masterslave 等角色信息;
  • connected 或断链状态;
  • 负责的槽位范围;
  • 故障标记。

8.2 节点故障检测

Cluster 节点会通过集群通信互相发送探测。某个节点无法在 cluster-node-timeout 范围内被联系到时,其他节点可能先把它视为疑似故障,随后通过集群消息传播故障状态。

与 Sentinel 类似,单个节点看到连接失败,不必然代表整个集群已经确认该节点故障。故障状态需要在集群成员之间传播,并结合配置纪元和投票过程处理。


8.3 副本如何接管主节点和槽位

假设:

M1:负责槽位 0~5460
R1:复制 M1

如果 M1 故障,R1 不能仅凭自己“还能运行”就立即成为主节点。它需要:

  1. 判断自己复制的主节点不可达;
  2. 参与集群故障转移竞争;
  3. 根据复制进度等信息决定候选优先级;
  4. 从其他主节点获得投票;
  5. 获得多数主节点认可;
  6. 提升为主节点;
  7. 宣布接管原主节点的槽位。

成功后拓扑变为:

R1:新的主节点,负责槽位 0~5460

故障转移的投票多数派通常是集群主节点的多数,而不是副本数量的多数。一个只拥有少量主节点但部署了很多副本的拓扑,并不会因为副本数量多就自动获得更高的故障转移投票能力。


8.4 cluster-require-full-coverage

Cluster 还涉及槽位覆盖问题。若部分槽位没有任何可用主节点,集群是否继续接受请求,受 cluster-require-full-coverage 等配置影响。

当要求完整覆盖时,只要存在未覆盖槽位,集群可能停止接受部分或全部写入,以避免应用继续写入一个已经不完整的键空间。

关闭完整覆盖检查可以提高部分故障下的可用性,但代价是:

一部分槽位不可用
而其他槽位仍然可以工作

这不是“集群正常运行”,而是把故障暴露给应用。应用必须能够处理某些键访问失败。


九、槽位迁移:为什么会出现 ASK

扩容或缩容时,需要把槽位从一个主节点迁移到另一个主节点。例如:

迁移前:
M1:槽位 0~8191
M2:槽位 8192~16383

新增 M3,需要把部分槽位从 M1、M2 迁移给 M3

一个槽位的迁移通常不是瞬间完成,而是经历:

1. 把槽位标记为 migrating/importing
2. 迁移该槽位中的键
3. 迁移期间处理客户端访问
4. 确认源和目标状态
5. 更新槽位最终归属

迁移期间,某些键已经在目标节点,槽位整体归属却还未完成切换。此时客户端访问可能得到 ASK。客户端只对当前请求临时跟随目标节点,不应马上把整个槽位永久改表。

迁移完成后,节点会返回 MOVED,表示槽位归属已经稳定改变。

迁移期间的风险包括:

  • 客户端不支持集群重定向;
  • 旧客户端错误处理 ASK
  • 多键命令跨越正在迁移的槽位;
  • 迁移任务与高峰写入争用网络、CPU 和内存;
  • 操作中断后槽位状态不完整。

验证槽位状态时可以使用:

redis-cli --cluster check 127.0.0.1:6379

具体 redis-cli --cluster 子命令和输出应以安装版本为准。


十、故障转移中的一致性:一个完整算例

假设 Cluster 中:

M1:主节点,负责槽位 100
R1:M1 的副本

键:

order:{100}:status

经过哈希计算后位于槽位 100。

时间线如下:

t0:M1 和 R1 数据一致,status = pending

t1:客户端向 M1 执行:
    SET order:{100}:status paid

t2:M1 执行成功并返回 OK

t3:复制流尚未到达 R1

t4:M1 故障

t5:R1 被提升为新主节点

此时新主节点 R1 仍可能是:

status = pending

这说明:

客户端收到 OK
不推出
新主节点一定拥有这次写入

如果在 t2 后执行:

WAIT 1 1000

并返回至少一个副本确认,则 R1 已经确认复制流达到相应位置的可能性更高。但这仍不能推导出绝对不会丢失,因为:

  • WAIT 等待的是副本复制确认,不是完整分布式提交;
  • 确认后副本仍可能故障;
  • 故障转移可能选择另一个状态更旧的副本,具体选择还受故障时拓扑和复制进度影响;
  • 客户端可能在写入结果未知时重试。

如果业务需要“支付状态不能回退”,不能只依赖 Redis 异步复制。应把 Redis 作为缓存或状态加速层,并在可靠存储中保存不可逆的业务事实,或者设计带版本号、幂等键和状态单调迁移的业务协议。


十一、读副本的一致性与读写分离

11.1 读副本可能读到旧值

主节点执行:

SET stock:sku-1 99

客户端立即把读取请求发送到副本:

GET stock:sku-1

可能得到旧值,因为复制流尚未到达或副本尚未处理完成。

这称为复制延迟,而不是缓存失效。延迟来源可能包括:

  • 网络传输;
  • 副本事件循环处理;
  • 副本加载 RDB;
  • 副本执行大量命令;
  • 主节点生成大快照或高负载;
  • 磁盘和操作系统资源争用。

11.2 读写分离的工程含义

适合读副本的场景通常是:

  • 允许短暂旧数据;
  • 统计和监控;
  • 非关键页面;
  • 大量读且不要求读己之写。

不适合直接读副本的场景包括:

  • 写入后立即查询并要求看到刚写入的数据;
  • 库存扣减后的强校验;
  • 权限变更后的即时鉴权;
  • 依赖严格顺序的状态机。

一种简单策略是:在完成关键写入后的短时间内继续读主节点,或者由客户端携带复制进度并等待目标副本追上。后一种方案需要应用和 Redis 客户端共同实现,不能仅凭“连接的是副本”自动得到会话一致性。


十二、Redis Cluster 与 Sentinel 的边界

12.1 Sentinel 的拓扑边界

Sentinel 适合:

一个逻辑主节点
多个副本
自动切换

它不负责:

  • 将 16384 个槽位分给多个主节点;
  • 处理跨分片命令;
  • 让多个主节点共同写入不同键空间;
  • 把多个独立主从组自动组成一个 Cluster。

如果数据量和单主节点容量已经成为瓶颈,Sentinel 不能通过增加副本解决写入和内存容量的分片问题。

12.2 Cluster 的拓扑边界

Cluster 适合:

多个主节点共同承载槽位
每个主节点拥有一个或多个副本

它要求客户端理解:

  • 槽位计算;
  • MOVED
  • ASK
  • 同槽多键约束;
  • 节点切换和连接重建。

如果应用严重依赖跨键事务、跨键 Lua 或大量多键命令,直接迁移到 Cluster 可能导致大量 CROSSSLOT 错误。此时应先重新设计键命名和聚合边界,而不是仅修改连接地址。

12.3 Sentinel 和 Cluster 不是上下级组合

通常不需要用 Sentinel 去管理 Cluster 的主节点。Cluster 自己包含节点发现、故障检测、复制关系和故障转移机制。

可以在外围部署监控系统、服务发现系统或代理,但这与“用 Sentinel 管理 Cluster”是不同概念。错误地叠加两套故障转移控制面,可能导致地址发现和角色判断互相冲突。


十三、持久化与故障转移不是同一层保证

复制解决的是:

内存状态从一个 Redis 实例传播到另一个 Redis 实例

持久化解决的是:

Redis 进程或主机重启后,是否能从磁盘恢复状态

两者可以组合,但不能互相替代。

13.1 只有复制,没有持久化

M1 的数据 -> R1

如果 M1 和 R1 同时损坏,内存中的数据可能全部丢失。

13.2 只有持久化,没有副本

主节点可以从 RDB 或 AOF 恢复,但恢复期间服务不可用,而且最近尚未写入持久化文件的数据可能丢失。

13.3 复制和持久化同时使用

可以降低单点损坏风险,但仍存在:

  • 主节点和副本同时受到机房或存储故障影响;
  • 副本复制延迟;
  • AOF/RDB 配置差异;
  • 故障转移选择了落后副本;
  • 恢复出的旧节点带有过期角色信息。

因此,Redis 的 RDB、AOF、Fork、恢复机制与复制、Sentinel、Cluster 应作为不同层次分析。故障转移选择的是某个内存数据状态,不会凭空恢复它从未收到的写入。


十四、常见误解与对应的失败表现

误解一:有副本就不会丢数据

错误。异步复制允许主节点已确认、副本尚未收到的写入在故障转移时丢失。

诊断方法:

redis-cli INFO replication

关注:

  • master_repl_offset
  • 各副本的复制 offset
  • master_link_status
  • master_last_io_seconds_ago

同时查看:

redis-cli --latency

以及应用侧写入确认和故障时间线。不能只看副本连接状态为 up,因为“连接正常”不等于“复制零延迟”。


误解二:Sentinel quorum 越小,故障转移越快越安全

quorum 越小,单个 Sentinel 更容易把主节点判定为 ODOWN,但这可能增加误判风险。它还不等于故障转移所需的 Sentinel 多数派。

例如网络只隔离了某个 Sentinel,而不是主节点时,小 quorum 可能导致更早的错误判断。


误解三:Cluster 的副本会保存完整数据集

错误。副本只复制对应主节点负责的槽位数据。

如果有三个主节点和三个副本:

M1/R1:数据分片 A
M2/R2:数据分片 B
M3/R3:数据分片 C

丢失整个 A 分片的所有实例,B、C 分片仍可能可用,但 A 分片的数据不可访问。Cluster 的可用性是按槽位划分的,不是简单的“只要还有一个 Redis 节点就可用”。


误解四:MOVEDASK 可以用同样方式处理

错误。

  • MOVED:更新槽位表并重试;
  • ASK:本次请求临时跟随目标节点,先发送 ASKING,不立即永久更新槽位表。

不支持这两个响应的客户端通常会出现:

  • 请求直接失败;
  • 把重定向错误返回给业务;
  • 反复访问错误节点;
  • 槽位迁移期间大量超时。

误解五:使用 Cluster 后就拥有分布式事务

错误。Redis Cluster 主要提供分片和分片内高可用。跨槽位的事务、Lua、多键命令通常不能直接执行。

如果业务操作必须原子地修改多个对象,应优先把这些对象建模到同一槽位,或者把原子性转移到其他存储和业务协议中。强行使用哈希标签把所有业务键放到一个槽位,又会产生热点和单分片容量瓶颈。


误解六:客户端超时后重试一定安全

错误。Redis 命令可能已经在服务端执行,只是响应在网络中丢失。

例如:

客户端发送 INCR order:counter
Redis 执行成功
网络断开
客户端超时
客户端重试 INCR

最终计数可能增加两次。

对于扣款、下单、发券等操作,应使用:

  • 幂等请求 ID;
  • 去重记录;
  • 可重试的状态转换;
  • 明确的查询和补偿流程。

连接重试只能恢复连接,不能自动恢复业务语义。


十五、诊断故障时应沿着数据流检查

15.1 先确认角色和拓扑

普通复制或 Sentinel 架构:

redis-cli INFO replication
redis-cli -p 26379 SENTINEL master mymaster
redis-cli -p 26379 SENTINEL replicas mymaster

Cluster 架构:

redis-cli -c -h 127.0.0.1 -p 6379 CLUSTER INFO
redis-cli -c -h 127.0.0.1 -p 6379 CLUSTER NODES

重点确认:

  • 客户端认为的主节点是否与 Redis 实际角色一致;
  • 副本是否仍复制旧主节点;
  • 是否存在多个节点都认为自己是主节点;
  • Cluster 是否处于 cluster_state:ok
  • 所有槽位是否有归属。

15.2 再确认复制延迟

查看:

redis-cli INFO replication

比较主节点和副本的 offset 差距:

lag = master_repl_offset - slave_repl_offset

这个差值是复制流位置差,不直接等价于“丢失了多少业务记录”,因为命令长度不同、命令执行时间不同,而且主节点状态可能继续变化。但它能帮助判断副本是否明显落后。

15.3 最后确认客户端路由和重试

Cluster 重点检查:

  • 客户端是否开启 Cluster 模式;
  • 是否正确处理 MOVEDASK
  • 连接池是否在拓扑变更后更新;
  • 是否因为跨槽位批量请求产生 CROSSSLOT
  • 节点地址是否是客户端可达地址,而非容器内部地址。

Sentinel 重点检查:

  • 客户端是否通过 Sentinel 查询主节点;
  • 是否订阅了主节点切换;
  • 是否能在旧连接失效后重建连接;
  • 是否把读连接和写连接错误地固定到旧主节点。

十六、如何根据需求选择架构

可以按数据和操作边界做选择。

选择普通复制

适合:

  • 只需要副本读;
  • 能接受人工或外部系统切换;
  • 数据量可以由一个主节点承载;
  • 重点是复制和读取扩展,而不是自动故障转移。

选择 Sentinel

适合:

  • 单个逻辑数据集;
  • 需要自动主从切换;
  • 客户端能够理解 Sentinel 发现机制;
  • 不需要通过多个主节点扩展键空间。

选择 Cluster

适合:

  • 单节点内存或写入吞吐不足;
  • 业务可以按键划分数据;
  • 多键操作可以限制在同一槽位;
  • 客户端和运维工具支持 Cluster 协议;
  • 能接受按槽位暴露故障边界。

一个简化的决策过程是:

是否需要分片?
    否 -> 普通复制或 Sentinel
    是 -> Cluster

是否要求自动故障转移?
    否 -> 复制加人工切换
    是 -> Sentinel 或 Cluster 自带故障转移

是否大量依赖跨键原子操作?
    是 -> 先重新设计键和事务边界
    否 -> Cluster 更容易落地

十七、最终的正确抽象

Redis 的复制、Sentinel 和 Cluster 可以用下面的关系理解:

复制:
    主节点把复制流发送给副本
    解决副本同步问题
    默认是异步的

Sentinel:
    监控一个主从组
    判断主节点故障
    选择副本并完成主从切换
    不负责分片

Cluster:
    用 16384 个槽位划分键空间
    多个主节点分别负责槽位
    每个主节点通过副本实现故障转移
    多键操作通常必须同槽

而一致性边界是:

主节点返回成功
    ≠ 副本已经收到
    ≠ 副本已经持久化
    ≠ 故障转移后一定保留
    ≠ 所有客户端立刻读到

如果能够明确区分:

  • 数据复制到哪里;
  • 槽位由谁负责;
  • 故障由谁判断;
  • 谁拥有投票权;
  • 客户端如何重定向;
  • 写入确认到达了哪一个时刻;
  • 故障转移可能丢失哪一段复制历史;

就能把 Redis 高可用从“部署几个节点”还原为可验证的状态机和数据流问题。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。