主从复制概述

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

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

主从复制的作用

主从复制的作用主要包括:

  • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
  • 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

如何使用主从复制

为了更直观的理解主从复制,在介绍其内部原理之前,先说明我们需要如何操作才能开启主从复制。

建立复制

需要注意,主从复制的开启,完全是在从节点发起的;不需要我们在主节点做任何事情。

从节点开启主从复制,有3种方式:

(1)配置文件

在从服务器的配置文件中加入:slaveof

(2)启动命令

redis-server启动命令后加入 --slaveof

(3)客户端命令

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

上述3种方式是等效的,下面以客户端命令的方式为例,看一下当执行了slaveof后,Redis主节点和从节点的变化。

实例

准备工作:启动两个节点
方便起见,实验所使用的主从节点是在一台机器上的不同Redis实例,其中主节点监听6379端口,从节点监听6380端口;从节点监听的端口号可以在配置文件中修改:

两个Redis节点启动后(分别称为6379节点和6380节点),默认都是主节点。

建立复制
此时在6380节点执行slaveof命令,使之变为从节点:

slaveof 127.0.0.1 6379

redis最简单的主从复制 redis的主从复制是什么_redis最简单的主从复制


观察效果

从节点获取mykey,现在可以看到是null

redis最简单的主从复制 redis的主从复制是什么_redis最简单的主从复制_02


在主节点上set mykey

redis最简单的主从复制 redis的主从复制是什么_redis_03


再到从节点上获取,就可以看到获取成功了。

redis最简单的主从复制 redis的主从复制是什么_数据_04


此时,在主节点上删除这个 mykey

redis最简单的主从复制 redis的主从复制是什么_redis_05


到从节点上观察,也随之没有了

redis最简单的主从复制 redis的主从复制是什么_redis最简单的主从复制_06

断开复制

通过slaveof 命令建立主从复制关系以后,可以通过slaveof no one断开。需要注意的是,从节点断开复制后,不会删除已有的数据,只是不再接受主节点新的数据变化。

从节点执行slaveof no one后,打印日志如下所示;可以看出断开复制后,从节点又变回为主节点。

[15192] 02 Aug 16:54:50.711 # Connection with master lost.
[15192] 02 Aug 16:54:50.712 * Caching the disconnected master state.
[15192] 02 Aug 16:54:50.713 * Discarding previously cached master state.
[15192] 02 Aug 16:54:50.713 * MASTER MODE enabled (user request from 'id=11 addr=127.0.0.1:52532 fd=7 name= age=1620 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=slaveof')

从节点断开后,主节点的日志中也可以看到

[15708] 02 Aug 16:54:50.713 # Connection with slave 127.0.0.1:6380 lost.

全量复制和部分复制

在Redis2.8以前,从节点向主节点发送sync命令请求同步数据,此时的同步方式是全量复制;在Redis2.8及以后,从节点可以发送psync命令请求同步数据,此时根据主从节点当前状态的不同,同步方式可能是全量复制或部分复制。后文介绍以Redis2.8及以后版本为例。

1.全量复制:用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作。

2.部分复制:用于网络中断等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效。需要注意的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制。

全量复制

Redis通过psync命令进行全量复制的过程如下:
(1)从节点判断无法进行部分复制,向主节点发送全量复制的请求;或从节点发送部分复制的请求,但主节点判断无法进行部分复制;具体判断过程需要在讲述了部分复制原理后再介绍。

(2)主节点收到全量复制的命令后,执行bgsave,在后台生成RDB文件,并使用一个缓冲区(称为复制缓冲区)记录从现在开始执行的所有写命令

(3)主节点的bgsave执行完成后,将RDB文件发送给从节点;从节点首先清除自己的旧数据,然后载入接收的RDB文件,将数据库状态更新至主节点执行bgsave时的数据库状态

(4)主节点将前述复制缓冲区中的所有写命令发送给从节点,从节点执行这些写命令,将数据库状态更新至主节点的最新状态

(5)如果从节点开启了AOF,则会触发bgrewriteaof的执行,从而保证AOF文件更新至主节点的最新状态

下面是执行全量复制时,主从节点打印的日志;可以看出日志内容与上述步骤是完全对应的。

主节点的打印日志如下:

redis最简单的主从复制 redis的主从复制是什么_redis_07

从节点打印日志如下图所示:

redis最简单的主从复制 redis的主从复制是什么_redis最简单的主从复制_08


其中,有几点需要注意:从节点接收了来自主节点的89260个字节的数据;从节点在载入主节点的数据之前要先将老数据清除;从节点在同步完数据后,调用了bgrewriteaof。

通过全量复制的过程可以看出,全量复制是非常重型的操作:

(1)主节点通过bgsave命令fork子进程进行RDB持久化,该过程是非常消耗CPU、内存(页表复制)、硬盘IO的

(2)主节点通过网络将RDB文件发送给从节点,对主从节点的带宽都会带来很大的消耗

(3)从节点清空老数据、载入新RDB文件的过程是阻塞的,无法响应客户端的命令;如果从节点执行bgrewriteaof,也会带来额外的消耗

部分复制

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

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

