2025-01-15🌱上海: ☀️ 🌡️+6°C 🌬️↓18km/h

# Redis 主从复制的实现原理是什么?

# 总结分析

Redis 的主从复制指一个主节点可将数据复制到一个或多个从节点,从节点与主节点保持数据同步,其流程如下:

  • 开始同步:从节点向主节点发送 PSYNC 命令发起同步请求。
  • 全量复制:首次连接或连接失效时,从节点请求全量复制,主节点发送当前数据快照(RDB 文件)。
  • 增量复制:全量复制完成后,主从保持长连接,主节点通过该连接将后续写操作传给从节点以保证数据一致。

# 扩展分析

# 主从架构

image.png

# 主从复制步骤

总的来说主从复制功能的详细步骤可以分为 7 个步骤:

  1. 设置主节点的地址和端口
  2. 建立套接字连接
  3. 发送 PING 命令
  4. 权限验证
  5. 同步
  6. 命令传播

image.png

接下来进行逐步分析:

# 1. 设置主服务器的地址和端口

  • 开启方式:主从复制由从节点发起,主节点无需操作。从节点开启主从复制有三种等效方式:

  • 配置文件:在从服务器配置文件中加入 slaveof masterip masterport

  • 启动命令redis-server 启动命令后加入 --slaveof masterip masterport

  • 客户端命令:Redis 服务器启动后,通过客户端执行 slaveof masterip masterport ,该实例即成为从节点。

  • 后续操作:完成配置后,从服务器会将主服务器的 IP 地址和端口号保存到服务器状态属性中,可使用 info Replication 命令分别查看从服务器和主服务器的主从信息。

# 2. 建立套接字连接

当在从服务器执行 slaveof 命令后,从服务器会依据所设置的 IP 和端口向主服务器建立 socket 连接。例如在 6380 从服务器执行 slave of 127.0.0.1 6379 ,即表示从服务器向主服务器发起 socket 连接。此时执行 info Replication 命令, 6380 服务器的角色已变为 slave 。这表明从服务器已成功开启主从复制流程中的连接建立步骤,为后续的数据同步等操作做好了准备。

# 3. 发送 Ping 命令

从节点成为主节点的客户端后,会发送 ping 命令进行首次请求,目的是检查 socket 连接可用性以及主节点能否处理请求。发送 ping 命令后可能出现以下 3 种情况:

  1. 返回 pong:表明 socket 连接正常,主节点可处理请求,复制过程继续。
  2. 超时:一定时间内未收到主节点回复,意味着 socket 连接不可用,从节点断开连接并重连。
  3. 返回 pong 以外的结果:若主节点返回其他结果,说明主节点当前无法处理命令,从节点断开连接并重连 。

image.png

# 4. 身份验证

  • 从节点的 masterauth 选项决定是否向主节点进行身份验证,设置了该选项就需要,未设置则不需要。
  • 从节点通过发送 auth 命令进行身份验证,命令参数是配置文件中 masterauth 的值。
  • 主节点设置密码的状态与从节点 masterauth 状态一致时(都存在且密码相同,或都不存在),身份验证通过,复制继续;不一致则从节点断开 socket 连接并重连。

image.png

# 5. 同步

  • 同步是使从节点数据库状态与主节点当前状态一致,从节点发送 psync 命令(Redis2.8 前是 sync 命令)开启同步。
  • 数据同步阶段是主从复制关键部分,根据主从节点状态不同分为全量复制部分复制,后续会详细介绍。

# 6. 命令传播

  • 主从同步后状态会因主节点新写命令而不一致,之后进入命令传播阶段,主节点发写命令给从节点执行来保证一致性。

  • 命令传播异步,主从难实时一致,延迟难免,其程度与网络、写命令频率、repl-disable-tcp-nodelay 配置等有关。

  • repl-disable-tcp-nodelay

  • 假如设置成 yes,则 redis 会合并小的 TCP 包从而节省带宽,但会增加同步延迟,造成 master 与 slave 数据不一致

  • 假如设置成 no,则 redis master 会立即发送同步数据,没有延迟

  • 概括来说就是:前者关注性能,后者关注一致性

Redis 是如何保证主从服务器一致处于连接状态以及命令是否丢失?
命令传播阶段,从服务器会利用心跳检测机制定时的向主服务发送消息。

