likes
comments
collection
share

Redis主从复制实现原理

作者站长头像
站长
· 阅读数 11

在引入主从复制之前,Redis 主要面临的问题是单点故障风险和性能瓶颈。如果单个 Redis 服务器出现故障,可能会导致数据丢失和服务中断。同时,随着用户和数据量的增加,单个服务器处理所有请求的能力也可能受限。这些问题促使了主从复制功能的引入,以增强系统的可靠性和扩展性。

主从复制的概念

主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为主节点(master),后者称为从节点(slave);

数据的复制是单向的,只能由主节点到从节点。

默认情况下,每台 Redis 服务器都是主节点,且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

在 Redis 中,用户可以通过执行 SLAVEOF 命令或者 slavrof 选项,让一个服务器(replicate)去复制另一个服务器,我们称呼被复制的服务器为主服务器 master,而对主服务进行复制的服务器则被称之为从服务器(slave)。

Redis 主从复制服务器架构如下图所示:

Redis主从复制实现原理

主从分离的作用

Redis 主从复制的作用主要体现在以下几个方面:

  1. 数据冗余:通过在一个或多个从服务器上复制主服务器的数据,提高了数据的冗余性,即使主服务器出现故障,数据也不会丢失。

  2. 读写分离:可以将读操作分散到从服务器上,减轻主服务器的负载。这样主服务器可以专注于处理写操作,而从服务器处理读请求,提高了系统的整体读写性能。

  3. 容灾备份:在不同的物理位置设置从服务器可以作为灾难恢复策略,即使某个地区的服务器全部故障,其他地区的从服务器仍然可以提供服务。

  4. 系统维护与升级:在进行系统维护或升级时,可以先升级从服务器,确保服务的连续性。在主服务器维护期间,某个从服务器可以被提升为新的主服务器,以继续处理写请求。

  5. 高可用性:与 Redis 哨兵(Sentinel)或集群一起使用时,主从复制可以支持自动故障转移。如果主服务器故障,哨兵或集群会自动将一个从服务器提升为新的主服务器,保证服务的高可用性。

总的来说,Redis 主从复制极大地提高了数据的安全性、系统的可用性和扩展性,是 Redis 高性能和高可靠性的关键特性之一。

旧版复制功能的实现

从这里我们先来介绍 Redis 在 2.8 版本之前复制功能的实现原理,并说明旧版复制功能在处理处理断线后重新连接的从服务器时,会遇上怎样的低效情况。

以下是旧版 Redis 复制功能的详细实现过程:

同步过程

  1. 开始同步:

    • 当一个从服务器(slave)启动并连接到主服务器(master)时,它需要载入与主服务器相同的数据集。从服务器会向主服务器发送 SYNC 命令来开始同步过程。
  2. 主服务器响应:

    • 主服务器接收到 SYNC 命令后,会启动一个新的后台进程来创建数据的快照。这个过程通常是通过执行 bgsave 命令完成的,该命令会创建一个 RDB 文件,包含了那一刻主服务器上所有的数据。
  3. 数据传输:

    • 在创建快照的同时,主服务器会开始累积所有新的写命令到一个缓冲区中。这些命令是在快照进行期间发生的,需要在快照完成后传输给从服务器,以确保数据的一致性。
    • 一旦快照文件创建完毕,主服务器就会将这个 RDB 文件发送给从服务器。随后,主服务器还会将累积的写命令发送给从服务器。
  4. 从服务器载入数据:

    • 从服务器接收到 RDB 文件后,会将其自身的旧数据集清空,然后载入 RDB 文件中的数据。这一步完成后,从服务器的数据集应该与主服务器在快照那一刻的数据集相同。
    • 然后,从服务器会执行收到的所有写命令,以便捕获在进行快照期间发生的所有更改。

如下图展示了命令执行期间,主从服务器的通信过程:

Redis主从复制实现原理

