Redis-10 主从复制原理
1. 为什么需要主从复制
单机 Redis 有三个无法回避的问题:
- 单点故障:机器宕机、进程崩溃就完全不可用。即使有持久化,重启 + 加载 10GB 数据也要几分钟;
- 读性能上限:单实例受单线程和网卡带宽限制;
- 数据丢失风险:磁盘损坏就彻底没了。
主从复制是解决这些问题的基础——注意是"基础"而不是"方案",因为主从复制本身不提供自动故障转移(那是哨兵和 Cluster 的职责)。
主从复制提供:
| 能力 | 说明 |
|---|---|
| 数据冗余 | 从节点保存一份完整数据,是热备份 |
| 读扩展 | 读请求分摊到从节点(读写分离) |
| 故障恢复的基础 | 主节点挂了可以手动/自动把从节点提升为主 |
| 持久化压力转移 | 可以只在从节点做 RDB/AOF,主节点专注处理请求 |
| 数据分析隔离 | 在从节点跑耗时的统计查询,不影响线上 |
2. 搭建主从
2.1 三种建立方式
# 方式一:配置文件(重启生效,永久)
# 在从节点的 redis.conf 里
replicaof 192.168.1.10 6379 # 5.0+ 推荐写法
# slaveof 192.168.1.10 6379 # 旧写法,仍兼容
masterauth <主节点密码> # 主节点有密码时必须配
masteruser <用户名> # 6.0+ ACL 场景
# 方式二:启动参数
redis-server --replicaof 192.168.1.10 6379 --port 6380
# 方式三:运行时命令(立即生效,不写配置文件)
redis-cli -p 6380 REPLICAOF 192.168.1.10 6379
redis-cli -p 6380 CONFIG SET masterauth <密码>
# 取消复制,变回独立主节点(会保留当前已有的数据)
redis-cli -p 6380 REPLICAOF NO ONE
重要:REPLICAOF 是运行时命令,重启后失效。要永久生效必须 CONFIG REWRITE 或改配置文件。
2.2 一主两从的完整示例
# 主节点 6379
mkdir -p /data/redis/{6379,6380,6381}
cat > /data/redis/6379/redis.conf <<'EOF'
port 6379
dir /data/redis/6379
logfile "redis-6379.log"
daemonize yes
requirepass mypassword
masterauth mypassword # 主节点也要配!因为它可能被降级为从节点
appendonly yes
EOF
# 从节点 6380
cat > /data/redis/6380/redis.conf <<'EOF'
port 6380
dir /data/redis/6380
logfile "redis-6380.log"
daemonize yes
requirepass mypassword
masterauth mypassword
replicaof 127.0.0.1 6379
replica-read-only yes
appendonly yes
EOF
# 从节点 6381 同理,端口改成 6381
redis-server /data/redis/6379/redis.conf
redis-server /data/redis/6380/redis.conf
redis-server /data/redis/6381/redis.conf
注意主节点也要配 masterauth:因为在故障转移后它可能被降级为从节点,那时它需要用这个密码去连新主节点。这是常见的配置疏漏。
2.3 验证
# 主节点视角
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=1234,lag=0
slave1:ip=127.0.0.1,port=6381,state=online,offset=1234,lag=1
master_failover_state:no-failover
master_replid:8371b4fb1155b71f4a04d3e1bc3e18bd2c3f4a5b
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1234
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1234
# 从节点视角
127.0.0.1:6380> INFO replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up # 【关键】up = 连接正常,down = 断开
master_last_io_seconds_ago:0 # 距上次与主节点交互多久
master_sync_in_progress:0 # 是否正在做全量同步
slave_read_repl_offset:1234
slave_repl_offset:1234 # 从节点已复制到的偏移量
slave_priority:100 # 故障转移时的优先级,0 = 永不被选为主
slave_read_only:1
replica_announced:1
connected_slaves:0
master_replid:8371b4fb... # 与主节点相同
master_repl_offset:1234
核心验证指标:
- 主节点的
connected_slaves和slaveN的state=online; - 从节点的
master_link_status:up; - 主从的
master_repl_offset是否接近(差值就是复制延迟的字节数)。
# 功能验证
redis-cli -p 6379 -a mypassword SET k v
redis-cli -p 6380 -a mypassword GET k # 应该能读到
redis-cli -p 6380 -a mypassword SET k2 v2
# (error) READONLY You can't write against a read only replica.
2.4 拓扑结构
(1)一主多从(最常见)
Master
/ | \
Slave1 Slave2 Slave3
优点:结构简单清晰。 缺点:从节点越多,主节点的复制压力越大(要给每个从节点维护一个复制缓冲区并分别发送命令流)。
(2)级联复制(从节点的从节点)
Master
|
Slave1 ← 它既是从节点,也是 Slave2/Slave3 的主节点
/ \
Slave2 Slave3
从节点可以有自己的从节点(叫 sub-replica)。这减轻了主节点的压力,把复制流的分发下沉。
代价:延迟叠加(数据要经过两跳),且中间节点故障会影响下游。
redis-cli -p 6381 REPLICAOF 127.0.0.1 6380 # 6381 从 6380 复制
注意:默认情况下从节点是只读的,但它仍然会把自己收到的复制流转发给下游从节点。
(3)主主复制(Redis 不支持)
Redis 没有双向复制/多主写入的能力。想要多点写入只能用 Cluster(分片,每个 slot 只有一个主)或者第三方方案(如 Redis Enterprise 的 CRDT)。
3. 复制的完整流程
3.1 三个阶段
阶段一:建立连接与握手
阶段二:数据同步(全量 或 部分)
阶段三:命令持续传播
3.2 阶段一:建立连接
从节点执行 REPLICAOF host port 后:
1. 从节点把主节点信息存入 server.masterhost/masterport,立即返回 OK
(所以 REPLICAOF 是【异步】的,返回时同步还没开始)
2. 从节点的定时任务 replicationCron(每秒执行)发现有 masterhost 但没连接
→ 创建到主节点的 socket 连接
3. 【握手过程】(在 syncWithMaster 状态机里逐步推进):
从 → 主:PING
主 → 从:+PONG ← 检测网络和主节点是否能处理命令
从 → 主:AUTH <password> ← 如果配了 masterauth
从 → 主:REPLCONF listening-port <从节点端口>
从 → 主:REPLCONF ip-address <从节点IP> ← 如果配了 replica-announce-ip
从 → 主:REPLCONF capa eof capa psync2 ← 声明自己支持的能力
从 → 主:PSYNC <replid> <offset> ← 请求同步!
REPLCONF capa 声明的两个能力:
eof:支持"无盘复制"(RDB 以流式传输,用 EOF 标记结束而不是先告知长度);psync2:支持 4.0 引入的 PSYNC2 协议(改进的部分重同步)。
3.3 阶段二:PSYNC 与两种同步
PSYNC <replid> <offset>
- 首次同步:从节点不知道主节点的 replid,发送
PSYNC ? -1; - 断线重连:发送
PSYNC <上次的replid> <已复制到的offset+1>。
主节点的响应有三种:
| 响应 | 含义 |
|---|---|
+FULLRESYNC <replid> <offset> |
做全量同步,并告知从节点新的 replid 和当前 offset |
+CONTINUE [<new replid>] |
做部分重同步(只补发缺失的命令) |
-ERR |
主节点版本 < 2.8,不支持 PSYNC,降级用老的 SYNC |
3.4 全量同步(Full Resynchronization)
1. 主节点收到 PSYNC ? -1(或判断无法部分重同步)
→ 回复 +FULLRESYNC <replid> <offset>
2. 主节点执行 BGSAVE(fork 子进程生成 RDB)
├─ 如果已经有一个 BGSAVE 在跑且是为复制而做的,可以【复用】它
└─ 从此刻起,主节点把所有新的写命令同时写入
【该从节点的复制缓冲区(client output buffer)】
3. RDB 生成完毕 → 主节点把 RDB 文件发给从节点
4. 从节点收到 RDB:
├─ 先把 RDB 存到磁盘临时文件(或直接从 socket 加载,见 3.7)
├─ 【清空自己现有的全部数据】(flushall!replica-lazy-flush 控制是否异步)
└─ 加载 RDB 到内存
↑ 【这期间从节点是阻塞的】,无法响应请求
5. 主节点把复制缓冲区里积累的命令发给从节点,从节点执行
(追上主节点当前的进度)
6. 之后进入命令持续传播阶段
几个关键点:
(1)从节点会清空自己所有数据
这是最危险的一点。如果误操作让一个有数据的节点去复制一个空节点,它的数据会被全部清空。
真实事故场景:主节点没开持久化,重启后成了空实例,从节点检测到连接恢复后做全量同步 → 整个集群的数据被清空。
(2)全量同步的开销巨大
- 主节点 fork(第 7 篇:每 GB 约 20~100ms 阻塞);
- 主节点生成 RDB 的 CPU 和磁盘 IO;
- 网络传输整个数据集(10GB 数据在千兆网上要 80+ 秒,且占满带宽);
- 从节点加载 RDB 期间阻塞;
- 主节点为该从节点维护的复制缓冲区在此期间不断增长。
所以必须尽量避免全量同步,这是复制优化的核心目标。
(3)从节点加载 RDB 期间怎么响应请求
replica-serve-stale-data yes # 默认 yes
yes(默认):从节点在同步期间(或与主节点失联时)仍然响应读请求,但返回的是旧数据(可能很旧)。首次同步时数据集是空的,会返回大量 nil;no:除了少数命令(INFO、REPLICAOF、AUTH、PING、SHUTDOWN、SUBSCRIBE等),其他所有命令都返回错误:
-MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'.
选择建议:对数据一致性敏感的业务设 no(宁可报错也不返回脏数据,让客户端故障转移到其他节点);对可用性优先的缓存场景保持 yes。
3.5 部分重同步(Partial Resynchronization)
2.8 引入的关键优化:短暂断线重连后,只补发缺失的那部分命令,不做全量同步。
这依赖三个东西:replid、offset、repl_backlog。
复制偏移量 offset
主节点和每个从节点都维护一个 offset(字节数):
- 主节点的
master_repl_offset:它累计传播了多少字节的命令; - 从节点的
slave_repl_offset:它累计接收并处理了多少字节。
两者的差值就是复制延迟(字节数)。
# 主节点
master_repl_offset:100000
slave0:...,offset=99500 # 这个从节点落后 500 字节
# 从节点
slave_repl_offset:99500
复制积压缓冲区 repl_backlog
主节点维护一个固定大小的环形缓冲区(circular buffer),保存最近传播出去的命令流。
repl-backlog-size 1mb # 默认 1MB(太小!)
repl-backlog-ttl 3600 # 没有从节点连接后,多久释放 backlog(秒)
关键特性:
- 全局只有一个(不是每个从节点一个),所有从节点共用;
- 是环形的:写满后覆盖最旧的数据;
- 只在有从节点时才创建(第一个从节点连上时创建),所有从节点断开且超过
repl-backlog-ttl后释放。
127.0.0.1:6379> INFO replication
repl_backlog_active:1
repl_backlog_size:1048576 # 缓冲区大小
repl_backlog_first_byte_offset:50000 # 缓冲区里最旧数据对应的 offset
repl_backlog_histlen:1048576 # 当前缓冲区里有多少有效字节
判断能否部分重同步
从节点重连时发 PSYNC <replid> <offset>,主节点检查两个条件:
条件一:replid 匹配
从节点发来的 replid == 主节点的 replid(或 replid2,见 3.6)
→ 确认从节点之前复制的确实是"我"(或我的前任)
条件二:offset 还在 backlog 范围内
repl_backlog_first_byte_offset <= offset <= master_repl_offset
→ 从节点缺失的那段数据还没被环形缓冲区覆盖掉
两个条件都满足 → +CONTINUE,从 backlog 里把 offset 之后的数据发过去
任一条件不满足 → +FULLRESYNC,做全量同步
repl-backlog-size 怎么设(重要实践)
默认 1MB 在生产环境几乎肯定不够。
计算公式:
repl-backlog-size >= 平均写入速率(bytes/s) × 最长可能的断线时间(s) × 安全系数
举例:写入速率 5MB/s,网络抖动可能持续 30 秒,安全系数 2:
5MB/s × 30s × 2 = 300MB
实践建议:
- 至少设 64MB,高写入量的实例设 128MB ~ 512MB;
- 这块内存是固定占用的(只要有从节点连接),要算进
maxmemory的余量里; - 怎么判断是否够用:监控
sync_full(全量同步次数)和sync_partial_err(部分重同步失败次数),如果这两个持续增长就说明 backlog 太小。
127.0.0.1:6379> INFO stats
sync_full:3 # 全量同步次数(应该很少,只在新从节点接入时)
sync_partial_ok:15 # 部分重同步成功次数
sync_partial_err:2 # 【关键】部分重同步失败次数(说明 backlog 不够)
3.6 PSYNC2:replid 与 replid2(4.0+)
2.8 版 PSYNC 的问题
2.8 的部分重同步只在"从节点断线重连到同一个主节点“时有效。两个常见场景会退化成全量同步:
场景一:从节点重启
从节点重启后内存里的 replid 和 offset 全丢了,只能 PSYNC ? -1 做全量同步。
场景二:主从切换(故障转移)
原来:Master A ← Slave B, Slave C
A 宕机,B 被提升为新主节点
C 要改成从 B 复制
但 C 记录的 replid 是 A 的,B 的 replid 是它自己新生成的
→ replid 不匹配 → C 必须对 B 做全量同步!
这在故障转移时是灾难:本来只是切换主节点,结果所有从节点都要全量同步一遍,网络和 CPU 被打满,故障恢复时间大幅延长。
PSYNC2 的两个改进
改进一:从节点持久化复制信息
从节点会把 replid 和 offset 写入 RDB 文件的 AUX 字段(repl-id、repl-offset)。重启后从 RDB 加载这些信息,就能尝试部分重同步。
限制:只在”优雅重启“时有效——因为只有 SHUTDOWN 时生成的 RDB 才包含最新的 offset。如果是 kill -9 或崩溃,RDB 里的 offset 是旧的(不匹配当前数据状态),Redis 会拒绝使用它做部分重同步。
改进二:引入 replid2(关键)
每个节点维护两个 replication ID:
| 字段 | 含义 |
|---|---|
master_replid |
当前的复制 ID |
master_replid2 |
上一个复制 ID(继承来的) |
second_repl_offset |
replid2 有效的 offset 上界 |
故障转移时的处理:
1. Slave B 被提升为主节点时:
├─ 把自己原来的 master_replid(即 A 的 ID)保存到 replid2
├─ 把当前 offset 保存到 second_repl_offset
└─ 生成一个【新的】 master_replid(因为它现在是一个新的复制历史的开端)
2. Slave C 来连 B,发送 PSYNC <A的replid> <offset>
3. B 检查:
├─ <A的replid> == 我的 master_replid? 不等
├─ <A的replid> == 我的 replid2? 【相等!】
├─ 且 offset <= second_repl_offset? 是
└─ → 可以部分重同步!回复 +CONTINUE <B的新replid>
(同时告知 C 新的 replid,让 C 更新)
4. C 更新自己的 replid 为 B 的新 ID,继续增量复制
这就让故障转移不再触发全量同步,是 4.0 一个非常重要的可用性改进。
为什么要生成新的 replid 而不直接沿用 A 的:因为 B 成为主节点后,它的数据历史和 A 已经分叉了(A 可能有一些还没同步给 B 的写入)。用新 replid 标识"这是一段新的复制历史”,避免其他节点误以为可以从 A 的任意 offset 继续。
# 查看
127.0.0.1:6379> INFO replication
master_replid:8371b4fb1155b71f4a04d3e1bc3e18bd2c3f4a5b
master_replid2:0000000000000000000000000000000000000000 # 全 0 表示没有前任
second_repl_offset:-1 # -1 表示无效
3.7 无盘复制(Diskless Replication)
默认的全量同步要先在主节点磁盘上生成 RDB 文件,再读出来发送。如果主节点的磁盘很慢(或者是磁盘紧张的容器环境),这一步会成为瓶颈。
# 主节点:直接把 RDB 通过 socket 流式发给从节点,不落盘
repl-diskless-sync yes # 6.0+ 默认 yes(之前默认 no)
repl-diskless-sync-delay 5 # 等待更多从节点一起同步的秒数
# 从节点:直接从 socket 加载 RDB,不先存磁盘
repl-diskless-load disabled # disabled | on-empty-db | swapdb
repl-diskless-sync-delay 的作用:如果同时有多个从节点要做全量同步,等待几秒让它们都到齐,然后只 fork 一次、生成一份 RDB 流同时发给所有从节点。这能大幅节省主节点开销。代价是先到的从节点要多等几秒。
从节点的 repl-diskless-load 三个选项:
| 值 | 说明 |
|---|---|
disabled(默认) |
先把 RDB 存到磁盘临时文件,再加载。最安全(如果传输中断,旧数据还在) |
on-empty-db |
只在自己数据库为空时才直接从 socket 加载(比较安全的折中) |
swapdb |
总是直接从 socket 加载,把当前数据库暂存在内存里(内存占用翻倍,但传输失败可以回滚) |
建议:主节点开 repl-diskless-sync yes(尤其磁盘慢时);从节点保持 disabled 或用 on-empty-db,swapdb 只在内存充足且想避免磁盘 IO 时用。
3.8 阶段三:命令持续传播
同步完成后,主节点把每个写命令异步地发给所有从节点。
客户端 --SET k v--> 主节点
├─ 执行命令,修改内存
├─ 【立即返回 +OK 给客户端】 ← 注意这里就返回了
├─ 写入 AOF 缓冲区
├─ 写入 repl_backlog
└─ 写入每个从节点的复制缓冲区(异步发送)
这就是"异步复制",也是 Redis 主从可能丢数据的根源(第 7 篇讲过)。
心跳机制
主 → 从:每 repl-ping-replica-period 秒发一次 PING
repl-ping-replica-period 10 # 默认 10 秒
作用:让从节点确认主节点还活着。从节点如果超过 repl-timeout 没收到任何数据(包括 PING),就认为主节点挂了,断开重连。
从 → 主:每秒发一次 REPLCONF ACK <offset>
作用有三个:
- 上报自己的复制进度,让主节点知道每个从节点落后多少(
INFO replication里slaveN的offset和lag就来自这里); - 实现
WAIT命令:主节点靠这些 ACK 判断有多少从节点已经收到了指定 offset; - 检测从节点是否存活:主节点如果超过
repl-timeout没收到某从节点的 ACK,就断开它。
repl-timeout 60 # 默认 60 秒
注意 repl-timeout 的一个坑:在做全量同步时,如果 RDB 很大、传输时间超过 repl-timeout,会导致同步被误判为超时而中断,然后重试,陷入死循环。所以大实例要把 repl-timeout 设得足够大(比如 300 秒)。
复制的延迟与无延迟
repl-disable-tcp-nodelay no # 默认 no
no(默认):开启 TCP_NODELAY,命令立即发送。延迟低(几毫秒),但小包多、占用更多带宽;yes:禁用 TCP_NODELAY,启用 Nagle 算法,把小包合并后发送。节省带宽但增加延迟(可能 40ms)。
建议:同机房保持 no(默认)追求低延迟;跨机房/跨地域且带宽紧张时可以设 yes。
4. 复制中的关键配置
4.1 完整配置清单
# ---------- 从节点侧 ----------
replicaof <masterip> <masterport>
masterauth <password> # 主节点密码
masteruser <username> # 6.0+ ACL
# 从节点是否只读(强烈建议 yes)
replica-read-only yes
# 与主节点失联/同步中时,是否仍响应读请求(返回旧数据)
replica-serve-stale-data yes
# 从节点在故障转移中的优先级,0 表示【永不】被提升为主节点
replica-priority 100
# 全量同步前清空数据时是否异步(避免阻塞)
replica-lazy-flush yes
# 从节点是否直接从 socket 加载 RDB
repl-diskless-load disabled
# 向主节点声明自己的 IP/端口(NAT、Docker 环境必需)
replica-announce-ip 1.2.3.4
replica-announce-port 6380
# ---------- 主节点侧 ----------
# 复制积压缓冲区大小(默认 1mb 太小!)
repl-backlog-size 128mb
repl-backlog-ttl 3600
# 无盘复制
repl-diskless-sync yes
repl-diskless-sync-delay 5
# 心跳与超时
repl-ping-replica-period 10
repl-timeout 60
# 是否禁用 TCP_NODELAY(yes 省带宽但增加延迟)
repl-disable-tcp-nodelay no
# 【重要】至少要有多少个从节点在线、且延迟小于多少秒,主节点才接受写入
min-replicas-to-write 1
min-replicas-max-lag 10
# 从节点的输出缓冲区限制(复制缓冲区)
client-output-buffer-limit replica 512mb 128mb 60
4.2 min-replicas-to-write:防止数据丢失
min-replicas-to-write 1 # 至少 1 个从节点在线
min-replicas-max-lag 10 # 且该从节点的 lag <= 10 秒
含义:如果满足条件的从节点数量少于 min-replicas-to-write,主节点拒绝所有写请求:
(error) NOREPLICAS Not enough good replicas to write.
作用:这是一种"宁可不可写,也不要丢数据“的保护。
考虑这个场景(脑裂的经典案例):
1. 主节点 M 与所有从节点、哨兵的网络断开(但 M 自己还活着,客户端能连到它)
2. 哨兵认为 M 挂了,把从节点 S 提升为新主节点
3. 【此时有两个主节点】:旧主 M(在网络分区的一侧)和新主 S
4. 部分客户端还在往 M 写数据(因为它们连的是 M)
5. 网络恢复,M 发现有了新主节点 S,M 被降级为从节点
6. M 作为从节点,必须【清空自己的数据】然后从 S 全量同步
→ 【步骤 4 里写入 M 的所有数据永久丢失】
设置 min-replicas-to-write 1 后:步骤 4 中 M 发现自己没有任何健康的从节点,直接拒绝写入,从而避免了这些数据的丢失(客户端会收到错误,可以重试或降级)。
代价:牺牲可用性换一致性。从节点全挂时主节点也不可写了。
建议:
- 数据重要的场景开启,一主两从时设
min-replicas-to-write 1; - 纯缓存场景可以不开(可用性优先);
- 注意这个配置不能完全防止脑裂,只能缩小丢数据的窗口(在从节点被判定为"不健康"之前的那段时间,写入仍会成功)。
4.3 replica-read-only:从节点必须只读
replica-read-only yes # 默认 yes,【强烈建议保持】
如果设为 no,从节点可以被写入。但这非常危险:
- 写入的数据不会传播到任何地方(从节点不会把自己的写入传给主节点或其他从节点);
- 主从数据不一致,而且这种不一致很难发现;
- 下次全量同步时,从节点上写入的数据会被全部清空;
- 唯一"合理"的用途是在从节点上创建一些临时的、本地的键(比如做数据分析时的中间结果),但这种需求应该用独立实例。
4.4 replica-announce-ip:容器与 NAT 环境必配
从节点在握手时会用 REPLCONF listening-port 告知主节点自己的端口,主节点则通过 socket 得到从节点的 IP。
在 Docker/K8s/NAT 环境下,主节点看到的可能是网关 IP 或容器内网 IP,这会导致:
INFO replication里显示的从节点地址是错的;- 哨兵无法正确发现和连接从节点(哨兵是通过主节点的
INFO来发现从节点的),导致故障转移失败。
replica-announce-ip 公网或可访问的IP
replica-announce-port 映射后的端口
这是容器化部署 Redis 主从/哨兵时最常见的坑。
5. 读写分离的问题
读写分离看起来很美好(读扩展),但有一系列必须知道的坑。
5.1 主从延迟导致读到旧数据
这是最根本的问题,无法完全解决(因为复制是异步的)。
时刻 T1:客户端向主节点写入 SET user:1001:name "new"
时刻 T2:主节点返回 OK,客户端认为写成功
时刻 T3:客户端立即从从节点读 GET user:1001:name
→ 【可能读到旧值 "old"】,因为复制还没到达
时刻 T4:复制到达从节点
典型的用户可感知问题:“我刚改了昵称,刷新页面还是旧的”。
应对方案:
| 方案 | 说明 | 代价 |
|---|---|---|
| 关键读走主节点 | 写后立即要读的场景强制读主 | 主节点压力大 |
WAIT 命令 |
写完 WAIT 1 100 等复制确认再返回 |
每次写多一个 RTT,吞吐下降 |
| 延迟双读/重试 | 从库读不到就读主库 | 复杂度上升 |
| 业务容忍 | 大部分场景(如浏览量、列表)能容忍几十毫秒延迟 | 最常用 |
| 会话粘性 | 同一用户的读写在一段时间内都走主节点 | 需要客户端/中间件支持 |
| 监控延迟并摘除 | 延迟大的从节点自动从读列表移除 | 需要监控体系 |
5.2 从节点的过期数据问题
第 6 篇讲过:从节点不主动删除过期 key,要等主节点发 DEL。
- 3.2+:从节点读取时会做逻辑过期判断,返回 nil。不会返回过期数据(正确);
- 3.2 之前:有 bug,从节点会直接返回已过期的值。
所以用读写分离必须用 3.2 以上的版本。
另一个表现:从节点的内存可能高于主节点(堆积着逻辑已过期但未收到 DEL 的 key)。
5.3 从节点故障时的处理
如果客户端把读请求发到一个已经宕机或与主节点失联的从节点:
replica-serve-stale-data yes(默认)→ 返回可能非常旧的数据,客户端察觉不到问题;replica-serve-stale-data no→ 返回MASTERDOWN错误,客户端可以感知并切换。
建议:读写分离场景下,配合客户端的健康检查和故障摘除。不要只依赖 Redis 自身。
5.4 读写分离到底该不该用
这是一个需要仔细权衡的决定,很多团队高估了它的价值。
读写分离的收益有限的原因:
- Redis 的单实例读性能已经很高(10 万 QPS 级别),大部分业务的读 QPS 远达不到瓶颈;
- 复杂度显著上升:要处理延迟、要管理从节点列表、要处理故障摘除、要区分哪些读能走从库;
- 一致性问题的排查成本高:“偶尔读到旧数据"这种问题极难复现和定位;
- 从节点越多,主节点的复制负担越大(每个从节点一个复制缓冲区 + 独立发送)。
更好的替代方案:
| 目标 | 更好的方案 |
|---|---|
| 提高读性能 | 用 Cluster 分片(读写都分散,且没有一致性问题) |
| 挡住热点 key 的读 | 本地缓存(进程内 caffeine/bigcache)+ Redis 6.0 的客户端缓存 |
| 减少主节点压力 | 优化访问模式(pipeline、减少大 key、批量命令) |
什么时候读写分离才真正合适:
- 读 QPS 极高且远大于写(比如 100:1 以上),且已经确认主节点是瓶颈;
- 能明确接受最终一致性的场景(如商品浏览、内容展示);
- 需要在从节点跑重查询(数据分析、
BITCOUNT批量统计、全量扫描导出)——这是读写分离最有价值的用途; - 已经有成熟的中间件/客户端支持(如 Redis Cluster 的
READONLY模式、代理层如 Twemproxy/Codis/predixy)。
6. 常见故障与排查
6.1 复制风暴(Replication Storm)
现象:主节点反复 fork 生成 RDB、CPU 和网络被打满、从节点反复重连、集群不可用。
成因链条:
1. 某个从节点因网络抖动断开
2. 重连时 offset 已不在 backlog 范围内(backlog 太小)→ 触发全量同步
3. 主节点 fork + 生成 RDB + 传输(占用大量 CPU、内存、带宽)
4. 传输期间该从节点的复制缓冲区不断增长
5. 缓冲区超过 client-output-buffer-limit replica 的限制 → 【主节点断开该从节点】
6. 从节点重连 → 又是全量同步 → 回到步骤 3
【无限循环】
如果有多个从节点同时陷入这个循环,主节点会被彻底压垮。
排查:
# 1. 看全量同步次数是否异常
redis-cli INFO stats | grep -E 'sync_full|sync_partial'
# sync_full 持续增长 = 有问题
# 2. 看从节点的复制缓冲区大小
redis-cli CLIENT LIST TYPE replica
# 看 omem 字段
# 3. 看日志
grep -E 'Starting BGSAVE for SYNC|Synchronization with replica|scheduled to be closed' redis.log
解决:
# 1. 【最关键】调大复制积压缓冲区,避免断线重连就全量同步
repl-backlog-size 256mb
# 2. 调大从节点的输出缓冲区限制(或干脆不限制)
client-output-buffer-limit replica 1gb 256mb 120
# 甚至 client-output-buffer-limit replica 0 0 0(不限,但要监控内存)
# 3. 调大复制超时(避免大 RDB 传输被误判超时)
repl-timeout 300
# 4. 开启无盘复制减少磁盘瓶颈
repl-diskless-sync yes
repl-diskless-sync-delay 5
# 5. 【根本解法】控制单实例内存 <= 10GB,用 Cluster 分片
6.2 主节点没开持久化导致数据全丢
这是最危险的事故,务必牢记。
1. 主节点 M 为了性能关闭了持久化(save "" + appendonly no)
2. M 因为某种原因重启(OOM、运维误操作、机器重启、systemd 自动重启)
3. M 启动后是【空的】,但它仍然是主节点
4. 从节点 S 检测到与 M 的连接恢复,发起 PSYNC
5. offset 不匹配(M 重启后 replid 变了)→ 全量同步
6. S 【清空自己的数据】,从 M 加载一个空 RDB
→ 【全集群数据永久丢失】
防范措施:
- 主节点至少保留低频 RDB(如
save 3600 1)作为兜底,不要完全关闭持久化; - 关闭主节点的自动重启:把 systemd 的
Restart=always改成Restart=no或on-failure加上重启次数限制,避免它悄悄重启; - 主节点重启后,绝不能让它自动以主节点身份恢复服务。正确流程是:先让它作为从节点从有数据的节点同步,确认数据完整后再考虑切换;
- 用哨兵管理拓扑:哨兵会记录拓扑关系,主节点重启后哨兵会把它设为从节点(但仍然要小心,见 Q9)。
6.3 master_link_status 一直是 down
排查清单:
# 1. 网络能不能通
telnet <master_ip> 6379
# 2. 密码对不对(最常见)
grep masterauth /path/to/replica.conf
# 从节点日志会报:
# MASTER aborted replication with an error: NOAUTH Authentication required
# 或 -ERR invalid password
# 3. 主节点的 bind 和 protected-mode
# 如果主节点 bind 127.0.0.1,从节点在另一台机器上就连不上
redis-cli -h master CONFIG GET bind
redis-cli -h master CONFIG GET protected-mode
# 4. 主节点的 maxclients 是否满了
redis-cli -h master INFO clients
# 5. 从节点日志(最重要,直接看错误原因)
tail -100 replica.log
# 6. 版本兼容性:从节点版本不能低于主节点太多
# (高版本 RDB 格式低版本读不了)
redis-cli -h master INFO server | grep redis_version
6.4 主从数据不一致的排查
# 1. 比较 key 数量(注意会包含未删除的过期 key,只能粗略参考)
redis-cli -h master DBSIZE
redis-cli -h replica DBSIZE
# 2. 比较复制偏移量(最准确的实时判断)
redis-cli -h master INFO replication | grep master_repl_offset
redis-cli -h replica INFO replication | grep slave_repl_offset
# 持续存在大差值 = 复制延迟;差值不变但不等 = 复制卡住了
# 3. 检查从节点是否被误写过
redis-cli -h replica CONFIG GET replica-read-only # 应该是 yes
# 4. 用工具做全量比对(大数据量时)
# redis-full-check(阿里开源,配合 redis-shake)
可能的不一致来源:
- 正常的复制延迟(最常见,不是真正的不一致);
- 从节点被写入过(
replica-read-only no); - 过期 key 未同步删除(表现为
DBSIZE不同,但读取都返回 nil,实际是一致的); - 网络分区期间的脑裂写入;
- 主从版本差异导致的行为差异(比如某个命令在不同版本的语义不同);
- 在从节点上执行了
FLUSHDB/DEBUG等操作。
6.5 监控指标清单
# ---------- 主节点 ----------
connected_slaves # 从节点数量,减少了要告警
slaveN:...,lag=X # 每个从节点的延迟(秒)
master_repl_offset # 主节点的复制偏移量
sync_full # 【关键】全量同步次数,持续增长要排查
sync_partial_ok # 部分重同步成功次数
sync_partial_err # 【关键】部分重同步失败次数(backlog 太小)
repl_backlog_histlen # backlog 当前使用量
# ---------- 从节点 ----------
master_link_status # 【最关键】必须是 up
master_last_io_seconds_ago # 距上次与主节点交互的秒数,异常大说明卡住了
master_sync_in_progress # 是否正在全量同步
slave_repl_offset # 从节点偏移量
slave_read_only # 应该是 1
# ---------- 延迟计算 ----------
复制延迟(字节) = master_repl_offset - slave_repl_offset
复制延迟(秒) = slaveN 里的 lag 字段
告警规则建议:
| 指标 | 阈值 |
|---|---|
master_link_status |
!= up 立即告警 |
connected_slaves |
< 期望值 立即告警 |
| 复制延迟字节数 | > 10MB 告警 |
lag |
> 30 秒 告警 |
sync_full |
5 分钟内增长 > 1 次 告警 |
sync_partial_err |
任何增长都要关注 |
master_last_io_seconds_ago |
> repl-ping-replica-period × 2 告警 |
7. 手动故障转移
在没有哨兵的环境下(或哨兵故障时),需要手动切换主从。
7.1 安全的手动切换流程
# 场景:主节点 M(6379) 要下线维护,把从节点 S(6380) 提升为主
# 1. 确认 S 的数据已经追上 M
redis-cli -p 6379 INFO replication | grep master_repl_offset
redis-cli -p 6380 INFO replication | grep slave_repl_offset
# 两个值应该相等或极接近
# 2. 【关键】阻止新的写入进入 M,等待复制完全追上
redis-cli -p 6379 CLIENT PAUSE 5000 WRITE # 6.2+ 只暂停写
# 或者更彻底:CONFIG SET min-replicas-to-write 999(让它拒绝写)
# 3. 再次确认 offset 完全相等
redis-cli -p 6379 INFO replication | grep master_repl_offset
redis-cli -p 6380 INFO replication | grep slave_repl_offset
# 4. 把 S 提升为主节点
redis-cli -p 6380 REPLICAOF NO ONE
# 5. 把其他从节点指向新主
redis-cli -p 6381 REPLICAOF 127.0.0.1 6380
# 6. 把旧主降级为新主的从节点
redis-cli -p 6379 REPLICAOF 127.0.0.1 6380
# 7. 切换客户端配置/VIP/DNS 指向新主
# 8. 持久化配置,避免重启回退
redis-cli -p 6380 CONFIG REWRITE
redis-cli -p 6381 CONFIG REWRITE
redis-cli -p 6379 CONFIG REWRITE
7.2 用 FAILOVER 命令(6.2+)
6.2 引入了 FAILOVER 命令,实现协调的、零数据丢失的主动故障转移:
FAILOVER [TO host port [FORCE]] [ABORT] [TIMEOUT milliseconds]
# 让主节点主动把角色让给指定的从节点
redis-cli -p 6379 FAILOVER TO 127.0.0.1 6380
# 让主节点自己选一个从节点
redis-cli -p 6379 FAILOVER
# 中止正在进行的 failover
redis-cli -p 6379 FAILOVER ABORT
FAILOVER 的执行流程(这是它优于手动操作的地方):
1. 主节点进入 FAILOVER_WAIT_FOR_SYNC 状态,【暂停所有客户端写入】
2. 等待目标从节点的 offset 完全追上自己(保证零数据丢失)
3. 向目标从节点发送 PSYNC FAILOVER 请求
4. 目标从节点提升为主节点
5. 原主节点自动变成新主节点的从节点
6. 解除客户端暂停
用 INFO replication 的 master_failover_state 字段可以观察状态:no-failover / waiting-for-sync / failover-in-progress。
这是 6.2+ 环境下手动切换的首选方式,比一堆 REPLICAOF 命令安全得多(它保证了零数据丢失)。
8. 高频面试题
Q1:Redis 主从复制的完整流程是什么?
分三个阶段:
阶段一:建立连接与握手
从节点执行 REPLICAOF → 存下主节点地址并【立即返回 OK】(异步)
定时任务 replicationCron 发现要复制 → 建立 socket
握手:
从→主 PING,主→从 +PONG (检测网络与主节点可用性)
从→主 AUTH <password> (如配了 masterauth)
从→主 REPLCONF listening-port <端口>
从→主 REPLCONF capa eof capa psync2 (声明支持无盘复制和 PSYNC2)
从→主 PSYNC <replid> <offset>
阶段二:数据同步。主节点回复三种之一:
+FULLRESYNC <replid> <offset>→ 全量同步;+CONTINUE [<new replid>]→ 部分重同步;-ERR→ 主节点太老(< 2.8),降级用 SYNC。
阶段三:命令持续传播。主节点把写命令异步发给所有从节点;同时主→从每 10 秒 PING,从→主每秒 REPLCONF ACK <offset>。
Q2:全量同步的过程是怎样的?开销在哪?
1. 主节点回复 +FULLRESYNC <replid> <offset>
2. 主节点执行 BGSAVE(fork 子进程生成 RDB)
同时把此后的新写命令写入【该从节点的复制缓冲区】
3. RDB 生成完 → 发送给从节点
4. 从节点:【清空自己所有现有数据】→ 加载 RDB(此期间阻塞,无法响应请求)
5. 主节点把复制缓冲区里积累的命令发过去,从节点执行追上进度
6. 进入命令持续传播
五处开销:
- 主节点 fork(每 GB 约 20~100ms 的阻塞);
- 主节点生成 RDB 的 CPU 和磁盘 IO;
- 网络传输全量数据(10GB 在千兆网要 80+ 秒且占满带宽);
- 从节点加载 RDB 期间完全阻塞;
- 主节点为该从节点维护的复制缓冲区不断增长(可能几百 MB)。
最危险的一点:从节点会清空自己的全部数据。 如果误让一个有数据的节点去复制空节点,数据会被全部清空。
Q3:什么是部分重同步?它依赖哪三个东西?
部分重同步(Partial Resynchronization,2.8+):从节点短暂断线重连后,主节点只补发缺失的那段命令,不做全量同步。
依赖三个机制:
- 复制偏移量 offset:主节点的
master_repl_offset(累计传播的字节数)和从节点的slave_repl_offset(累计接收的字节数),差值就是复制延迟; - 复制积压缓冲区 repl_backlog:主节点维护的一个固定大小的环形缓冲区(
repl-backlog-size,默认 1MB),保存最近传播的命令流。全局只有一个(所有从节点共用),第一个从节点连上时创建; - 复制 ID replid:40 字符的随机串,标识一段"复制历史”。
判断能否部分重同步的两个条件:
条件一:从节点发来的 replid == 主节点的 replid(或 replid2)
条件二:repl_backlog_first_byte_offset <= offset <= master_repl_offset
(从节点缺失的数据还没被环形缓冲区覆盖)
两个都满足 → +CONTINUE;否则 → +FULLRESYNC
Q4:repl-backlog-size 应该设多大?怎么判断是否够用?
默认 1MB 在生产环境几乎肯定不够。
计算公式:
repl-backlog-size >= 平均写入速率(bytes/s) × 最长可能断线时间(s) × 安全系数
例如写入 5MB/s、网络抖动可能持续 30 秒、安全系数 2 → 300MB。
实践建议:至少 64MB,高写入量的实例设 128MB ~ 512MB。注意这块内存是固定占用的(只要有从节点连着),要算进 maxmemory 的余量。
怎么判断不够用:
redis-cli INFO stats | grep -E 'sync_full|sync_partial'
# sync_full 持续增长 → 频繁全量同步
# sync_partial_err 增长 → 部分重同步失败(backlog 覆盖了需要的数据)
这两个指标增长就说明 backlog 太小,或者网络抖动太频繁。
Q5:PSYNC2 解决了什么问题?replid2 是干什么的?
2.8 版 PSYNC 的两个问题:
- 从节点重启后必须全量同步:内存里的 replid 和 offset 丢了;
- 主从切换后所有从节点都要全量同步:新主节点(原从节点)的 replid 是它自己新生成的,而其他从节点记录的是旧主的 replid,不匹配。这在故障转移时是灾难——本来只是切主,结果全部从节点全量同步,网络和 CPU 被打满。
PSYNC2(4.0+)的两个改进:
改进一:从节点把 replid 和 offset 写入 RDB 的 AUX 字段(repl-id、repl-offset),重启后加载,可以尝试部分重同步。限制:只在优雅重启(SHUTDOWN)时有效,因为只有那时生成的 RDB 才含最新 offset。kill -9 后 RDB 里的 offset 是旧的,Redis 会拒绝用它。
改进二:引入 replid2 和 second_repl_offset
每个节点维护两个 replication ID:master_replid(当前)和 master_replid2(继承的上一个)。
故障转移时:
1. 从节点 B 被提升为主节点:
├─ 把原来的 master_replid(旧主 A 的 ID)保存到 replid2
├─ 把当前 offset 保存到 second_repl_offset
└─ 生成一个【新的】 master_replid
2. 其他从节点 C 来连 B,发 PSYNC <A的replid> <offset>
3. B 检查:不等于我的 replid,但【等于我的 replid2】,且 offset <= second_repl_offset
→ 可以部分重同步!回复 +CONTINUE <B的新replid>,同时让 C 更新 replid
这让故障转移不再触发全量同步,是 4.0 一个非常重要的可用性改进。
为什么 B 要生成新 replid 而不沿用 A 的:因为 B 成为主节点后数据历史已经和 A 分叉了(A 可能有未同步给 B 的写入),新 replid 标识"这是一段新的复制历史”。
Q6:主从复制是同步还是异步的?会丢数据吗?
异步的。 主节点执行完命令立即返回客户端成功,然后才异步地把命令写入各从节点的复制缓冲区并发送。
客户端 --SET k v--> 主节点
├─ 执行命令
├─ 【立即返回 +OK】 ← 此时数据还没到从节点
└─ 异步发给从节点
所以一定会丢数据的两个场景:
- 主节点宕机时未同步的写入:那批命令在新主节点上不存在,永久丢失。即使
appendfsync always也无法避免(这是分布式层面的丢失,不是持久化问题); - 脑裂:网络分区时旧主仍在接受写入,恢复后被降级并清空数据,这期间的写入全丢。
缓解手段:
WAIT numreplicas timeout:阻塞等待 N 个从节点确认收到。但它只保证从节点收到并写入内存,不保证 fsync,且每次写多一个 RTT;WAITAOF numlocal numreplicas timeout(7.0+):保证更强,能等待 AOF 落盘;min-replicas-to-write+min-replicas-max-lag:从节点不够健康时主节点拒绝写入,缩小脑裂丢数据的窗口。
结论:Redis 不适合做"一条都不能丢"的主存储。
Q7:min-replicas-to-write 是干什么的?
min-replicas-to-write 1 # 至少 1 个健康从节点
min-replicas-max-lag 10 # "健康"的定义:lag <= 10 秒
不满足条件时主节点拒绝所有写请求,返回 NOREPLICAS Not enough good replicas to write.
核心作用是缓解脑裂的数据丢失:
1. 主节点 M 与所有从节点、哨兵网络断开(但 M 还活着,部分客户端能连到它)
2. 哨兵把从节点 S 提升为新主 → 【此时有两个主节点】
3. 部分客户端还在往 M 写数据
4. 网络恢复,M 发现有了新主,被降级为从节点
5. M 作为从节点必须【清空数据】从 S 全量同步
→ 步骤 3 写入 M 的数据永久丢失
设了 min-replicas-to-write 1 后,步骤 3 中 M 发现自己没有任何健康从节点,直接拒绝写入,避免了这些数据的丢失。
代价:牺牲可用性换一致性(从节点全挂时主节点也不可写)。且它不能完全防止脑裂——在从节点被判定为不健康之前(即 min-replicas-max-lag 的窗口内),写入仍会成功。
Q8:读写分离有什么问题?该不该用?
四个问题:
- 主从延迟导致读到旧数据(根本问题,无法完全解决,因为复制是异步的)。典型现象:“刚改了昵称,刷新还是旧的”;
- 从节点的过期 key 问题:从节点不主动删除过期 key。3.2+ 读取时会做逻辑过期判断返回 nil(正确),但 3.2 之前有 bug 会返回过期数据;
- 从节点故障时返回脏数据:
replica-serve-stale-data yes(默认)时,与主节点失联的从节点仍会返回可能非常旧的数据,客户端察觉不到; - 从节点越多,主节点复制负担越重(每个从节点一个复制缓冲区 + 独立发送)。
该不该用——很多团队高估了它的价值:
- Redis 单实例读性能已达 10 万 QPS 级别,大部分业务远达不到瓶颈;
- 复杂度显著上升(延迟处理、从节点列表管理、故障摘除、区分哪些读能走从库);
- “偶尔读到旧数据"的问题极难复现定位。
更好的替代方案:用 Cluster 分片(读写都分散,且无一致性问题);热点读用本地缓存 + Redis 6.0 客户端缓存。
真正适合读写分离的场景:读 QPS 极高(100:1 以上)且已确认主节点是瓶颈;明确接受最终一致性;在从节点跑重查询/数据分析/全量导出(这是最有价值的用途)。
Q9:主节点没开持久化,重启后会发生什么?
这是最危险的事故,会导致全集群数据永久丢失:
1. 主节点 M 为了性能关闭了持久化(save "" + appendonly no)
2. M 重启(OOM Killer、运维误操作、机器重启、systemd Restart=always 自动重启)
3. M 启动后是【空的】,但它仍然是主节点
4. 从节点 S 检测到与 M 连接恢复,发起 PSYNC
5. replid 不匹配(M 重启后重新生成了)→ 全量同步
6. S 【清空自己的所有数据】,从 M 加载一个空的 RDB
→ 【全集群数据永久丢失】
四个防范措施:
- 主节点至少保留低频 RDB(如
save 3600 1)作为兜底,不要完全关闭持久化; - 关闭主节点的自动重启:systemd 的
Restart=always改成no或on-failure加重启次数限制,避免它悄悄重启; - 主节点重启后绝不能自动以主节点身份恢复服务——正确流程是先让它作为从节点从有数据的节点同步,确认数据完整后再切换;
- 用哨兵管理拓扑,哨兵会把重启的旧主设为从节点。
这也是 Redis 官方明确不推荐"主节点完全关闭持久化"的原因。
Q10:什么是复制风暴?怎么解决?
现象:主节点反复 fork、CPU 和网络被打满、从节点反复重连、集群不可用。
成因链条:
1. 从节点因网络抖动断开
2. 重连时 offset 已不在 backlog 范围(backlog 太小)→ 全量同步
3. 主节点 fork + 生成 RDB + 传输(占大量 CPU、内存、带宽)
4. 传输期间该从节点的复制缓冲区不断增长
5. 缓冲区超过 client-output-buffer-limit replica 限制 → 【主节点断开它】
6. 从节点重连 → 又全量同步 → 【无限循环】
多个从节点同时陷入这个循环时,主节点会被彻底压垮。
解决:
repl-backlog-size 256mb # 【最关键】避免断线就全量
client-output-buffer-limit replica 1gb 256mb 120 # 调大从节点输出缓冲区
repl-timeout 300 # 避免大 RDB 传输被误判超时
repl-diskless-sync yes # 无盘复制减少磁盘瓶颈
repl-diskless-sync-delay 5 # 多个从节点合并为一次 fork
根本解法:控制单实例内存 <= 10GB,用 Cluster 分片。
监控:sync_full(5 分钟内增长 > 1 次就要排查)、sync_partial_err。
Q11:无盘复制是什么?repl-diskless-load 的三个选项有什么区别?
无盘复制(repl-diskless-sync yes,6.0+ 默认开启):主节点直接把 RDB 通过 socket 流式发给从节点,不先在本地磁盘生成文件。适合磁盘慢或磁盘紧张(容器)的环境。
配套的 repl-diskless-sync-delay 5:如果同时有多个从节点要全量同步,等几秒让它们都到齐,然后只 fork 一次、一份 RDB 流同时发给所有从节点,大幅节省主节点开销。代价是先到的从节点多等几秒。
从节点侧的 repl-diskless-load 三个选项:
| 值 | 说明 |
|---|---|
disabled(默认) |
先把 RDB 存磁盘临时文件再加载。最安全——传输中断时旧数据还在 |
on-empty-db |
只在自己数据库为空时才直接从 socket 加载(安全的折中) |
swapdb |
总是直接从 socket 加载,当前数据库暂存在内存里(内存翻倍,但传输失败可回滚) |
建议:主节点开 repl-diskless-sync yes;从节点保持 disabled 或用 on-empty-db。
Q12:从节点在全量同步期间能响应读请求吗?
由 replica-serve-stale-data 决定:
yes(默认):仍然响应读请求,但返回旧数据。首次同步时数据集为空,会返回大量 nil(业务可能误判为"数据不存在”);no:除了INFO、REPLICAOF、AUTH、PING、SHUTDOWN、SUBSCRIBE等少数命令,其他都返回错误:
-MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'.
这个配置也适用于"从节点与主节点失联"的情况(不只是同步中)。
选择:对一致性敏感的业务设 no(宁可报错也不返回脏数据,让客户端故障转移);可用性优先的缓存场景保持 yes。
另外注意从节点加载 RDB 的过程本身是阻塞的——那期间连 PING 都不响应,这跟这个配置无关。
Q13:主从之间的心跳机制是怎样的?
双向心跳:
主 → 从:每 repl-ping-replica-period(默认 10)秒发一次 PING
作用:让从节点确认主节点还活着。从节点超过 repl-timeout(默认 60 秒)没收到任何数据就认为主节点挂了,断开重连。
从 → 主:每秒发一次 REPLCONF ACK <offset>
三个作用:
- 上报复制进度(
INFO replication里slaveN的offset和lag就来自这里); - 实现
WAIT命令(主节点靠这些 ACK 判断有多少从节点收到了指定 offset); - 检测从节点存活(主节点超过
repl-timeout没收到某从节点的 ACK 就断开它)。
一个重要的坑:全量同步时如果 RDB 很大、传输时间超过 repl-timeout,同步会被误判为超时而中断,然后重试,陷入死循环。所以大实例必须把 repl-timeout 设得足够大(如 300 秒)。
Q14:FAILOVER 命令(6.2+)和手动 REPLICAOF 切换有什么区别?
手动切换(一堆 REPLICAOF 命令)的问题:无法保证零数据丢失。在你执行 REPLICAOF NO ONE 的那一刻,从节点可能还没完全追上主节点,那部分数据就丢了。你只能靠人工比对 offset,中间还可能有新写入。
FAILOVER 是协调的、保证零数据丢失的主动故障转移:
FAILOVER [TO host port [FORCE]] [ABORT] [TIMEOUT ms]
执行流程:
1. 主节点进入 FAILOVER_WAIT_FOR_SYNC 状态,【暂停所有客户端写入】
2. 等待目标从节点的 offset 完全追上自己(这一步保证了零数据丢失)
3. 向目标从节点发 PSYNC FAILOVER 请求
4. 目标从节点提升为主节点
5. 原主节点【自动】变成新主的从节点
6. 解除客户端暂停
用 INFO replication 的 master_failover_state 观察状态(no-failover / waiting-for-sync / failover-in-progress),FAILOVER ABORT 可以中止。
6.2+ 环境下手动切换的首选方式。
Q15:为什么主节点也要配 masterauth?
因为主节点可能被降级为从节点:
- 故障转移后,旧主节点恢复时会被哨兵设为新主的从节点;
- 手动切换时也会把旧主降级。
此时它需要用 masterauth 去认证连接新主节点。如果没配,会一直报 NOAUTH Authentication required,master_link_status 永远是 down。
这是主从/哨兵部署中最常见的配置疏漏之一。正确做法:所有节点的配置文件都同时配 requirepass 和 masterauth,且值相同(用同一份配置模板,只改端口)。
同理,6.0+ 用 ACL 时要配 masteruser。
Q16:容器/K8s 环境下部署主从要注意什么?
核心问题:主节点通过 socket 得到的从节点 IP 可能是容器内网 IP 或 NAT 网关 IP,导致:
INFO replication里显示的从节点地址是错的;- 哨兵无法正确发现和连接从节点(哨兵是通过主节点的
INFO输出来发现从节点的),故障转移失败。
解决:在从节点配置里显式声明自己的可访问地址:
replica-announce-ip <宿主机IP或Service地址>
replica-announce-port <映射后的端口>
哨兵侧对应的是 sentinel announce-ip / sentinel announce-port。
其他容器化注意点:
- 用 StatefulSet 而不是 Deployment(需要稳定的网络标识和存储);
- 不要用 hostname 而要用稳定的 IP/Service(哨兵存的是 IP,Pod 重建后 IP 变了会有问题);
- 数据卷必须是持久化的(emptyDir 会随 Pod 销毁);
- 内存 limit 要留出 fork COW 的余量,且
maxmemory要小于 limit(否则容器被 OOM Kill); - K8s 环境更推荐用 Redis Cluster 或 Operator(如 Redis Enterprise Operator、KubeDB),哨兵在 K8s 里的网络模型下坑很多。
小结
- 主从复制提供数据冗余、读扩展、故障恢复的基础、持久化压力转移,但本身不提供自动故障转移(那是哨兵/Cluster 的职责)。
- 建立方式三种:配置文件
replicaof、启动参数、运行时REPLICAOF(重启失效,要CONFIG REWRITE)。主节点也必须配masterauth(它可能被降级为从节点)。 - 复制三阶段:握手(PING/AUTH/REPLCONF/PSYNC)→ 同步(全量或部分)→ 命令持续传播(异步 + 双向心跳)。
- 全量同步的五处开销:fork 阻塞、生成 RDB 的 CPU/IO、网络传输全量数据、从节点加载时阻塞、复制缓冲区增长。从节点会清空自己所有数据——这是最危险的一点。
- 部分重同步依赖三件事:
offset(复制偏移量)、repl_backlog(固定大小的全局环形缓冲区)、replid。条件是 replid 匹配 + offset 还在 backlog 范围内。 repl-backlog-size默认 1MB 远远不够,按"写入速率 × 最长断线时间 × 安全系数"算,至少 64MB,高写入设 128~512MB。用sync_full和sync_partial_err判断是否够用。- PSYNC2(4.0+) 两个改进:从节点把 replid/offset 写入 RDB AUX 字段(仅优雅重启有效);引入
replid2+second_repl_offset,让故障转移不再触发全量同步。 - 复制是异步的:主节点执行完立即返回客户端,所以主节点宕机必然丢失未同步的写入,
appendfsync always也救不了。缓解靠WAIT/WAITAOF和min-replicas-to-write。 min-replicas-to-write+min-replicas-max-lag缓解脑裂丢数据:从节点不够健康时主节点拒绝写入。不能完全防止脑裂,只缩小窗口。- 读写分离的价值常被高估:延迟导致读旧数据(无法根治)、3.2 前从节点会返回过期数据、失联从节点返回脏数据、从节点越多主节点负担越重。优先考虑 Cluster 分片和本地缓存;读写分离最有价值的用途是在从节点跑重查询/数据分析。
- 两个最危险的事故:① 主节点无持久化重启后变空实例 → 从节点全量同步清空数据 → 全集群数据丢失(防范:保留低频 RDB + 关闭自动重启 + 重启后不自动做主);② 复制风暴(backlog 太小 → 全量同步 → 缓冲区超限被断开 → 重连 → 无限循环)。
- 无盘复制:主节点
repl-diskless-sync yes+repl-diskless-sync-delay(多从节点合并一次 fork);从节点repl-diskless-load保持disabled或on-empty-db。 repl-timeout大实例必须调大(如 300 秒),否则大 RDB 传输会被误判超时导致同步死循环。- 手动切换在 6.2+ 用
FAILOVER命令(暂停写 → 等 offset 追平 → 切换 → 旧主自动变从),保证零数据丢失,远优于手工REPLICAOF。 - 容器/NAT 环境必配
replica-announce-ip/replica-announce-port,否则哨兵发现不了从节点。 - 核心监控:从节点
master_link_status(必须 up)、master_last_io_seconds_ago;主节点connected_slaves、slaveN的lag、sync_full、sync_partial_err、offset 差值。
xingliuhua