目录

Redis-10 主从复制原理

1. 为什么需要主从复制

单机 Redis 有三个无法回避的问题:

  1. 单点故障:机器宕机、进程崩溃就完全不可用。即使有持久化,重启 + 加载 10GB 数据也要几分钟;
  2. 读性能上限:单实例受单线程和网卡带宽限制;
  3. 数据丢失风险:磁盘损坏就彻底没了。

主从复制是解决这些问题的基础——注意是"基础"而不是"方案",因为主从复制本身不提供自动故障转移(那是哨兵和 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_slavesslaveNstate=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:除了少数命令(INFOREPLICAOFAUTHPINGSHUTDOWNSUBSCRIBE 等),其他所有命令都返回错误:
-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(秒)

关键特性

  1. 全局只有一个(不是每个从节点一个),所有从节点共用;
  2. 是环形的:写满后覆盖最旧的数据;
  3. 只在有从节点时才创建(第一个从节点连上时创建),所有从节点断开且超过 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 的两个改进

改进一:从节点持久化复制信息

从节点会把 replidoffset 写入 RDB 文件的 AUX 字段repl-idrepl-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-dbswapdb 只在内存充足且想避免磁盘 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>

作用有三个:

  1. 上报自己的复制进度,让主节点知道每个从节点落后多少(INFO replicationslaveNoffsetlag 就来自这里);
  2. 实现 WAIT 命令:主节点靠这些 ACK 判断有多少从节点已经收到了指定 offset;
  3. 检测从节点是否存活:主节点如果超过 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,从节点可以被写入。但这非常危险:

  1. 写入的数据不会传播到任何地方(从节点不会把自己的写入传给主节点或其他从节点);
  2. 主从数据不一致,而且这种不一致很难发现;
  3. 下次全量同步时,从节点上写入的数据会被全部清空
  4. 唯一"合理"的用途是在从节点上创建一些临时的、本地的键(比如做数据分析时的中间结果),但这种需求应该用独立实例。

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 读写分离到底该不该用

这是一个需要仔细权衡的决定,很多团队高估了它的价值。

读写分离的收益有限的原因

  1. Redis 的单实例读性能已经很高(10 万 QPS 级别),大部分业务的读 QPS 远达不到瓶颈;
  2. 复杂度显著上升:要处理延迟、要管理从节点列表、要处理故障摘除、要区分哪些读能走从库;
  3. 一致性问题的排查成本高:“偶尔读到旧数据"这种问题极难复现和定位;
  4. 从节点越多,主节点的复制负担越大(每个从节点一个复制缓冲区 + 独立发送)。

更好的替代方案

目标 更好的方案
提高读性能 用 Cluster 分片(读写都分散,且没有一致性问题)
挡住热点 key 的读 本地缓存(进程内 caffeine/bigcache)+ Redis 6.0 的客户端缓存
减少主节点压力 优化访问模式(pipeline、减少大 key、批量命令)

什么时候读写分离才真正合适

  1. 读 QPS 极高且远大于写(比如 100:1 以上),且已经确认主节点是瓶颈;
  2. 能明确接受最终一致性的场景(如商品浏览、内容展示);
  3. 需要在从节点跑重查询(数据分析、BITCOUNT 批量统计、全量扫描导出)——这是读写分离最有价值的用途;
  4. 已经有成熟的中间件/客户端支持(如 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
   → 【全集群数据永久丢失】

防范措施

  1. 主节点至少保留低频 RDB(如 save 3600 1)作为兜底,不要完全关闭持久化;
  2. 关闭主节点的自动重启:把 systemd 的 Restart=always 改成 Restart=noon-failure 加上重启次数限制,避免它悄悄重启;
  3. 主节点重启后,绝不能让它自动以主节点身份恢复服务。正确流程是:先让它作为从节点从有数据的节点同步,确认数据完整后再考虑切换;
  4. 用哨兵管理拓扑:哨兵会记录拓扑关系,主节点重启后哨兵会把它设为从节点(但仍然要小心,见 Q9)。

排查清单:

# 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)

可能的不一致来源

  1. 正常的复制延迟(最常见,不是真正的不一致);
  2. 从节点被写入过replica-read-only no);
  3. 过期 key 未同步删除(表现为 DBSIZE 不同,但读取都返回 nil,实际是一致的);
  4. 网络分区期间的脑裂写入
  5. 主从版本差异导致的行为差异(比如某个命令在不同版本的语义不同);
  6. 在从节点上执行了 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 replicationmaster_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. 进入命令持续传播

五处开销

  1. 主节点 fork(每 GB 约 20~100ms 的阻塞);
  2. 主节点生成 RDB 的 CPU 和磁盘 IO
  3. 网络传输全量数据(10GB 在千兆网要 80+ 秒且占满带宽);
  4. 从节点加载 RDB 期间完全阻塞
  5. 主节点为该从节点维护的复制缓冲区不断增长(可能几百 MB)。

最危险的一点:从节点会清空自己的全部数据。 如果误让一个有数据的节点去复制空节点,数据会被全部清空。

Q3:什么是部分重同步?它依赖哪三个东西?

部分重同步(Partial Resynchronization,2.8+):从节点短暂断线重连后,主节点只补发缺失的那段命令,不做全量同步。

依赖三个机制:

  1. 复制偏移量 offset:主节点的 master_repl_offset(累计传播的字节数)和从节点的 slave_repl_offset(累计接收的字节数),差值就是复制延迟;
  2. 复制积压缓冲区 repl_backlog:主节点维护的一个固定大小的环形缓冲区repl-backlog-size,默认 1MB),保存最近传播的命令流。全局只有一个(所有从节点共用),第一个从节点连上时创建;
  3. 复制 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 的两个问题

  1. 从节点重启后必须全量同步:内存里的 replid 和 offset 丢了;
  2. 主从切换后所有从节点都要全量同步:新主节点(原从节点)的 replid 是它自己新生成的,而其他从节点记录的是旧主的 replid,不匹配。这在故障转移时是灾难——本来只是切主,结果全部从节点全量同步,网络和 CPU 被打满。