时间主服务器从服务器
T0服务器启动服务器启动
T1执行 set k1 v1
T2执行 set k2 v2
T3执行 set k3 v3
T4向主服务发送 SYNC 命令
T5接收到从服务器发来的 SYNC 命令,执行 BGSAVE 命令,创建 k1、k2、k3 的 RDB 文件,并使用缓冲区域记录接下来执行的所有写命令
T6执行 set k4 v4,并将这个命令记录到缓冲区里面
T7执行 set k5 v5,并将这个命令记录到缓冲区里面
T8BGSAVE 命令执行完毕,向从服务器发送 RDB 文件
T9接收并载入主服务器发来的 EDB 文件,获得 k1、k2、k3 三个键
T10向从服务器发送缓冲区中保存的写命令 set k4 v4 和 set k5
T11接收并执行主服务器发来的两个 SET 命令,得到 k4 和 k5 两个键
T12同步完成,现在主从服务器两者数据库都包含了键 k1、k2、k3 、k4 和 k5同步完成,现在主从服务器两者数据库都包含了键 k1、k2、k3 、k4 和 k5

命令传播

在同步操作执行完毕之后,主从服务器两者的数据库将达到一致状态,但这种一致并不是一成不变的,每当主服务器执行客户端发送的写命令时,主服务器的数据库就有可能会被修改,并导致主服务器状态不再一致。

由于主服务器持续处理客户端的写命令,这种一致性是短暂且动态变化的。以下是一个例子来详细说明这个问题:假设你有一个主服务器(Master)和一个从服务器(Slave),它们通过 Redis 的复制功能连接。初始时刻,Master 和 Slave 的数据完全一致,比如都只有一个键值对:{user1: "Alice"}。

现在 Slave 通过发送 SYNC 命令与 Master 进行了一次全量同步,同步完成后,Slave 的数据完全与 Master 一致。

写命令和状态不一致:

  1. 主服务器接收写命令:

    • 假设此时,Master 接收到一个客户端发送的写命令:SET user2 "Bob"。Master 执行这个命令,其数据库状态变为{user1: "Alice", user2: "Bob"}。
  2. 命令传播延迟:

    • Master 需要将这个写命令实时传播给 Slave。但由于网络延迟、处理时间或其他因素,这个过程并非瞬时完成。即使延迟只有几毫秒,也意味着在这段时间内,Master 和 Slave 的状态是不一致的。
  3. 再次状态不一致:

    • 而且,在 Master 将 user2 的更新传播给 Slave 之前,它可能又接收到了更多的写命令并执行,例如 DEL user1。这会导致 Master 和 Slave 之间的不一致状态进一步扩大。
  4. 持续的状态变化:

    • 在一个高负载的环境中,客户端可能会持续不断地发送写命令给 Master,而 Master 也会持续不断地尝试将这些变化同步给 Slave。这意味着系统需要不断地处理状态不一致和同步问题。

这样就造成了在任何给定的时间点,由于写命令的持续处理和传播延迟,Master 和 Slave 之间可能存在短暂的数据不一致。

如果由于网络问题或其他原因导致 Slave 断开连接,当它重新连接时,旧版复制机制将执行另一次全量同步,这在大数据集情况下非常消耗资源和时间。

在高负载或网络延迟的情况下,Slave 可能会显著落后于 Master,导致延迟和数据不一致问题更加严重。

旧版复制功能的缺陷

Redis 旧版复制功能虽然提供了基本的主从数据同步能力,但存在一些缺陷和限制,这些问题随着 Redis 的发展和更高性能需求的出现逐渐显现出来:

  1. 全量同步的高成本

    • 数据传输成本:每次从服务器连接到主服务器时,无论数据变化的大小,都会进行全量同步。这意味着整个数据集都需要被重新传输和加载,导致大量的网络流量和较高的延迟。
    • 磁盘和 CPU 消耗:全量同步需要主服务器执行 bgsave 生成快照,这个过程会占用大量磁盘空间和 CPU 资源,影响主服务器的性能。
  2. 短暂的不一致性

    • 实时性问题:由于网络延迟和处理时间,主服务器上的写命令不能即时反映到从服务器上。这导致了主从服务器之间的短暂数据不一致,尤其在高负载或网络不稳定的环境下更加明显。
  3. 缺乏故障恢复机制

    • 自动故障转移缺失:旧版复制功能没有内置的故障转移机制。当主服务器出现故障时,需要手动干预进行故障恢复,这在需要高可用性的系统中是一个重大缺陷。
  4. 重同步的效率问题

    • 断线后的全量同步:如果从服务器暂时断线,即使错过了很少量的数据更新,重新连接后也需要进行一次全量同步。这种重同步机制效率低下,尤其是对于大型数据集来说。
  5. 缺乏灵活性和扩展性

    • 缩放问题:在旧版复制机制下,扩展读操作(通过增加更多的从服务器)会增加全量同步的频率和开销,尤其是在动态扩展和缩放的环境中。
    • 写操作瓶颈:所有的写操作都必须通过单个主服务器,这在高写负载的情况下成为性能瓶颈。
  6. 复制延迟

    • 延迟的影响:在数据集很大或网络条件较差的情况下,复制延迟可能会变得非常显著,影响从服务器的数据时效性和应用的响应速度。

