数据库基础体系 · 第 37/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。
Redis 复制、Sentinel 与 Cluster:槽位、故障转移和一致性
Redis 的高可用方案常被概括为三组名词:
- 复制(Replication):把一个主节点的数据流发送给一个或多个副本节点。
- Sentinel:监控一组主节点及其副本,在主节点故障时自动选举并完成故障转移。
- Cluster:把整个键空间划分为槽位,由多个主节点共同承载数据,并为每个主节点配置副本。
它们解决的问题并不相同。复制本身主要解决数据副本和读扩展,Sentinel 解决非分片架构中的自动故障转移,Cluster 同时解决数据分片和分片集群内的故障转移。三者都以异步复制为基础,因此“节点可用”不等于“所有已确认写入都不会丢失”。
一、先区分三个维度:复制、故障转移和分片
可以把 Redis 高可用问题拆成三个独立维度:
-
数据是否有副本
- 由主从复制提供。
- 没有副本时,主节点故障通常只能依赖持久化恢复。
-
主节点故障后是否能自动选出新主节点
- 单独使用复制时不会自动完成。
- Sentinel 或 Cluster 的故障转移逻辑负责完成。
-
多个主节点是否共同承载不同键
- 复制架构中通常只有一个主节点承载写入,多个副本承载复制数据。
- 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 改善的是复制确认语义,但它不等于严格一致性,原因包括:
- 副本确认复制进度不等于副本已经持久化到磁盘;
- 随后仍可能发生网络分区和故障转移;
- Redis 的故障转移不是围绕每个客户端操作建立线性一致性证明;
- 读请求如果发往其他副本,仍可能读到旧数据。
如果版本支持 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 的故障转移包含两个不同判断:
-
是否把主节点判定为 ODOWN
- 需要达到配置的 quorum。
-
是否允许某个 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 故障,副本为 R1 和 R2。
第一步:多个 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_host和master_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];10是ARGV[1];- 脚本只访问一个槽位中的键。
不能把跨槽位的两个键直接放入同一个脚本,期待 Cluster 自动提供跨节点原子性。
八、Cluster 的故障检测与故障转移
8.1 Cluster 节点之间的通信
Cluster 节点不仅接受客户端请求,还会使用集群总线端口进行节点间通信。默认情况下,集群总线端口通常是客户端端口加上一个固定偏移量;部署时必须同时开放客户端端口和集群总线端口。
节点之间通过通信传播:
- 节点身份;
- 地址和端口;
- 槽位归属;
- 主从关系;
- 节点状态;
- 故障检测信息;
- 配置纪元。
Cluster 节点通常通过 CLUSTER NODES 查看本节点已知的拓扑:
redis-cli -h 127.0.0.1 -p 6379 CLUSTER NODES
输出中会包含:
- 节点 ID;
- 地址;
master或slave等角色信息;connected或断链状态;- 负责的槽位范围;
- 故障标记。
8.2 节点故障检测
Cluster 节点会通过集群通信互相发送探测。某个节点无法在 cluster-node-timeout 范围内被联系到时,其他节点可能先把它视为疑似故障,随后通过集群消息传播故障状态。
与 Sentinel 类似,单个节点看到连接失败,不必然代表整个集群已经确认该节点故障。故障状态需要在集群成员之间传播,并结合配置纪元和投票过程处理。
8.3 副本如何接管主节点和槽位
假设:
M1:负责槽位 0~5460
R1:复制 M1
如果 M1 故障,R1 不能仅凭自己“还能运行”就立即成为主节点。它需要:
- 判断自己复制的主节点不可达;
- 参与集群故障转移竞争;
- 根据复制进度等信息决定候选优先级;
- 从其他主节点获得投票;
- 获得多数主节点认可;
- 提升为主节点;
- 宣布接管原主节点的槽位。
成功后拓扑变为:
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_statusmaster_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 节点就可用”。
误解四:MOVED 和 ASK 可以用同样方式处理
错误。
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 模式;
- 是否正确处理
MOVED和ASK; - 连接池是否在拓扑变更后更新;
- 是否因为跨槽位批量请求产生
CROSSSLOT; - 节点地址是否是客户端可达地址,而非容器内部地址。
Sentinel 重点检查:
- 客户端是否通过 Sentinel 查询主节点;
- 是否订阅了主节点切换;
- 是否能在旧连接失效后重建连接;
- 是否把读连接和写连接错误地固定到旧主节点。
十六、如何根据需求选择架构
可以按数据和操作边界做选择。
选择普通复制
适合:
- 只需要副本读;
- 能接受人工或外部系统切换;
- 数据量可以由一个主节点承载;
- 重点是复制和读取扩展,而不是自动故障转移。
选择 Sentinel
适合:
- 单个逻辑数据集;
- 需要自动主从切换;
- 客户端能够理解 Sentinel 发现机制;
- 不需要通过多个主节点扩展键空间。
选择 Cluster
适合:
- 单节点内存或写入吞吐不足;
- 业务可以按键划分数据;
- 多键操作可以限制在同一槽位;
- 客户端和运维工具支持 Cluster 协议;
- 能接受按槽位暴露故障边界。
一个简化的决策过程是:
是否需要分片?
否 -> 普通复制或 Sentinel
是 -> Cluster
是否要求自动故障转移?
否 -> 复制加人工切换
是 -> Sentinel 或 Cluster 自带故障转移
是否大量依赖跨键原子操作?
是 -> 先重新设计键和事务边界
否 -> Cluster 更容易落地
十七、最终的正确抽象
Redis 的复制、Sentinel 和 Cluster 可以用下面的关系理解:
复制:
主节点把复制流发送给副本
解决副本同步问题
默认是异步的
Sentinel:
监控一个主从组
判断主节点故障
选择副本并完成主从切换
不负责分片
Cluster:
用 16384 个槽位划分键空间
多个主节点分别负责槽位
每个主节点通过副本实现故障转移
多键操作通常必须同槽
而一致性边界是:
主节点返回成功
≠ 副本已经收到
≠ 副本已经持久化
≠ 故障转移后一定保留
≠ 所有客户端立刻读到
如果能够明确区分:
- 数据复制到哪里;
- 槽位由谁负责;
- 故障由谁判断;
- 谁拥有投票权;
- 客户端如何重定向;
- 写入确认到达了哪一个时刻;
- 故障转移可能丢失哪一段复制历史;
就能把 Redis 高可用从“部署几个节点”还原为可验证的状态机和数据流问题。
系列导航与关联阅读
- 系列入口:数据库完整学习路线:从关系模型、事务索引到分布式与向量检索
- 上一篇:Redis 持久化与内存:RDB、AOF、过期淘汰、Fork 和恢复
- 下一篇:Redis 缓存体系:一致性、穿透、击穿、雪崩和多级缓存
官方资料
本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。

评论
0 条讨论