Redis 主从复制原理浅解

63 阅读10分钟

在高并发业务场景中,单个 Redis 实例往往难以承载海量的读写请求 —— 写操作集中在单节点会导致性能瓶颈,读请求激增则可能直接压垮实例。为解决这一问题,Redis 提供了主从复制(Master-Slave Replication) 机制,通过部署多副本节点实现 “读写分离”:主节点(Master)负责处理写请求,从节点(Slave)分担读请求,同时从节点实时同步主节点数据,既提升了整体吞吐量,也为故障恢复提供了基础。

本文将从复制核心流程、关键机制原理,到实际运维中的问题与优化方案,全面解析 Redis 主从复制,为高可用 Redis 集群部署提供参考。

一、Redis 主从复制核心流程

当 Slave 执行slaveof命令后,复制流程分为全量同步部分同步两种场景,具体触发哪种同步,由 Slave 与 Master 的历史同步状态决定。

1.1 核心参数:runid 与 offset

在解析流程前,需先理解两个关键标识,它们是复制决策的核心依据:

  • runid:每个 Redis 实例启动时会生成一个 40 位的十六进制唯一标识(如5f4dcc3b5aa765d61d8327deb882cf99),用于区分不同的 Master 实例。Slave 首次同步时会记录 Master 的 runid,后续重连时需验证 runid 是否一致(若 Master 重启,runid 会变化,需重新全量同步)。
  • offset:主从节点均维护一个 “复制偏移量”,记录当前同步到的命令位置。Master 每执行一个写命令,offset 会递增;Slave 同步命令后,offset 也会同步更新。通过对比 Master 与 Slave 的 offset,可快速判断数据是否一致。

1.2 全量同步(首次同步或长期断连后)

当 Slave 是首次与 Master 同步,或断连时间过长导致增量数据丢失时,会触发全量同步,流程分为 5 个步骤:

步骤 1:Slave 发送同步请求

Slave 与 Master 建立 TCP 连接后,发送psync ? -1命令:

  • ?:表示 Slave 未知 Master 的 runid;
  • -1:表示 Slave 无历史同步偏移量,需全量数据。

步骤 2:Master 响应全量同步指令

Master 收到psync ? -1后,执行以下操作:

  1. 生成并记录当前的 runid;
  1. 回复+fullsync [runid] [master_offset],告知 Slave “开启全量同步”,并传递自身的 runid 和当前 offset;
  1. Slave 接收后,保存 Master 的 runid 和 offset,用于后续重连验证。

步骤 3:Master 生成 RDB 快照

Master 在后台通过fork()创建一个子进程,由子进程扫描全量数据并生成 RDB 快照文件(若 Master 已开启 RDB 持久化,会优先复用最近的 RDB 文件,减少资源消耗)。

需要注意,fork()操作会阻塞 Master 主线程, Master 需拷贝内存页表(仅拷贝表结构,不拷贝实际数据),若 Master 内存较大(如 10GB+),页表拷贝可能耗时几十到几百毫秒,期间 Master 无法处理请求,需合理控制实例内存大小(建议单实例内存不超过 4GB)。

步骤 4:Master 发送 RDB 文件与增量命令

  1. Master 将生成的 RDB 文件通过网络发送给 Slave;
  1. 在发送 RDB 的过程中,Master 若收到新的写命令,会将这些命令暂存到复制缓冲区(repl_baklog) 中(避免 RDB 同步期间的数据丢失)。

步骤 5:Slave 加载 RDB 并同步增量命令

  1. Slave 接收完 RDB 文件后,先清空本地数据库(避免旧数据干扰);
  1. Slave 加载 RDB 文件到内存,此时数据与 Master 生成 RDB 时的状态一致;
  1. Slave 加载 RDB 完成后,Master 将复制缓冲区中的增量命令发送给 Slave,Slave 执行这些命令,最终与 Master 数据完全一致。

1.3 部分同步(短期断连后重连)

