单机 Redis 再快,宕机即丢数据,也无法横向扩展读能力。生产环境的 Redis 几乎都是多节点部署:主从复制提供数据冗余与读扩展,哨兵负责自动故障转移,Cluster 把数据分片到多个节点突破单机内存上限。这三套机制层层递进,构成 Redis 高可用的完整图景。
这一篇讲 Redis 怎么在多节点之间保持数据一致、怎么在节点故障时自动恢复。复制的细节远比想象中复杂:一次全量同步可能引发性能抖动,一次网络分区可能丢掉几十秒写入,一个配错的 backlog 可能导致从节点反复回退到全量复制。这些坑都有明确的根因和对策,理解原理才能在生产事故中快速定位。
前置知识
了解 Redis 基本数据类型与持久化机制(RDB/AOF),参见 Redis 数据结构与持久化原理
基本的分布式概念:CAP 定理、Leader 选举、Quorum
TCP 长连接与心跳机制
一、为什么需要复制与高可用
单机 Redis 有三个绕不开的问题。
第一,单点故障。Redis 把数据放在内存里,进程崩溃或机器宕机,内存中的数据就没了。即使开了持久化,恢复也需要时间,这段时间内所有请求都打不到 Redis。对缓存场景,这可能导致后端数据库被打满;对存储场景,这是不可接受的数据不可用。
第二,读扩展瓶颈。单机 Redis 的 QPS 上限在 10 万级别,对于读多写少的场景(比如排行榜、热门商品详情),单机很快成为瓶颈。加机器是自然的需求,但多份数据之间怎么保持一致,就是复制要解决的问题。
第三,写扩展与内存瓶颈。单机内存有上限,一台 64 GB 的机器装不下 200 GB 的数据。即使装得下,单机的写性能也有限。Redis Cluster 通过分片把数据和写流量分散到多个节点。
这三类问题对应三套机制:主从复制解决冗余和读扩展,哨兵解决自动故障转移,Cluster 解决分片和写扩展。
复制是所有高可用方案的基础。无论是哨兵还是 Cluster,底层都依赖主从复制把数据从主节点同步到从节点。理解复制的原理,是理解后续一切的前提。
二、全量复制:RDB 与缓冲区
全量复制是最基础的同步方式。从节点第一次连接主节点,或者断开太久无法增量同步时,主节点把当前全量数据发给从节点。
2.1 PSYNC 命令与握手过程
Redis 2.8 之后用 PSYNC 命令替代了旧的 SYNC,支持增量复制。从节点连接主节点时,发送 PSYNC <runid> <offset>:
runid是从节点上次同步的主节点 ID,首次连接时为?offset是从节点上次同步到的复制偏移量,首次连接时为-1
主节点收到 PSYNC 后,判断能否增量同步。如果不能,回复 +FULLRESYNC <runid> <offset>,触发全量复制。如果能,回复 +CONTINUE,进入增量复制。
2.2 BGSAVE 与 RDB 传输
全量复制的核心是主节点执行一次 BGSAVE,把内存数据快照成 RDB 文件。BGSAVE 通过 fork() 创建子进程,利用 COW(Copy-On-Write)机制在不阻塞主线程的前提下生成快照:子进程共享主进程的内存页,写操作触发的页才被复制,读操作不受影响。
RDB 生成后,主节点把文件内容通过 socket 发给从节点。这里有两种模式:
# redis.conf 关键配置repl-diskless-sync yes # 无盘复制:子进程直接把 RDB 通过 socket 发给从节点repl-diskless-sync-delay 5 # 无盘复制延迟启动时间(秒),用于等待更多从节点一起同步磁盘模式(repl-diskless-sync no)的流程是:子进程把 RDB 写到磁盘临时文件,写完后主线程读文件发给从节点。好处是磁盘上有 RDB 副本,可以复用给多个从节点;坏处是多了磁盘 I/O。
无盘模式(repl-diskless-sync yes)的流程是:子进程直接把 RDB 数据通过 socket 发给从节点,不落盘。好处是省去磁盘 I/O,对网络带宽充足的场景更快;坏处是无法复用,每个从节点都要单独生成一次 RDB。
repl-diskless-sync 的默认值在 Redis 7.0 起从 no 改为 yes,无盘复制成为默认行为。6.2 及更早版本默认是 no(先写磁盘 RDB 再读磁盘发送)。如果沿用旧版配置或明确需要磁盘 RDB 副本,才显式设为 no。
repl-diskless-sync-delay 让主节点延迟几秒再开始无盘同步,目的是等更多从节点连上来,用一次 fork 同时给多个从节点发数据。这个延迟是性能与同步及时性的权衡。
2.3 发送期间的增量命令怎么处理
BGSAVE 生成 RDB 需要时间,这期间主节点还在接受客户端写命令。这些增量命令如果不存下来,从节点加载完 RDB 后就落后了。
Redis 的做法是为每个正在全量同步的从节点维护一个 client output buffer(客户端输出缓冲区)。主节点在执行写命令后,除了写自己的数据,还会把命令追加到所有从节点的输出缓冲区。RDB 发送完毕后,主节点继续把缓冲区里的命令发给从节点。
# 从节点输出缓冲区限制# 格式:硬限制 软限制 软限制持续秒数client-output-buffer-limit replica 256mb 64mb 60这条配置的含义是:从节点输出缓冲区超过 256 MB 立即断开连接,超过 64 MB 且持续 60 秒也断开。断开后从节点会重新连接,重新触发全量同步。如果缓冲区一直超限,从节点就会陷入全量同步的死循环。
全量同步期间主节点写入过快,或者从节点网络太慢,都会导致输出缓冲区超限。一旦超限断开,从节点重连又触发全量同步,形成同步风暴。生产环境中如果发现从节点反复全量同步,先查这个缓冲区。
2.4 从节点加载 RDB
从节点收到 RDB 后,先清空本地数据,再加载 RDB。加载期间从节点无法提供服务,这个阻塞时间和数据量成正比。加载完 RDB 后,从节点继续接收并执行主节点发来的增量命令,追平到全量同步开始那一刻的状态。
从节点追平后,会定期向主节点发送 REPLCONF ACK <offset>,报告自己的复制偏移量。主节点根据这个 ACK 判断从节点的同步进度。
# 查看主从复制状态# 在从节点执行redis-cli INFO replication# Replicationrole:slavemaster_host:127.0.0.1master_port:6379master_link_status:upmaster_last_io_seconds_ago:0master_sync_in_progress:0slave_repl_offset:1048576slave_read_repl_offset:1048576master_link_status:up 表示与主节点的连接正常,slave_repl_offset 是从节点已同步的偏移量,master_sync_in_progress:0 表示当前没有全量同步在进行。
2.5 全量复制的代价
全量复制开销很大:
| 环节 | 开销 | 影响因素 |
|---|---|---|
| BGSAVE | fork 子进程,页表拷贝阻塞主线程 | 实例内存大小 |
| RDB 传输 | 占用主节点网络带宽与 CPU | 数据量、网络带宽 |
| RDB 加载 | 从节点阻塞,无法服务 | 数据量 |
| 输出缓冲区 | 占用主节点内存 | 全量同步期间的写入量 |
一个 10 GB 的实例,全量同步可能耗时几十秒到几分钟。这期间主节点的 fork 会造成短暂阻塞,网络带宽被 RDB 传输占满,从节点在加载 RDB 期间完全不可用。如果多个从节点同时全量同步,主节点的负担会成倍增加。
全量复制是最后的兜底方案,正常生产环境应该尽量走增量复制。判断全量复制的触发条件是否合理,是排查复制问题的关键。
三、增量复制:replid 与 offset
全量复制代价太高,Redis 需要一种机制在短暂断连后只同步缺失的部分。增量复制就是为此设计,核心是 master_replid、offset 和 repl_backlog 这三个东西。
3.1 复制偏移量 offset
主节点和从节点各自维护一个复制偏移量(replication offset):
- 主节点每向从节点发送 N 字节的复制流,就把自己的
master_repl_offset加 N - 从节点每收到 N 字节,把自己的
slave_repl_offset加 N
正常情况下两者相等。如果不等,差值就是从节点的落后量。
# 主节点查看redis-cli INFO replication | grep repl_offset# master_repl_offset:1048576# repl_backlog_active:1# repl_backlog_size:1048576# repl_backlog_first_byte_offset:1# repl_backlog_histlen:1048576master_repl_offset 是主节点发送的总字节数,repl_backlog_size 是 backlog 环形缓冲区的大小,repl_backlog_first_byte_offset 是缓冲区中最早还保留的字节偏移量,repl_backlog_histlen 是缓冲区中当前有效数据的长度。
3.2 master_replid 与实例身份
每个 Redis 实例启动时生成一个 40 字符的十六进制 runid(实例 ID)。主从握手时,从节点记录主节点的 runid。断连重连后,从节点把上次记录的 runid 发给主节点,主节点据此判断从节点之前连的是不是自己。
如果主节点重启了,runid 会变化,从节点拿着旧 runid 来连,主节点发现不匹配,直接拒绝增量复制,触发全量同步。这是 runid 机制的核心作用:识别主节点是否发生过重启。
Redis 4.0 引入了 replid 和 replid2 两个字段,改进了这个机制:
replid是当前实例的复制 IDreplid2是故障转移后从节点提升为主节点时,继承的原主节点replid
故障转移时,新主节点的 replid 变成自己的,但 replid2 保留原主节点的 replid。其他从节点拿着原主节点的 replid 来连新主节点,新主节点通过 replid2 匹配,可以继续增量复制,避免一次全量同步。
# 查看主节点的 replidredis-cli INFO replication | grep replid# master_replid:83714433e6a28e78e8a2e6b6e3a9b6e3a9b6e3a9# master_replid2:0000000000000000000000000000000000000000# second_repl_offset:-1master_replid2 全零表示当前实例没有经历过故障转移,second_repl_offset 为 -1 表示没有继承的偏移量。
3.3 repl_backlog 环形缓冲区
repl_backlog 是主节点维护的一个环形缓冲区,保存最近的复制流。它是增量复制能成立的关键。
环形缓冲区的工作方式:
- 主节点发送复制流时,同时写入 backlog
- 写到末尾后绕回头部继续写,覆盖最老的数据
- 从节点断连重连时,如果请求的 offset 还在 backlog 范围内,主节点就把 backlog 中从该 offset 到当前 offset 的数据发给从节点
判断从节点能否增量同步,需要同时满足两个条件:
- 从节点请求的 offset 大于等于 backlog 的
first_byte_offset(请求的字节还在缓冲区内) - 从节点请求的 offset 小于等于主节点当前的
master_repl_offset(没有超前于主节点)
两条都满足,才能增量同步。只要有一条不满足,就回退到全量复制。
# backlog 大小配置repl-backlog-size 1mb # 默认 1 MB,生产环境通常要调大repl-backlog-ttl 3600 # backlog 空闲多久后释放(秒),0 表示永不释放3.4 增量复制的判断逻辑
主节点收到 PSYNC <runid> <offset> 后的判断流程:
这个判断在源码 masterTryPartialResynchronization 函数中。简化后的逻辑:
// Redis 源码 replication.c(简化)char *masterTryPartialResynchronization(client *c) { long long psync_offset; char *master_replid = c->argv[1]->ptr;
// 1. 检查 replid 是否匹配(当前 replid 或 replid2) if (strcasecmp(master_replid, server.replid) != 0 && strcasecmp(master_replid, server.replid2) != 0) { // replid 不匹配,需要全量 goto need_full_resync; }
// 2. 如果匹配的是 replid2,调整 offset if (strcasecmp(master_replid, server.replid2) == 0) { psync_offset = server.second_repl_offset; } else { psync_offset = c->argv[2]->ptr 解析为 long long; }
// 3. 检查 offset 是否在 backlog 范围内 if (!server.repl_backlog || psync_offset < server.repl_backlog_off || psync_offset > (server.repl_backlog_off + server.repl_backlog_histlen)) { // offset 已被覆盖,需要全量 goto need_full_resync; }
// 4. 增量复制:发送 backlog 中差异数据 addReply(c, shared.cont); // ... 计算 psync_len 并发送 ... return;}3.5 backlog 大小怎么定
backlog 太小,从节点断连稍久就会被覆盖,回退全量。backlog 太大,浪费内存。合理的大小取决于两个因素:
- 从节点可能断连的最大时长
- 主节点的写入速率
估算公式:backlog_size = max_disconnect_seconds × write_bytes_per_second
例如主节点每秒写入 1 MB,从节点可能断连 30 秒,那么 backlog 至少需要 30 MB。
# 查看主节点每秒写入量(近似)redis-cli INFO stats | grep -E "total_net_input_bytes|instantaneous_ops_per_sec"# total_net_input_bytes:客户端输入总字节数# instantaneous_ops_per_sec:当前每秒命令数
# 估算写入速率(采样两次)redis-cli INFO stats | grep total_net_input_bytessleep 10redis-cli INFO stats | grep total_net_input_bytes# 差值除以 10 就是每秒写入字节数生产环境 backlog 默认 1 MB 几乎肯定不够。建议根据写入速率调到几十 MB 甚至上百 MB。内存开销相比全量复制的代价微不足道。
四、哨兵 Sentinel:监控与故障转移
主从复制解决了数据冗余,但主节点宕机后,需要人工把从节点提升为主节点,再改所有客户端的连接地址。这个过程手动完成,耗时且容易出错。哨兵(Sentinel)就是把这个过程自动化。
4.1 哨兵的职责
哨兵是一个独立的进程,与 Redis 主从节点分开部署。它的职责有四个:
| 职责 | 说明 |
|---|---|
| 监控 | 定期向主节点、从节点、其他哨兵发送 PING,检测是否存活 |
| 通知 | 节点异常时通知运维或客户端(可通过脚本回调) |
| 自动故障转移 | 主节点宕机时,自动选一个从节点提升为主节点 |
| 配置中心 | 客户端连哨兵查询当前主节点地址,主节点切换后哨兵通知客户端 |
哨兵本身也要高可用,所以至少部署 3 个,形成哨兵集群。哨兵之间通过 pub/sub 互相发现,定期交换各自对被监控节点的判断。
4.2 主观下线与客观下线
哨兵判断主节点故障分两步,避免误判:
主观下线(SDOWN,Subjectively Down):单个哨兵发现主节点在 down-after-milliseconds 时间内没有响应 PING,就标记为主观下线。这只是这个哨兵自己的判断,可能是网络抖动。
客观下线(ODOWN,Objectively Down):一个哨兵把主观下线判断通过 pub/sub 发给其他哨兵。如果超过 quorum 个哨兵都同意主节点主观下线,就标记为客观下线,确认故障,进入故障转移流程。
# sentinel.conf 关键配置sentinel monitor mymaster 127.0.0.1 6379 2# 含义:监控名为 mymaster 的主节点,地址 127.0.0.1:6379,quorum 为 2
sentinel down-after-milliseconds mymaster 30000# 30 秒无响应判定为主观下线
sentinel parallel-syncs mymaster 1# 故障转移后,多少个从节点同时和新主节点做全量同步
sentinel failover-timeout mymaster 180000# 故障转移超时时间(毫秒),超时则重试quorum 的值取决于哨兵总数。3 个哨兵通常 quorum 设为 2,5 个哨兵 quorum 设为 3。这样即使一个哨兵故障,剩下的仍能达成多数。quorum 不是“选举 Leader 的票数”,而是“多少个哨兵同意才认定客观下线”,这两个概念容易混淆。
4.3 Leader 选举
确认客观下线后,哨兵之间要选出一个 Leader 来执行故障转移。Leader 选举基于 Raft 协议的简化版本:
Raft 选举的要点:
- 每个哨兵都有一个纪元(epoch),每次选举递增
- 想当 Leader 的哨兵先投自己一票,然后向其他哨兵发
SENTINEL IS-MASTER-DOWN-BY-ADDR请求票 - 一个纪元内每个哨兵只能投一票,先到先得
- 获得多数票(超过半数)的哨兵成为 Leader
- 如果本轮无人获得多数,等待随机时间后进入下一轮
因为先到先得和随机超时,通常几轮内就能选出 Leader。3 个哨兵的集群,只要 2 个活着就能完成选举。
4.4 选择新主节点
Leader 哨兵负责从从节点中选一个提升为新主节点。选择规则按优先级排序:
- 过滤:排除断线的、最近 5 秒没回复过哨兵 PING 的从节点
- 优先级:选
slave-priority值最小的(值越小优先级越高,0 表示永不提升) - 复制偏移量:优先级相同,选
slave_repl_offset最大的(数据最接近主节点) - runid:偏移量也相同,选 runid 字典序最小的(纯粹为了确定性)
# 从节点配置优先级slave-priority 100 # 默认 100,值越小越优先被选为新主选出新主节点后,Leader 执行:
# 1. 对新主节点发送,让它成为主节点SLAVEOF NO ONE
# 2. 对其他从节点发送,让它们复制新主节点SLAVEOF <new_master_ip> <new_master_port>
# 3. 更新哨兵配置,后续客户端查询会返回新主节点地址parallel-syncs 控制有多少个从节点同时和新主节点做全量同步。值越大故障恢复越快,但新主节点的负载也越大。通常设为 1,让从节点逐个同步。
4.5 旧主节点回来怎么办
故障转移完成后,如果旧主节点恢复上线,它还以为自己是主节点。哨兵会把它降级为从节点,让它复制新主节点的数据。
这个过程中有一个数据丢失风险:旧主节点在宕机前接受了写入,但还没来得及同步给从节点。这些写入在旧主节点降级为从节点、加载新主节点的数据后会被覆盖丢失。这就是脑裂的雏形,后面专门讲。
# 哨兵查看集群状态redis-cli -p 26379 SENTINEL mastersredis-cli -p 26379 SENTINEL master mymasterredis-cli -p 26379 SENTINEL replicas mymasterredis-cli -p 26379 SENTINEL sentinels mymaster五、Cluster:分片与 Gossip
哨兵解决了高可用,但数据还在一个主节点上,单机内存和写性能是天花板。Redis Cluster 把数据分片到多个节点,突破单机限制。
5.1 16384 个哈希槽
Redis Cluster 把所有 key 映射到 16384 个槽(slot),每个节点负责一部分槽。key 到槽的映射用 CRC16 算法:
# 槽计算公式SLOT = CRC16(key) mod 16384
# Redis CLI 计算示例redis-cli CLUSTER KEYSLOT mykey# (integer) 14687
redis-cli CLUSTER KEYSLOT "{user}:1"# (integer) 5474
redis-cli CLUSTER KEYSLOT "{user}:2"# (integer) 5474{user}:1 和 {user}:2 映射到同一个槽,这是哈希标签(hash tag)机制。key 中 { 和 } 之间的内容会被用作 CRC16 的输入,这样不同 key 只要哈希标签相同就落在同一个槽,同一个节点上。多键操作(MSET、MGET)要求涉及的 key 都在同一个槽,哈希标签就是为此设计。
每个主节点有自己的从节点做数据冗余。主节点宕机,从节点提升为新主节点,机制和哨兵类似,但由 Cluster 内部自动完成,不需要额外的哨兵进程。
5.2 槽分配与迁移
Cluster 创建时把 16384 个槽分配到各主节点。查看分配:
redis-cli -p 7000 CLUSTER NODES# 节点 ID 抶态 标志 IP:Port 主节点 ID ping pong epoch link 状态 槽范围a1b2c3... myself master 127.0.0.1:7000 - 0 0 1 connected 0-5460d4e5f6... slave 127.0.0.1:7004 a1b2c3... 0 0 4 connectedb7c8d9... master 127.0.0.1:7001 - 0 0 2 connected 5461-10922e0f1a2... slave 127.0.0.1:7005 b7c8d9... 0 0 5 connectedc3d4e5... master 127.0.0.1:7002 - 0 0 3 connected 10923-16383f6a7b8... slave 127.0.0.1:7006 c3d4e5... 0 0 6 connectedCLUSTER NODES 的输出每行一个节点,关键字段:
| 字段 | 含义 |
|---|---|
| 节点 ID | 40 字符十六进制,集群内唯一标识 |
| flags | myself/master/slave/fail?/fail/handshake 等状态标记 |
| IP | 节点地址 |
| 主节点 ID | 从节点记录其主节点 ID,主节点为 - |
| ping/pong | 最近发送和收到 PONG 的时间戳 |
| epoch | 当前节点的配置纪元 |
| 槽范围 | 主节点负责的槽区间,如 0-5460 或 5461-5460 10923-10923(多段) |
扩容时把一部分槽从旧节点迁移到新节点。迁移是逐个 key 进行的:
# 1. 通知目标节点准备导入槽 5461CLUSTER SETSLOT 5461 IMPORTING <source_node_id>
# 2. 通知源节点准备迁出槽 5461CLUSTER SETSLOT 5461 MIGRATING <target_node_id>
# 3. 逐个迁移 keyCLUSTER GETKEYSINSLOT 5461 100 # 每次取 100 个 keyMIGRATE 127.0.0.1 7001 "" 0 5000 KEYS key1 key2 ...
# 4. 迁移完毕,通知所有节点更新槽归属CLUSTER SETSLOT 5461 NODE <target_node_id>迁移期间,客户端访问正在迁移的槽可能收到 ASK 重定向,指向目标节点。这是 Cluster 处理在线扩缩容的关键机制。
5.3 MOVED 与 ASK 重定向
客户端把命令发给错误的节点时,会收到重定向响应:
MOVED:槽已经稳定地属于另一个节点,客户端应该更新本地路由表,后续请求直接发到新节点。
# 客户端向节点 A 请求槽 5461 的 key# 但 5461 已迁到节点 Bredis-cli -p 7000 GET mykey# (error) MOVED 5461 127.0.0.1:7001ASK:槽正在迁移中,源节点还有部分 key 没迁走。这次请求临时转发到目标节点,但客户端不更新路由表,下次还是先问源节点。
# 槽 5461 正在从 A 迁到 B# key1 还在 A 上,A 直接处理# key2 已经在 B 上,A 回复 ASKredis-cli -p 7000 GET key2# (error) ASK 5461 127.0.0.1:7001两者的区别在于永久性。MOVED 是“槽换主人了,记住”,ASK 是“这个 key 临时在别人那,这次过去找,下次还是先问我”。
| 重定向 | 触发场景 | 客户端动作 | 是否更新路由表 |
|---|---|---|---|
| MOVED | 槽归属已确定变更 | 重新发到新节点 | 是 |
| ASK | 槽正在迁移,key 临时在目标节点 | 临时发到目标节点,带 ASKING 标记 | 否 |
客户端收到 ASK 后,向目标节点发送请求时要带上 ASKING 命令,否则目标节点会拒绝(因为槽还没正式归它)。这是迁移期间保证一致性的细节。
5.4 Gossip 协议
Cluster 的节点之间通过 Gossip 协议交换状态信息。每个节点定期向少数几个随机节点发送 PING,PING 中携带自己已知的其他节点信息(一部分)。收到 PING 的节点回复 PONG,也携带自己知道的信息。这样信息像病毒一样在集群内传播,最终所有节点都知道彼此的状态。
Gossip 的关键参数:
# cluster.conf 默认配置(源码 cluster.h 中定义)cluster-node-timeout 15000 # 节点失联多久判定为故障(毫秒)cluster-announce-ip "" # 节点对外通告的 IP,默认空表示用自动探测到的 IP(NAT 环境需显式指定)cluster-announce-port 0 # 节点对外通告的端口cluster-announce-bus-port 0 # 集群总线端口(默认数据端口+10000)cluster-node-timeout 是核心参数。节点 A 超过这个时间没收到节点 B 的 PONG,就标记 B 为 PFAIL(疑似故障)。超过这个时间的一半没有 PONG 交换,就会触发更频繁的 Gossip。
Gossip 传播故障信息后,超过半数的主节点都标记 B 为 PFAIL,B 的状态就升级为 FAIL。然后 B 的从节点发起故障转移,提升为新主节点。
5.5 Cluster 故障转移
Cluster 的故障转移和哨兵类似,但由从节点自己发起,不需要外部组件:
从节点发起故障转移的选举规则:
从节点用复制偏移量计算自己的 rank:offset 越大(数据越接近主节点),rank 数值越小(rank 0 表示数据最新)。rank 越小的从节点越早发起选举,从而越容易抢到多数主节点的投票。主节点对它收到的第一个有效 FAILOVER_AUTH_REQUEST 投赞成票,先到先得,并不额外挑选“最优”从节点。
这里和哨兵的晋升规则有明显区别:哨兵的 Leader 会按 slave-priority → slave_repl_offset → runid 三级排序主动挑出最优从节点提升;而 Cluster 的故障转移由从节点自己发起,slave-priority 不参与自动选举的排名计算,只用于手动故障转移等场景。
从节点获得超过半数主节点的投票后,成为新主节点,接管原主节点的槽。整个过程不需要哨兵参与,Cluster 自治。
5.6 哨兵模式与 Cluster 对比
| 维度 | 哨兵模式 | Cluster |
|---|---|---|
| 数据分片 | 不支持,单主全量数据 | 支持,16384 槽分布到多节点 |
| 容量上限 | 单机内存 | 多机内存之和,可水平扩展 |
| 读写扩展 | 读可扩展(加从节点),写受单机限制 | 读写都可扩展(加主节点) |
| 故障转移 | 哨兵集群负责,外部组件 | Cluster 内部自治 |
| 客户端复杂度 | 低,连哨兵查主节点地址即可 | 高,需处理 MOVED/ASK 重定向 |
| 多键操作 | 支持,所有 key 在同一主节点 | 受限,需同一槽(哈希标签) |
| 运维复杂度 | 中等 | 较高,扩缩容涉及槽迁移 |
| 适用场景 | 数据量不大、写量不高、需要多键操作 | 数据量超单机内存、高并发读写 |
选择标准很简单:数据量在单机内存范围内、写量不高,用哨兵;数据量超单机内存、写量需要分摊,用 Cluster。
六、脑裂与数据丢失
主从复制最大的隐患不是性能,而是数据丢失。最典型的场景是脑裂(split-brain):网络分区导致主节点和从节点、哨兵之间失联,哨兵认为主节点故障,提升从节点为新主节点。这时出现两个主节点,旧主节点还在接受写入。分区恢复后,旧主节点降级为从节点,同步新主节点的数据,它在分区期间接受的写入全部丢失。
6.1 脑裂的成因
脑裂的本质是“主节点失联但还活着,仍在接受写入”。哨兵因为收不到主节点的响应而判定它故障,但实际上它还在服务部分客户端。这种情况下提升从节点,就会出现双主。
数据丢失量取决于网络分区持续时间和旧主节点的写入速率。分区几秒,丢几百条;分区几分钟,丢几万条。
6.2 min-slaves-to-write 防脑裂
Redis 提供两个配置项来缓解脑裂:
# redis.confmin-slaves-to-write 1 # 至少 1 个从节点同步正常,主节点才接受写入min-slaves-max-lag 10 # 从节点的同步延迟不超过 10 秒这两个配置配合工作。主节点每秒检查从节点的同步状态,如果同步正常的从节点数量少于 min-slaves-to-write,或者这些从节点的延迟超过 min-slaves-max-lag,主节点就拒绝写入。
脑裂场景下,旧主节点和所有从节点断开,同步正常的从节点数为 0,主节点拒绝写入,避免了数据丢失。
min-slaves-to-write 只能减少脑裂的数据丢失,不能完全杜绝。如果网络分区只隔离了部分从节点,旧主节点仍有足够数量的从节点同步正常,它会继续接受写入,分区恢复后这些写入仍可能丢失。
这两个值的权衡:
min-slaves-to-write设太高,从节点抖动会导致主节点频繁拒绝写入,影响可用性- 设太低,防不住脑裂
- 通常设为 1,配合
min-slaves-max-lag设为 10 到 30 秒
# 查看从节点同步状态redis-cli INFO replication# 关注每个从节点的 lag 字段# slave0:ip=127.0.0.1,port=6380,state=online,offset=1048576,lag=0# lag 表示从节点最后一次 ACK 距今多少秒6.3 复制延迟与数据丢失
除了脑裂,复制本身就是异步的,主节点接受写入后不等从节点确认就返回成功。如果主节点在写入后立即宕机,而这条写入还没同步给从节点,故障转移后这条写入就丢了。
这种丢失是异步复制的固有问题,无法完全避免。可以减少丢失窗口,但无法消除。min-slaves-to-write 在这里也有作用:强制至少一个从节点同步了数据,主节点才接受新写入,间接减小了丢失窗口。
完全避免数据丢失需要同步复制,但同步复制会显著降低写入性能,Redis 在设计上选择了异步复制换取性能。如果业务要求零丢失,应该用 Redis 的事务和 Lua 脚本保证原子性,配合持久化策略,而不是依赖复制。
七、踩坑与排查
理论讲完,看几个实际会遇到的坑。这些坑的共同点是表现隐蔽,等监控告警时已经造成影响。
7.1 脑裂导致写入丢失
现象:业务反馈部分写入“成功”了但查询不到,时间点对应一次主节点切换。
根因:网络分区期间旧主节点接受了写入,分区恢复后旧主节点降级为从节点,数据被新主节点的数据覆盖。
排查:
# 1. 查看哨兵故障转移日志redis-cli -p 26379 SENTINEL master mymaster# 关注 failover 相关时间戳
# 2. 对比旧主节点和新主节点的数据# 如果旧主节点还有日志,看它分区期间的写入命令# AOF 文件中可以找到这段时间的写命令记录解决:开启 min-slaves-to-write 和 min-slaves-max-lag。分区期间旧主节点因没有同步正常的从节点而拒绝写入,客户端收到错误后重试到新主节点。
待补充真实案例。脑裂导致的数据丢失在生产中确实发生,但具体丢失的数据量和恢复方式需要真实事故记录。
7.2 全量复制风暴
现象:主节点 CPU 和网络飙升,多个从节点同时请求全量同步,主节点负载过高导致延迟尖峰。
根因:多个从节点同时断连(比如网络抖动),断连时间超过 backlog 能覆盖的范围,全部回退全量复制。主节点为每个从节点执行 BGSAVE,多个 fork 叠加,内存和 CPU 承压。
排查:
# 查看从节点同步状态redis-cli INFO replication# 关注每个从节点的 state 和 master_sync_in_progress
# 查看主节点 fork 情况redis-cli INFO stats | grep -E "latest_fork_usec|total_forks"解决:
- 调大 backlog:让从节点断连后能走增量复制,避免回退全量。根据写入速率和可能断连时长计算大小。
- 级联复制:从节点不从主节点直接复制,而是从另一个从节点复制,分散主节点的 RDB 生成负担。
# 从节点 B 从从节点 A 复制,而不是从主节点复制# 在从节点 B 上配置redis-cli SLAVEOF <slave_a_ip> <slave_a_port>- 错峰同步:新加从节点时,避免多个同时加入,逐个加入,等前一个同步完再加下一个。
- 无盘复制:网络带宽充足时开启
repl-diskless-sync yes,配合repl-diskless-sync-delay等多个从节点一起同步。
7.3 backlog 太小反复全量
现象:从节点复制状态反复在 connect 和 sync 之间跳动,master_repl_offset 和 slave_repl_offset 差距大,INFO replication 里频繁看到 master_sync_in_progress:1。
根因:backlog 太小,从节点短暂断连(几秒)后请求增量复制,但请求的 offset 已经被覆盖,只能全量。全量还没完成又因为超时断开,再次请求增量,又回退全量,陷入死循环。
排查:
# 主节点查看 backlog 信息redis-cli INFO replication | grep -E "repl_backlog"# repl_backlog_size:1048576 # backlog 大小# repl_backlog_first_byte_offset:1 # 最早保留的 offset# repl_backlog_histlen:1048576 # 当前有效数据长度
# 如果 first_byte_offset 远大于从节点请求的 offset,说明已被覆盖解决:调大 repl-backlog-size。先用上一节的方法估算写入速率,然后根据从节点可能断连的最大时长计算。生产环境常见配置 64 MB 到 256 MB。
# 动态调整 backlog 大小(不需要重启)redis-cli CONFIG SET repl-backlog-size 256mb
# 调整后确认redis-cli CONFIG GET repl-backlog-size7.4 repl-timeout 导致断连
现象:从节点日志报 Connection with master lost,主节点日志报 replica timeout,但网络实际没问题。
根因:repl-timeout 默认 60 秒。如果主节点生成 RDB 时间超过 60 秒(数据量大),从节点等不到 RDB 传输开始,认为主节点失联,主动断开。或者 RDB 传输期间主从之间没有心跳,超过 60 秒后从节点断开。
排查:
# 查看主节点 RDB 生成耗时redis-cli INFO stats | grep latest_fork_usec# 如果 latest_fork_usec 超过 60 秒(60000000 微秒),就是这个问题
# 查看从节点复制超时配置redis-cli CONFIG GET repl-timeout解决:对大实例调大 repl-timeout。
# 大实例建议调到 180 秒或更长redis-cli CONFIG SET repl-timeout 180同时检查 repl-ping-replica-period(主节点向从节点发心跳的间隔,默认 10 秒)。心跳间隔应该远小于 repl-timeout,否则正常的心跳也可能被判超时。经验值是 repl-timeout 至少是心跳间隔的 6 倍。
八、运维要点
8.1 核心监控指标
复制相关的监控围绕“主从是否同步”和“同步是否健康”展开:
| 指标 | 获取方式 | 关注点 |
|---|---|---|
master_link_status | 从节点 INFO replication | up 正常,down 需立即排查 |
slave_repl_offset 差值 | 主节点 master_repl_offset 减从节点 slave_repl_offset | 差值持续增大说明同步跟不上 |
master_last_io_seconds_ago | 从节点 INFO replication | 从节点与主节点最后通信距今秒数,超过 repl-timeout 一半需警惕 |
master_sync_in_progress | 从节点 INFO replication | 1 表示正在全量同步,持续为 1 说明全量同步卡住 |
connected_slaves | 主节点 INFO replication | 已连接从节点数,和预期不符需排查 |
lag | 主节点 INFO replication 中每个 slave 的 lag 字段 | 从节点最后一次 ACK 距今秒数,超过 min-slaves-max-lag 会触发写入拒绝 |
repl_backlog_histlen | 主节点 INFO replication | backlog 当前有效数据长度,持续接近 repl_backlog_size 说明 backlog 快满 |
# 一键查看复制健康度(在从节点执行)redis-cli INFO replication | grep -E "master_link_status|slave_repl_offset|master_last_io|sync_in_progress"
# 在主节点查看所有从节点的 lagredis-cli INFO replication | grep slave8.2 关键参数速查
| 参数 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
repl-backlog-size | 1 MB | 64-256 MB | backlog 大小,太小导致回退全量 |
repl-backlog-ttl | 3600 | 0 | backlog 空闲释放时间,0 表示永不释放 |
repl-timeout | 60 | 60-180 | 复制超时,大实例调大 |
repl-ping-replica-period | 10 | 10 | 主节点发心跳间隔,应远小于 repl-timeout |
repl-diskless-sync | yes(7.0+) / no(6.2-) | yes(网络充足) | 无盘复制,省磁盘 I/O |
repl-diskless-sync-delay | 5 | 5-30 | 无盘复制延迟,等更多从节点一起同步 |
min-slaves-to-write | 0 | 1 | 防脑裂,至少 N 个从节点同步正常才接受写入 |
min-slaves-max-lag | 10 | 10-30 | 从节点最大允许延迟,配合上一项 |
client-output-buffer-limit replica | 256mb 64mb 60 | 256mb 64mb 60 | 从节点输出缓冲区限制,超限断开 |
slave-priority | 100 | 100 | 从节点优先级,0 表示永不提升为主 |
# 批量查看复制相关配置redis-cli CONFIG GET '*' | grep -E '^repl-|^min-slaves-|^slave-priority'8.3 min-slaves 配置的权衡
min-slaves-to-write 是一把双刃剑。设高了,从节点抖动会导致主节点拒绝写入,影响可用性。设低了,防不住脑裂。
权衡的思路:
- 先评估业务对可用性和数据一致性的要求。缓存场景偏向可用性,可以不设或设为 1;存储场景偏向一致性,必须设。
- 从节点数量是动态的,
min-slaves-to-write设为“常态从节点数减一”比较稳妥。比如常态 3 个从节点,设为 2,允许一个从节点抖动。 min-slaves-max-lag不要设太小,否则正常的复制延迟波动就会触发拒绝写入。10 秒到 30 秒是常见范围。- 配置后要在监控中关注
connected_slaves和每个 slave 的lag,确认从节点状态稳定。
8.4 故障转移演练
纸上谈兵不如实际演练。定期模拟主节点故障,验证故障转移是否符合预期:
# 1. 模拟主节点宕机(在主节点执行)redis-cli SHUTDOWN NOSAVE
# 2. 观察哨兵是否在 down-after-milliseconds 内检测到redis-cli -p 26379 SENTINEL master mymaster# 关注 flags 字段是否出现 S_DOWN 或 O_DOWN
# 3. 观察故障转移过程redis-cli -p 26379 SENTINEL failover mymaster# 或查看哨兵日志
# 4. 确认新主节点redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
# 5. 验证客户端是否自动切换到新主节点# 取决于客户端实现,多数 Redis 客户端支持哨兵模式故障转移演练要在非高峰期进行,并提前通知业务方。演练时关注三件事:故障检测时间、新主选举时间、客户端切换时间。这三个时间加起来就是业务感知到的不可用时长。
8.5 槽迁移在线扩容
Cluster 扩容时迁移槽,Redis 提供了 redis-cli --cluster 工具简化操作:
# 1. 加入新节点到集群redis-cli --cluster add-node 127.0.0.1:7003 127.0.0.1:7000
# 2. 给新节点分配从节点redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7000 --cluster-slave --cluster-master-id <new_node_id>
# 3. 重新分片,把一部分槽迁到新节点redis-cli --cluster reshard 127.0.0.1:7000# 交互式输入:迁移多少槽、目标节点 ID、源节点
# 4. 检查集群状态redis-cli --cluster check 127.0.0.1:7000redis-cli --cluster info 127.0.0.1:7000迁移过程中关注 CLUSTER NODES 中各节点的 cluster-require-full-coverage 配置。默认 yes 表示任何一个槽不在服务,整个集群拒绝写入。如果允许部分槽不可用时其他槽继续服务,设为 no,但可能导致跨槽操作不一致。
8.6 核心机制速查
| 机制 | 核心设计 | 关键取舍 |
|---|---|---|
| 全量复制 | BGSAVE 生成 RDB + 缓冲区存增量 | 简单可靠 vs 开销大 |
| 增量复制 | replid + offset + backlog 环形缓冲 | 高效 vs 断连过久回退全量 |
| 哨兵故障转移 | SDOWN 到 ODOWN 到 Leader 选举 | 自动化 vs 配置复杂 |
| Cluster 分片 | 16384 槽 + CRC16 + MOVED/ASK | 水平扩展 vs 客户端复杂、多键受限 |
| Gossip | 节点间随机传播状态 | 去中心化 vs 传播延迟 |
| 防脑裂 | min-slaves-to-write + max-lag | 减少丢数据 vs 可能影响可用性 |
参考资料
- Redis Replication - 官方文档,主从复制机制与配置项说明
- Redis Sentinel Documentation - 官方文档,哨兵的监控、故障转移与配置
- Redis Cluster Specification - 官方集群规范,哈希槽、Gossip、故障转移的实现细节
- PSYNC2: Partial Resynchronization - Redis 源码 replication.c,PSYNC2 协议与 replid2 的实现
- Redis Cluster Tutorial - 官方教程,集群搭建、扩缩容与槽迁移操作
- min-slaves-to-write - 官方文档,防脑裂配置的原理与权衡
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