PSYNC2(4.0+)的两个改进

改进一:从节点把 replid 和 offset 写入 RDB 的 AUX 字段repl-idrepl-offset),重启后加载,可以尝试部分重同步。限制:只在优雅重启SHUTDOWN)时有效,因为只有那时生成的 RDB 才含最新 offset。kill -9 后 RDB 里的 offset 是旧的,Redis 会拒绝用它。

改进二:引入 replid2second_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】     ← 此时数据还没到从节点
                    └─ 异步发给从节点

所以一定会丢数据的两个场景

  1. 主节点宕机时未同步的写入:那批命令在新主节点上不存在,永久丢失。即使 appendfsync always 也无法避免(这是分布式层面的丢失,不是持久化问题);
  2. 脑裂:网络分区时旧主仍在接受写入,恢复后被降级并清空数据,这期间的写入全丢。

缓解手段

  • 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:读写分离有什么问题?该不该用?

四个问题

  1. 主从延迟导致读到旧数据(根本问题,无法完全解决,因为复制是异步的)。典型现象:“刚改了昵称,刷新还是旧的”;
  2. 从节点的过期 key 问题:从节点不主动删除过期 key。3.2+ 读取时会做逻辑过期判断返回 nil(正确),但 3.2 之前有 bug 会返回过期数据
  3. 从节点故障时返回脏数据replica-serve-stale-data yes(默认)时,与主节点失联的从节点仍会返回可能非常旧的数据,客户端察觉不到;
  4. 从节点越多,主节点复制负担越重(每个从节点一个复制缓冲区 + 独立发送)。

该不该用——很多团队高估了它的价值

  • 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
   → 【全集群数据永久丢失】

四个防范措施

  1. 主节点至少保留低频 RDB(如 save 3600 1)作为兜底,不要完全关闭持久化;
  2. 关闭主节点的自动重启:systemd 的 Restart=always 改成 noon-failure 加重启次数限制,避免它悄悄重启;
  3. 主节点重启后绝不能自动以主节点身份恢复服务——正确流程是先让它作为从节点从有数据的节点同步,确认数据完整后再切换;
  4. 用哨兵管理拓扑,哨兵会把重启的旧主设为从节点。

这也是 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:除了 INFOREPLICAOFAUTHPINGSHUTDOWNSUBSCRIBE 等少数命令,其他都返回错误:
-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>

三个作用:

  1. 上报复制进度INFO replicationslaveNoffsetlag 就来自这里);
  2. 实现 WAIT 命令(主节点靠这些 ACK 判断有多少从节点收到了指定 offset);
  3. 检测从节点存活(主节点超过 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 replicationmaster_failover_state 观察状态(no-failover / waiting-for-sync / failover-in-progress),FAILOVER ABORT 可以中止。

6.2+ 环境下手动切换的首选方式。

Q15:为什么主节点也要配 masterauth

因为主节点可能被降级为从节点

  1. 故障转移后,旧主节点恢复时会被哨兵设为新主的从节点;
  2. 手动切换时也会把旧主降级。

此时它需要用 masterauth 去认证连接新主节点。如果没配,会一直报 NOAUTH Authentication requiredmaster_link_status 永远是 down

这是主从/哨兵部署中最常见的配置疏漏之一。正确做法:所有节点的配置文件都同时配 requirepassmasterauth,且值相同(用同一份配置模板,只改端口)。

同理,6.0+ 用 ACL 时要配 masteruser

Q16:容器/K8s 环境下部署主从要注意什么?

核心问题:主节点通过 socket 得到的从节点 IP 可能是容器内网 IP 或 NAT 网关 IP,导致:

  1. INFO replication 里显示的从节点地址是错的;
  2. 哨兵无法正确发现和连接从节点(哨兵是通过主节点的 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_fullsync_partial_err 判断是否够用。
  • PSYNC2(4.0+) 两个改进:从节点把 replid/offset 写入 RDB AUX 字段(仅优雅重启有效);引入 replid2 + second_repl_offset,让故障转移不再触发全量同步
  • 复制是异步的:主节点执行完立即返回客户端,所以主节点宕机必然丢失未同步的写入,appendfsync always 也救不了。缓解靠 WAIT/WAITAOFmin-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 保持 disabledon-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_slavesslaveNlagsync_fullsync_partial_err、offset 差值。