Redis 2.8 版本后引入部分同步机制,解决了 “断连后必须全量同步” 的性能问题。当 Slave 与 Master短期断连(如网络抖动) 后重连,仅需同步断连期间的增量数据,流程如下:

步骤 1:Slave 发送带标识的同步请求

Slave 重连时,发送psync [master_runid] [slave_offset]命令:

  • master_runid:Slave 上次记录的 Master 的 runid;
  • slave_offset:Slave 断连前最后的同步偏移量。

步骤 2:Master 验证并决定同步方式

Master 收到请求后,进行两步验证:

  1. runid 验证:若 Slave 发送的 runid 与自身当前 runid 不一致(如 Master 重启过),则触发全量同步;
  1. offset 验证:若 runid 一致,检查 Slave 的slave_offset是否在复制缓冲区(repl_baklog)的有效范围内(即断连期间的增量命令未被缓冲区覆盖)。

步骤 3:部分同步执行

  1. 若slave_offset有效,Master 回复+continue,表示开启部分同步;
  1. Master 从复制缓冲区中读取slave_offset之后的所有命令,发送给 Slave;
  1. Slave 执行这些增量命令,快速恢复与 Master 的数据一致性。

复制缓冲区(repl_baklog)是 Master 维护的一个固定大小的环形队列(默认 1MB,由repl-backlog-size配置),用于暂存最近的写命令。无论有多少个 Slave,Master 仅维护一份 repl_baklog,所有 Slave 共享增量数据,减少内存消耗。

1.4 命令传播:实时同步的核心机制

全量或部分同步完成后,主从进入 “实时同步” 阶段,Master 通过命令传播机制将新写命令同步到 Slave:

  1. Master 每执行一个写命令,会先更新自身的 offset,再将命令写入客户端输出缓冲区(client output buffer)
  1. Redis 的网络线程会将缓冲区中的命令通过 TCP 连接发送给 Slave;
  1. Slave 接收命令后,先执行命令,再更新自身的 offset,完成一次同步。

Master 执行完写命令后,无需等待 Slave 执行完成即可返回给客户端,因此可能存在 “Master 写入后,Slave 短暂查询不到” 的延迟(通常毫秒级),业务需根据一致性要求设计重试或路由策略。

二、主从复制的关键支撑机制

为保证复制链路的稳定性,Redis 设计了心跳检测、缓冲区限制等机制,解决 “断连检测”“数据延迟” 等问题。

2.1 心跳机制:维护复制链路健康

主从节点通过双向心跳检测,实时感知对方状态,流程如下:

1. Master 向 Slave 发送心跳(ping)

  • 频率:由repl-ping-slave-period配置,默认 10 秒;
  • 作用:Slave 若超过repl-timeout时间(默认 60 秒)未收到 ping,会认为 Master 故障,主动断开连接并尝试重连。

2. Slave 向 Master 发送心跳(replconf ack)

  • 频率:固定每 1 秒发送一次;
  • 内容:replconf ack [slave_offset],携带 Slave 当前的同步偏移量;
  • 作用:
    • Master 若超过repl-timeout未收到 ack,会断开与 Slave 的连接;
    • Master 通过对比自身 offset 与 Slave 的slave_offset,可实时感知 Slave 的数据延迟,若延迟过大且开启了min-slaves-to-write配置,会禁止 Master 写入(保障数据安全性)。

2.2 客户端输出缓冲区限制:避免内存溢出

Master 会为每个 Slave 维护一个客户端输出缓冲区(client output buffer) ,用于暂存待发送给 Slave 的命令。若 Slave 处理命令过慢(如磁盘 IO 高)或网络延迟大,缓冲区可能持续增长,导致 Master 内存溢出。

  • 硬限制(256MB) :若缓冲区大小直接达到 256MB,Master 会强制断开与 Slave 的连接;
  • 软限制(64MB + 60 秒) :若缓冲区大小达到 64MB 且持续 60 秒,Master 也会断开连接。

需根据业务写入量调整该配置,例如高写入场景(如每秒 10 万 + 写请求)可将硬限制调至 512MB,避免频繁断连。