(1)复制偏移量
主节点和从节点分别维护一个复制偏移量(offset),代表的是主节点向从节点传递的字节数;主节点每次向从节点传播N个字节数据时,主节点的offset增加N;从节点每次收到主节点传来的N个字节数据时,从节点的offset增加N。

offset用于判断主从节点的数据库状态是否一致:如果二者offset相同,则一致;如果offset不同,则不一致,此时可以根据两个offset找出从节点缺少的那部分数据。例如,如果主节点的offset是1000,而从节点的offset是500,那么部分复制就需要将offset为501-1000的数据传递给从节点。而offset为501-1000的数据存储的位置,就是下面要介绍的复制积压缓冲区。

(2)复制积压缓冲区
复制积压缓冲区是由主节点维护的、固定长度的、先进先出(FIFO)队列,默认大小1MB;当主节点开始有从节点时创建,其作用是备份主节点最近发送给从节点的数据。注意,无论主节点有一个还是多个从节点,都只需要一个复制积压缓冲区。

在命令传播阶段,主节点除了将写命令发送给从节点,还会发送一份给复制积压缓冲区,作为写命令的备份;除了存储写命令,复制积压缓冲区中还存储了其中的每个字节对应的复制偏移量(offset)。由于复制积压缓冲区定长且是先进先出,所以它保存的是主节点最近执行的写命令;时间较早的写命令会被挤出缓冲区。

由于该缓冲区长度固定且有限,因此可以备份的写命令也有限,当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。反过来说,为了提高网络中断时部分复制执行的概率,可以根据需要增大复制积压缓冲区的大小(通过配置repl-backlog-size);例如如果网络中断的平均时间是60s,而主节点平均每秒产生的写命令(特定协议格式)所占的字节数为100KB,则复制积压缓冲区的平均需求为6MB,保险起见,可以设置为12MB,来保证绝大多数断线情况都可以使用部分复制。

从节点将offset发送给主节点后,主节点根据offset和缓冲区大小决定能否执行部分复制:

如果offset偏移量之后的数据,仍然都在复制积压缓冲区里,则执行部分复制;
如果offset偏移量之后的数据已不在复制积压缓冲区中(数据已被挤出),则执行全量复制。

服务器运行ID(runid)

每个Redis节点(无论主从),在启动时都会自动生成一个随机ID(每次启动都不一样),由40个随机的十六进制字符组成;runid用来唯一识别一个Redis节点。通过info Server命令,可以查看节点的runid:

redis最简单的主从复制 redis的主从复制是什么_主从复制_09


主从节点初次复制时,主节点将自己的runid发送给从节点,从节点将这个runid保存起来;当断线重连时,从节点会将这个runid发送给主节点;主节点根据runid判断能否进行部分复制:

  • 如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况);
  • 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。

psync命令的执行

在了解了复制偏移量、复制积压缓冲区、节点运行id之后,本节将介绍psync命令的参数和返回值,从而说明psync命令执行过程中,主从节点是如何确定使用全量复制还是部分复制的。

psync命令的执行过程可以参见下图(图片来源:《Redis设计与实现》):

redis最简单的主从复制 redis的主从复制是什么_redis_10


(1)首先,从节点根据当前状态,决定如何调用psync命令:

  • 如果从节点之前未执行过slaveof或最近执行了slaveof no one,则从节点发送命令为psync ? -1,向主节点请求全量复制;
  • 如果从节点之前执行了slaveof,则发送命令为psync ,其中runid为上次复制的主节点的runid,offset为上次复制截止时从节点保存的复制偏移量。

(2)主节点根据收到的psync命令,及当前服务器状态,决定执行全量复制还是部分复制:

  • 如果主节点版本低于Redis2.8,则返回-ERR回复,此时从节点重新发送sync命令执行全量复制;
  • 如果主节点版本够新,且runid与从节点发送的runid相同,且从节点发送的offset之后的数据在复制积压缓冲区中都存在,则回复+CONTINUE,表示将进行部分复制,从节点等待主节点发送其缺少的数据即可;
  • 如果主节点版本够新,但是runid与从节点发送的runid不同,或从节点发送的offset之后的数据已不在复制积压缓冲区中(在队列中被挤出了),则回复+FULLRESYNC ,表示要进行全量复制,其中runid表示主节点当前的runid,offset表示主节点当前的offset,从节点保存这两个值,以备使用。

部分复制演示

在下面的演示中,网络中断几分钟后恢复,断开连接的主从节点进行了部分复制;为了便于模拟网络中断,本例中的主从节点在局域网中的两台机器上。

网络中断

网络中断一段时间后,主节点和从节点都会发现失去了与对方的连接(关于主从节点对超时的判断机制,后面会有说明);此后,从节点便开始执行对主节点的重连,由于此时网络还没有恢复,重连失败,从节点会一直尝试重连。

主节点日志如下:

redis最简单的主从复制 redis的主从复制是什么_Redis_11


从节点日志如下:

redis最简单的主从复制 redis的主从复制是什么_数据_12


网络恢复

网络恢复后,从节点连接主节点成功,并请求进行部分复制,主节点接收请求后,二者进行部分复制以同步数据。

主节点日志如下:

redis最简单的主从复制 redis的主从复制是什么_数据_13


从节点日志如下:

redis最简单的主从复制 redis的主从复制是什么_主从复制_14