尽管旧版的复制功能为 Redis 提供了基本的数据复制和持久化能力,但上述缺陷限制了其在大规模和高可用性要求的场景下的应用。因此,在 Redis 2.8 及以后的版本中,引入了部分重同步(PSYNC)、复制积压缓冲区和其他改进,以解决这些问题,提供更高效、更可靠的复制机制。

新版复制功能的实现

Redis 新版复制功能从 2.8 版本开始引入了显著的改进,解决了旧版中的一些问题,并提供了更加高效和可靠的数据同步机制。

这个主要的改动就是使用 PSYNC 命令代替 SYNC 命令来执行复制时的同步操作。

PSYNC 命令具有完整重同步和部分重同步两种模式:

  1. 其中完整冲同步用于初次复制情况:完整重同步的执行步骤和 SYNC 命令的执行步骤基本一样,它们都是通过让主服务器创建并发送 RDB 文件,以及向从服务器发送保存在缓冲区里面的写命令来进行同步

  2. 而部分重同步则用于处理断线后重复制情况:当从服务器在断线后重现连接主服务器时,如果条件允许,主服务器可以将主服务器连线断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务当前所处的状态。

PSYNC 命令引入了部分重同步机制。如果从服务器断线后不久重新连接,并且主服务器的复制积压缓冲区仍然包含从服务器所缺失的所有写命令,那么只需要传输这些缺失的命令,而不是整个数据集。

如下表展示了如何使用 PSYNC 命令高效地处理地处理断线后复制的情况:

时间主服务器从服务器
T0主从服务器完成同步主从服务器完成同步
T1执行并传播 SET k1 v1执行主服务器传来的 SET k1 v1
T2执行并传播 SET k2 v2执行主服务器传来的 SET k2 v2
.........
T100执行并传播 SET k100 v100执行主服务器传来的 SET k100 v100
T101执行并传播 SET k101 v101执行主服务器传来的 SET k101 v101
T102主从服务器连接断开主从服务器连接断开
T103执行并传播 SET k102 v102断线中,尝试重现连接主服务器
T104执行并传播 SET k103 v103断线中,尝试重现连接主服务器
T105执行并传播 SET k104 v104断线中,尝试重现连接主服务器
T106向主服务发送 PSYNC
T107向从服务器返回 +CONTINUE 回复,表示执行部分重同步
T108接收 +CONTINUE 回复,准备执行部分重同步
T109向从服务器发送 SET k102 v102、SET k103 v103、SET k104 v104 三个命令
T110接受并执行主服务器传来的三个 SET 命令
T111主从服务器再次完成同步主从服务器再次完成同步

通过上表再和之前的内容对比不难得出,两者都可以实现让断线的主从服务器重新回到一致的状态,但执行部分冲同步所需要的资源比执行 SYNC 命令所需的资源要少得多,完成同步的速度也快得多。

PSYNC 命令在断线后重复制机制通过引入部分重同步和复制积压缓冲区,相比旧版在效率、资源消耗和系统可用性方面都有了显著的改进。这些改进使得 Redis 在维护数据一致性的同时,也能更好地满足高可用性和性能的要求。

执行 SYNC 命令所需要生成、传送和载入整个 RDB 文件,而部分冲同步只要将从服务器缺少的写命令发送给服务器执行就可以了。

部分冲同步的实现

在前面的内容中,我们了解到了 PSYNC 命令的由来,以及部分重同步的工作方式之后,在接下来的内容中,我们来学习一下部分重同步的实现细节。