# 全量复制和部分复制

  • Redis 2.8 以前,从节点用 sync 命令请求同步,同步方式是全量复制。

  • Redis 2.8 及以后,从节点用 psync 命令请求同步,同步方式根据主从节点状态可能是全量复制或部分复制,后文以该版本及以后为例。

  • 全量复制:用于初次复制或无法进行部分复制的情况,主节点将所有数据发送给从节点,操作重型。

  • 部分复制:用于网络中断等情况后的复制,只发送中断期间主节点的写命令,更高效;但网络中断时间过长,主节点未完整保存中断期间写命令时,无法进行部分复制,仍用全量复制 。

# 全量复制

  • 发起请求:从节点判断无法部分复制而请求全量复制,或从节点请求部分复制但主节点判定无法进行,具体判断后续介绍。
  • 生成文件与记录命令:主节点接收到全量复制命令后,执行 bgsave 后台生成 RDB 文件,同时用复制缓冲区记录新写命令。
  • 传输与更新数据:主节点 bgsave 完成后发送 RDB 文件给从节点,从节点清除旧数据、载入文件,将数据库状态更新至主节点执行 bgsave 时的状态。
  • 更新至最新状态:主节点把复制缓冲区的写命令发送给从节点执行,使从节点数据库状态更新至主节点最新状态。
  • 更新 AOF 文件(若开启):若从节点开启 AOF,会触发 bgrewriteaof,确保 AOF 文件更新至主节点最新状态 。

流程图如下:

image.png

存在问题

  • 主节点 RDB 持久化开销大:主节点执行 bgsave 命令 fork 子进程进行 RDB 持久化,此过程对 CPU、内存(页表复制)以及硬盘 IO 消耗较大。
  • 网络带宽消耗大:主节点需通过网络将 RDB 文件发送给从节点,这会大量消耗主从节点的带宽。
  • 从节点操作影响服务:从节点清空老数据、载入新 RDB 文件的过程会阻塞,无法响应客户端命令,若执行 bgrewriteaof 还会带来额外消耗。

# 部分复制

由于全量复制在主节点数据量较大时效率太低,因此 Redis2.8 开始提供部分复制,用于处理网络中断时的数据同步。

部分复制的实现,依赖于三个重要的概念:

  1. 复制偏移量
  2. 复制积压缓冲区
  3. 服务器运行 ID (runid)
    下面我们分别讲解一下这三个概念。

# 复制偏移量

  • 主从节点在执行复制时,各自维护一个复制偏移量 offset
  • 主节点向从节点同步 N 字节数据后,自身 offset 增加 N ;从节点从主节点同步 N 字节数据后,自身 offset 也增加 N
  • offset 可用于判断主从节点数据库状态一致性:若两者 offset 相同,状态一致;若不同则不一致。
  • offset 不同时,能依据两个 offset 确定从节点缺少的数据部分,如主节点 offset 为 1000,从节点 offset 为 500,部分复制需传递 offset 为 501 - 1000 的数据,这部分数据存储在复制积压缓冲区。

# 复制积压缓冲区(repl_backlog_buffer)

  • 主节点维护一个默认大小为 1MB 的固定长度、先进先出(FIFO)队列作为复制积压缓冲区。
  • 主节点在命令传播时,既向从节点同步写命令,也将写命令写入复制积压缓冲区,该缓冲区保存主节点最近执行的写命令,较早的命令会被挤出。
  • 若主从节点 offset 差距过大超过缓冲区长度,无法执行部分复制,只能进行全量复制。
  • 可通过配置 repl-backlog-size 增大复制积压缓冲区大小来提高网络中断时部分复制执行的概率,例如根据网络中断平均时间和主节点每秒产生写命令字节数计算出缓冲区需求,适当增大以保证多数断线情况可用部分复制。
  • 从节点向主节点发送 offset 后,主节点依据 offset 和缓冲区大小决定复制方式:若 offset 偏移量之后的数据仍在缓冲区,则执行部分复制;若不在(已被挤出),则执行全量复制。

image.png