三、主从复制常见问题与优化方案

实际运维中,主从复制可能遇到 “断连后全量同步”“Master 阻塞”“同步缓慢” 等问题,需针对性优化。

3.1 断连后重连触发全量同步,而非部分同步

原因分析

部分同步的前提是 “Slave 的 offset 在 Master 的复制缓冲区(repl_baklog)有效范围内”。若 repl_baklog 配置过小,断连期间 Master 的写命令过多,会覆盖缓冲区中的旧命令,导致 Slave 的 offset 失效,触发全量同步。

优化方案

  1. 调大复制缓冲区大小:根据业务写入量估算合理大小,公式参考:

repl-backlog-size = 平均每秒写入命令数 × 最大断连时间(秒)

例如:每秒写入 1000 条命令,最大断连时间 60 秒,建议配置repl-backlog-size 60mb(预留一定冗余);

  1. 减少断连频率:优化主从节点网络(如部署在同一机房),避免网络抖动或带宽瓶颈。

3.2 Master 写入量过大,导致主从频繁断连

原因分析

高写入场景下,若 Slave 处理命令速度慢(如开启 AOF 实时刷盘appendfsync always,磁盘 IO 瓶颈)或网络延迟高,Master 的客户端输出缓冲区会持续增长,触发client-output-buffer-limit限制,导致 Master 强制断开 Slave 连接,形成 “断连→重连→再断连” 的循环。

优化方案

  1. 调大客户端输出缓冲区限制:针对高写入场景,适当提高client-output-buffer-limit slave的阈值,例如:

client-output-buffer-limit slave 512mb 128mb 60;

  1. 优化 Slave 处理性能
    • 关闭 Slave 的 AOF 持久化(或设置appendfsync everysec,平衡一致性与性能);
    • 若 Slave 仅用于读服务,可关闭 RDB 持久化(save ""),减少磁盘 IO 消耗;
  1. 分散写入压力:若单 Master 写入量过高(如每秒 5 万 +),可考虑拆分实例(如按业务模块分库),避免单点过载。

3.3 添加 Slave 时,Master 发生短暂阻塞

原因分析

Slave 首次同步时,Master 需通过fork()创建子进程生成 RDB 快照。若 Master 内存较大(如 8GB+),fork()拷贝内存页表的时间会变长(可能达数百毫秒),期间 Master 主线程无法处理请求,导致业务超时。

优化方案

  1. 控制 Master 实例内存大小:建议单 Redis 实例内存不超过 4GB,减少fork()耗时;
  1. 选择低峰期添加 Slave:避免在业务高峰期(如秒杀、促销)添加 Slave,减少对线上服务的影响;
  1. 复用 RDB 文件:若 Master 已开启 RDB 持久化,且最近生成过 RDB 文件,可手动将 RDB 文件拷贝到 Slave,Slave 加载 RDB 后再执行slaveof,跳过 Master 生成 RDB 的步骤(需确保 RDB 文件未损坏)。

3.4 全量同步时数据传输缓慢

原因分析

全量同步耗时通常集中在三个阶段:

  1. Master 生成 RDB 缓慢(CPU 不足或内存过大);
  1. RDB 文件网络传输缓慢(带宽瓶颈);
  1. Slave 加载 RDB 缓慢(内存不足或开启 swap)。

优化方案

  1. 优化 Master RDB 生成速度
    • 确保 Master 有足够的 CPU 资源(避免 CPU 使用率长期超过 80%);
    • 关闭 Master 非必要的功能(如 Redis 集群模式下的槽位迁移、自定义脚本);
  1. 提升网络带宽:主从节点尽量部署在同一机房,使用万兆网卡,避免跨地域同步;
  1. 优化 Slave RDB 加载速度
    • 禁用 Slave 的 swap(通过sysctl -w vm.swappiness=0),避免内存数据交换到磁盘;
    • 确保 Slave 内存充足(至少比 RDB 文件大小多 20%,预留加载时的内存膨胀空间)。