部分冲同步功能主要由一下三个部分构成:

  1. 主服务器的复制偏移量和从服务器的复制偏移量。
  2. 主服务器的复制积压缓冲区。
  3. 服务器的允许 ID。

接下来我们将会分别介绍这三个部分。

复制偏移量

复制偏移量是一个长整型数字,它表示从复制开始以来,主服务器发送给从服务器的字节总数。每次主服务器向从服务器发送数据时,它的复制偏移量就会增加。它允许 Redis 精确地跟踪主从服务器间数据传输的位置。这对于确定从服务器是否与主服务器同步,以及在断线后恢复复制过程非常重要。

执行复制的双方分别维护一个复制偏移量:

  1. 主服务器偏移量:主服务器的复制偏移量反映了它到目前为止发送给所有从服务器的数据量。
  2. 从服务器偏移量:每个从服务器也会维护一个复制偏移量,表示它到目前为止从主服务器接收的数据量。

在之前的例子中,主从服务器的复制偏移量的值为 101。

Redis主从复制实现原理

如果这时候主服务向三个从服务器传播长度为 33 字节的数据,那么主服务的复制偏移量将更新为 101+33=134,而三个从服务器在接收到主服务器转播的数据之后,也会将复制偏移量更新为 134,如下图所示:

Redis主从复制实现原理

当从服务器请求部分重同步时(使用 PSYNC 命令),它会发送自己当前的复制偏移量给主服务器。

主服务器使用这个偏移量来判断是否可以进行部分重同步。如果主服务器的复制积压缓冲区包含了从这个偏移量开始的所有数据,则部分重同步是可能的;否则,需要进行完整重同步。

每当主服务器处理一个写命令并将其发送到从服务器时,它的复制偏移量就会更新。相应地,当从服务器接收并应用这个写命令时,它的偏移量也会更新。

主服务器会定期在 RDB 快照中记录自己的复制偏移量,而从服务器也可能将其偏移量记录在自己的快照中。这有助于在服务器重启后恢复复制状态。

Redis主从复制实现原理

使用 info replication 命令可以返回当前实例的复制信息。在返回的值中,与复制偏移量相关的字段是:

  1. master_repl_offset:这个字段表示主服务器的当前复制偏移量。对于主服务器来说,它代表到目前为止主服务器发送给从服务器的字节总数。对于从服务器,这个字段表示它从主服务器接收到的数据量。

  2. slave_repl_offset:这个字段出现在从服务器的 info replication 输出中,表示该从服务器的当前复制偏移量。

如下图所示:

Redis主从复制实现原理

复制积压缓冲区

复制积压缓冲区是主服务器上的一个固定大小的缓冲区,它记录了最近的写操作。这个缓冲区的目的是帮助快速同步那些暂时断开连接的从服务器。当从服务器重新连接主服务器时,它们可以请求自断开以来发生的所有更改,而不是重新复制整个数据库。

缓冲区是固定大小的(可以配置),默认为 1MB,当新的数据写入时,最老的数据会被删除。

当主服务器上发生写操作时,这些变更会被追加到复制积压缓冲区。

当从服务器连接到主服务器请求数据时,它们会发送自上次同步以来的最后一个已知偏移量。主服务器会使用这个偏移量在复制积压缓冲区中找到相应的数据并发送给从服务器。

当从服务器重新连接上主服务器时,从服务器会通过 PSYNC 命令将自己的复制偏移量 offset 发送给主服务器,主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作:

  1. 发送同步请求:

    • 从服务器向主服务器发送 PSYNC 命令,附带它的最后复制偏移量。
    • 如果是首次连接或需要全量同步,从服务器发送 PSYNC ? -1。
  2. 主服务器响应:

    • 主服务器接收到 PSYNC 请求后,会检查复制积压缓冲区。

    • 如果请求的偏移量在缓冲区的范围内:

      1. 主服务器响应+CONTINUE,表示可以进行部分重同步。
      2. 主服务器从请求的偏移量开始,发送所有缺失的数据给从服务器。
    • 如果请求的偏移量不在缓冲区的范围内:

      1. 主服务器响应+FULLRESYNC,表示需要进行全量同步。
      2. 主服务器同时发送一个新的复制偏移量和一个积压缓冲区标识符给从服务器,从服务器将使用这些信息来跟踪未来的增量同步。
  3. 发送同步请求:

    • 部分重同步(+CONTINUE):从服务器接收缺失的数据,并应用到本地数据集,迅速达到与主服务器相同的状态。
    • 全量同步(+FULLRESYNC):从服务器清空本地数据,接收并应用主服务器发送的完整数据集。