# 服务器运行 id(runid)

  • 每个 Redis 节点都有在启动时自动生成的运行 ID。主节点会将自身运行 ID 发送给从节点,从节点保存该运行 ID。

  • 从节点断开重连时,依据运行 ID 判断同步进度:

  • 若从节点保存的 runid 与主节点当前 runid 相同,表明主从节点此前同步过,主节点会尝试进行部分复制(最终能否部分复制取决于 offset 和复制积压缓冲区情况)。

  • 若从节点保存的 runid 与主节点当前 runid 不同,意味着从节点断线前同步的并非当前主节点,此时只能进行全量复制 。

执行流程如下:

image.png

主节点收到 psync 命令后,会出现以下三种可能:

  • 主节点返回 fullresync {runid} {offset} 回复,表示主节点要求与从节点进行数据的完整全量复制,其中 runid 表示主节点的运行 ID,offset 表示当前主节点的复制偏移量
  • 如果主服务器返回 +continue,表示主节点与从节点会进行部分数据的同步操作,将从服务器缺失的数据复制过来即可
  • 如果主服务器返回 -err,表示主服务器的 Redis 版本低于 2.8,无法识别 psync 命令,此时从服务器会向主服务器发送 sync 命令,进行完整的数据全量复制

# 心跳检测机制

心跳检测机制有以下三个作用:

  • 检查网络连接状态:主节点信息中可查看从节点连接信息,包括状态、复制偏移量、延迟值(几秒前有过心跳检测)等,以此检查主从服务器网络连接状态。
  • 辅助实现 min-slaves 选项:Redis.conf 配置文件中有 min-slaves-to-write (最少包含的从服务器数量)和 min-slaves-max-lag (延迟值)两个参数,若取消注释,当从服务器数量少于 3 个或三个从服务器延迟大于等于 10 秒时,主服务器会拒绝执行写命令。
# 未达到下面两个条件时,写操作就不会被执行
# 最少包含的从服务器
# min-slaves-to-write 3
# 延迟值
# min-slaves-max-lag 10
  • 检测命令丢失:从服务器连接信息中有复制偏移量,若主从服务器复制偏移量不一致,主服务器会补发缺失数据。

# Redis 数据过期后的删除策略是什么?

# 总结分析

Redis 数据过期有两种删除策略:

  • 定期删除:Redis 每隔约 100 毫秒随机检查一定数量键,若发现过期键则删除,可在后台持续清理过期数据,防止内存膨胀。
  • 惰性删除:每次访问键时,Redis 检查其是否过期,若过期则删除,此策略确保使用时只删不再需的数据,不访问过期键时不立即清除。

# 扩展分析

# 定期删除

  • 基本原理:Redis 内部定时任务,每 100ms 周期性扫描设置过期时间的键。
  • 扫描限制:不会一次性扫描所有,每次扫描限制时间和数量,每次取 20 个 key 判断是否过期,过期 key 占比超 25% 则再取 20 个,小于 25% 或一次删除时间超 25ms 则停止,防止过度消耗 CPU 资源,可能导致部分过期键未及时删除。

# 惰性删除

  • 优点:减少 CPU 占用,仅在查询相关数据时执行删除操作,无需定时主动删除。
  • 缺点:若一直未查询某 Key,可能不被删除,易造成内存泄漏。

# 内存回收机制

  • 触发条件:Redis 内存使用达 maxmemory 限制时触发。

  • 删除策略

  • volatile-lru:从设置过期时间的键中用 LRU 算法删除。

  • allkeys-lru:从所有键中用 LRU 算法删除。

  • volatile-lfu:从设置过期时间的键中用 LFU 算法删除。

  • allkeys-lfu:从所有键中用 LFU 算法删除。

  • volatile-random:从设置过期时间的键中随机删除。

  • allkeys-random:从所有键中随机删除。

  • volatile-ttl:从设置过期时间的键中根据 TTL 优先删除存活时间短的键。

  • noeviction:不删除键,拒绝写入新数据。

Redis 正常情况下使用惰性删除 + 定期删除处理过期键,内存回收机制属于异常时的兜底处理。

# Redis 键过期时间设置

  • EXPIRE:以秒为单位设置键的过期时间。
  • PEXPIRE:以毫秒为单位设置键的过期时间。
  • SETEX:设置键值同时定义过期时间(秒级)。
  • PSETEX:类似 SETEX,支持毫秒级过期时间。

之前看到过一篇阿里的文章,感觉结合实际场景更能深入了解和记忆

从一个事故中理解 Redis(几乎)所有知识点