假设一个从服务器因网络问题与主服务断开了链接。在这段时间内,主服务器继续处理写命令,并将这些变更记录到复制积压缓冲区中。

  1. 网络恢复后,从服务器尝试重新连接,并发送 PSYNC 以及它记录的最后复制偏移量给主服务器。
  2. 主服务器接收到 PSYNC 请求,检查复制积压缓冲区,发现自从服务器断开后的所有变更都在缓冲区内。
  3. 主服务器发送+CONTINUE 响应给从服务器,并从请求的偏移量开始发送缺失的数据。
  4. 从服务器接收数据,应用这些变更,快速同步到当前的数据状态,而无需进行耗时的全量同步。

通过这种方式,PSYNC 命令使得 Redis 的主从复制更加高效和弹性。它最大化了网络和存储资源的使用效率,同时提供了快速恢复和数据一致性的保障。

复制积压缓冲区示意图:

Redis主从复制实现原理

服务器运行 ID

Redis 的复制功能中,服务器运行 ID(Run ID)是一个非常重要的组成部分。每个 Redis 服务器(无论是主服务器还是从服务器)在启动时都会生成一个唯一的运行 ID。这个 ID 是一个随机的字符串,用来在复制过程中唯一标识每个服务器实例。

在 Redis CLI 或客户端中,执行 INFO 命令。INFO 命令返回关于 Redis 服务器的各种信息和统计数据:

当一个从服务器连接到主服务器时,它会记录主服务器的运行 ID。这样,如果连接中断再重新连接,从服务器可以确认它重新连接的是同一个主服务器。

在使用 PSYNC 命令进行部分重同步时,从服务器会发送其所知的主服务器的运行 ID 和复制偏移量。主服务器会检查这个运行 ID 是否与自己的 ID 匹配,以确定是否可以继续进行部分重同步。

运行 ID 帮助确保即使在网络断开或服务器重启的情况下,复制的持久性和一致性也能得到保障。如果从服务器试图连接到一个新的主服务器(可能因为原来的主服务器不可用了),它会注意到运行 ID 的变化,并知道它需要进行一个完全同步,而不是部分重同步。

假设你有一个 Redis 主服务器和两个从服务器。每个服务器启动时都生成了唯一的运行 ID:

  • 主服务器:运行 ID 为 runid-master-12345
  • 从服务器 1:运行 ID 为 runid-slave1-67890
  • 从服务器 2:运行 ID 为 runid-slave2-abcde

如果主服务器因为某些原因重启了,并且得到了一个新的运行 ID(比如 runid-master-54321),以下是会发生什么:

  • 从服务器尝试重新连接到主服务器。
  • 它们发送自己的最后复制偏移量和所知的主服务器的运行 ID(runid-master-12345)。
  • 主服务器检查这个运行 ID 与自己当前的运行 ID 不匹配,说明自己是一个新的实例。
  • 因此,主服务器告知从服务器需要进行全量同步,而不是部分重同步。

通过这种方式,服务器运行 ID 在 Redis 复制中起到了确保数据一致性、辨识服务器身份和指导同步过程的关键作用。

参考资料

  • 书籍:Redis 设计与实现

总结

Redis 的主从复制是一种机制,用于将数据从一个主节点自动复制到一个或多个从节点。这增加了数据的冗余、提高了读取能力、支持了读写分离,并有助于实现高可用性和灾难恢复。主从复制通过全量同步和部分重同步两种方式同步数据,依赖于复制偏移量和复制积压缓冲区来有效管理数据一致性和网络中断。通过这种方式,Redis 可以在保持高性能的同时,提供数据的安全性和稳定性。

最后分享两个我的两个开源项目,它们分别是:

这两个项目都会一直维护的,如果你也喜欢,欢迎 star 🚗🚗🚗