复制

在Redis中,用户可以通过执行SLAVEOF命令或者设置slaveof选项,让一个服务器去复制(replicate)另一个服务器,我们称呼被复制的服务器为主服务器(master),而对主服务器进行复制的服务器则被称为从服务器(slave),如图1-1所示

图1-1   主服务器和从服务器

假设现在有两个Redis服务器,地址分别为127.0.0.1:6379和127.0.0.1:12345,如果我们向服务器127.0.0.1:12345发送以下命令:

127.0.0.1:12345> SLAVEOF 127.0.0.1 6379
OK

  

那么服务器127.0.0.1:12345将成为127.0.0.1:6379的从服务器,而服务器127.0.0.1:6379则成为127.0.0.1:12345的主服务器。比如说,如果我们向主服务器执行以下命令:

127.0.0.1:6379> SET msg "hello world"
OK

  

又可以在从服务器上获取msg键的值:

127.0.0.1:12345> GET msg
"hello world"

  

另一方面,如果我们在主服务器中删除了键msg:

127.0.0.1:6379> DEL msg
(integer) 1

  

那么不仅主服务器上的msg键会被删除:

127.0.0.1:6379> EXISTS msg
(integer) 0

  

从服务器上的msg键也会被删除:

127.0.0.1:12345> EXISTS msg
(integer) 0

  

旧版复制功能的实现

Redis的复制功能分为同步(sync)和命令传播(command propagate)两个操作:

  • 同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态
  • 命令传播操作则用于在主服务器状态诶修改,导致主从服务器的数据库状态出现不一致时,让主从服务器的数据库状态重新回到一致状态

同步

当客户端向从服务器发送SLAVEOF命令,要求从服务器复制主服务器时,从服务器首先需要执行同步操作,也就是,将从服务器的数据库状态更新至主服务器当前所处的数据库状态。从服务器对主服务器的同步操作需要通过向主服务器发送SYNC命令来完成,以下是SYNC命令的执行步骤:

  1. 从服务器向主服务器发送SYNC命令
  2. 收到SYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有命令
  3. 当主服务器的BGSAVE命令执行完毕时,主服务器会将BGSAVE命令生成的RDB文件发送给从服务器,从服务器接收并载入这个RDB文件,将自己的数据库状态更新至主服务器执行执行BGSAVE命令时的数据库状态
  4. 主服务器将记录在缓冲区中的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新至主服务器数据库当前所处的状态

图1-2展示了SYNC命令执行期间,主从服务器的通信过程

图1-2   主从服务器在执行SYNC命令期间的通信过程

表1-1展示了一个主从服务器进行同步的例子

表1-1   主从服务器的同步过程
时间 主服务器 从服务器
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 ,并将这个命令记录到缓冲区里面  
T8 BGSAVE 命令执行完毕,向从服务器发送 RDB 文件  
T9   接收并载入主服务器发来的 RDB 文件 ,获得 k1 、 k2 、 k3 三个键
T10 向从服务器发送缓冲区中保存的写命令SET k4 v4 和 SET k5 v5 接收并执行主服务器发来的两个 SET 命令,得到 k4 和 k5 两个键
T11 同步完成,现在主从服务器两者的数据库都包含了键k1 、 k2 、 k3 、 k4 和k5  同步完成,现在主从服务器两者的数据库都包含了键k1 、 k2 、 k3 、 k4 和k5

命令传播

在同步操作执行完毕之后,主从服务器两者的数据库状态将达到一致,但这种一致并不是一成不变的,每当主服务器执行客户端发送的写命令时,主服务器的数据库就有可能会被修改,并导致主从服务器状态不再一致。举个栗子,假设一个主服务器和一个从服务器刚刚完成同步操作,它们的数据库都保存了相同的五个键k1至k5,如图1-3所示

图1-3   处于一致状态的主从服务器

如果这时,客户端向主服务器发送命令DEL k3,那么主服务器在执行完这个DEL命令之后,主从服务器的数据库状态将出现不一致:主服务器的数据库已经不再包含键k3,但这个键却仍然包含在从服务器的数据库中,如图1-4所示

图1-4   处于不一致状态的主从服务器

为了让主从服务器再次回到一致状态,主服务器需要对从服务器执行命令传播操作,主从服务器会将自己的写命令,即是造成主从服务器不一致的那条写命令发送给从服务器执行,当从服务器执行了相同的写命令后,主从服务器将再次回到一致的状态

在上面的例子中,主服务器因为执行了命令DEL k3而导致主从服务器不一致,所以主服务器将向从服务器发送相同的命令DEL k3。当从服务器执行完在这个命令之后,主从服务器再次回到一致状态,现在主从服务器两者的数据库都不再包含键k3了,如图1-5所示

图1-5   主服务器向从服务器发送命令

旧版复制功能的缺陷

在Redis中,从服务器对主服务器的复制可以分为以下两种情况:

  • 初次复制:从服务器以前没有复制过任何主服务器,或者从服务器当前要复制的主服务器和上一次复制的主服务器不同
  • 断线后重复制:处于命令传播阶段的主从服务器因为网络原因而中断了复制,但从服务器通过自动重连接重新连上了主服务器,并继续复制主服务器

对于初次复制来说,旧版复制功能能够很好的完成任务,但对于断线后重复制来说,旧版复制功能虽然也能让主从服务器重新回到一致状态,但效率非常低。要理解这一情况,请看表1-2展示的断线后重复值例子

表1-2   从服务器在断线之后重新复制主服务器的例子
时间 主服务器 从服务器
T0 主从服务器完成同步  主从服务器完成同步 
T1 执行并传播SET k1 v1  执行主服务器传来的SET k1 v1 
T2 执行并传播SET k2 v2 执行主服务器传来的SET k2 v2 
…… ……  ……
T10085 执行并传播SET kl0085 v10085  执行主服务器传来的SET kl0085 v10085 
T10086 执行并传播SET kl0086 v10086 执行主服务器传来的SET k10086 v10086 
T10087 主从服务器连接断开  主从服务器连接断开 
T10088 执行 SET kl0087 v10087  断线中,尝试重新连接主服务器 
T10089 执行 SET k10088 v1008  断线中,尝试重新连接主服务器 
T10090 执行 SET kl0089 v10089 断线中,尝试重新连接主服务器 
T10091 主从服务器重新连接  主从服务器重新连接 
T10092   向主服务器发送SYNC命令
T10093 接收到从服务器发来的SYNC命令,执行BGSAVE命令,创建包含键k1至键k10089的RDB文件,并使用缓冲区记录接下来执行的所有写命令  
T10094 BGSAVE命令执行完毕向从服务器发送RDB文件  
T10095   接收并载入主服务器发来的RDB文件,获得键k1至键kl0089
T10096 因为在BGSAVE命令执行期间,主从务器没有执行任何写命令,所以跳过发送缓冲区包含的写命令这一步   
T10097 主从服务器再次完成同步 主从服务器再次完成同步 

在时间T10091,从服务器终于重新连接上主服务器,因为这时候主从服务器的状态已经不再一致,所以从服务器将向主服务器发送SYNC命令,而主服务器会将包含键k1至键k10089的RDB文件发送给从服务器,从服务器通过接收和载入这个RDB文件来将自己的数据库更新至主服务器数据库当前所处的状态

虽然再次发送SYNC命令可以让主从服务器重新回到一致状态,但如果我们仔细研究这个断线重复过程,就会发现传送RDB文件这一步实际上并不是非做不可的:

  • 主从服务器在时间T0只时间T10086中一直处于一致状态,这两个服务器保存的数据大部分是相同的
  • 从服务器想要将自己更新至主服务器当前所处的状态,真正需要的是主从服务器连接中断期间,主服务器新添加了k10087、k10088、k10089三个键的数据
  • 可惜的是,旧版复制功能并没有利用以上例举的两点,而是继续让主服务器生成RDB文件传输给从服务器,但对于RDB文件中从k1至k10086的数据其实是没必要的

上面给出的例子可能有一点理想化,因为在主从服务器断线期间,主服务器执行的写命令可能会有成百上千之多,而不仅仅是两三个写命令。但总的来说,主从服务器断开的时间越短,主服务器在断线期间执行的写命令就越少,而执行少量写命令所产生的数据量通常比整个数据库的数据量要少的多,在这种情况下,为了让从服务器补足一小部分缺失的数据,却要让主从服务器重新执行一次SYNC,这种做法无疑是非常低效的

SYNC命令是非常消耗资源的,因为每次执行SYNC命令,主从服务器需要执行一下操作:

  • 主服务器需要执行BGSAVE命令来生成RDB文件,这个生成操作会耗费主服务器大量的CPU、内存和磁盘I/O资源
  • 主服务器需要将自己生成的RDB文件发送给从服务器,这个发送操作会耗费主从服务器大量的网络资源(带宽和流量),并对主服务器响应命令请求的时间产生影响
  • 接收到RDB文件的从服务器需要载入主服务器发来的RDB文件,并且在载入期间,从服务器会因为阻塞而没办法处理命令请求

SYNC是一个如此消耗资源的命令,所以Redis最好在真需要的时候才需要执行SYNC命令

新版复制功能的实现

为了解决旧版复制功能在处理断线重复制情况时的低效问题,Redis从2.8版本开始,使用PSYNC命令代替SYNC命令来执行复制时的同步操作。PSYNC命令具有完整同步和部分同步两种模式:

  • 其中完整重同步用于处理初次复制情况:完整重同步的执行步骤和SYNC命令的执行步骤基本一样,它们都是通过让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区里面的写命令来进行同步
  • 而部分重同步则用于处理断线后重复制情况:当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态

PSYNC命令的部分重同步模式解决了旧版复制功能在处理断线后重复值时出现的低效情况,表1-3展示了如何使用PSYNC命令高效地处理上一节断线后的情况

表1-3   使用PSYNC命令来进行断线后重复制
时间 主服务器 从服务器
T0 主从服务器完成同步 主从服务器完成同步
T1 执行并传播SET k1 v1 执行主服务器传来的SET k1 v1
T2 执行并传播SET k2 v2 执行主服务器传来的SET k2 v2
…… …… ……
T10085 执行并传播SET kl0085 v10085 执行主服务器传来的SET kl0085 v10085
T10086 执行并传播SET kl0086 v10086 执行主服务器传来的SET kl0086 v10086
T10087 主从服务器连接断开 主从服务器连接断开
T10088 执行 SET k10087 v10087 断线中,尝试重新连接主服务器
T10089 执行 SET k10088 v10088 断线中,尝试重新连接主服务器
T10090 执行 SET k10089 v10089 断线中,尝试重新连接主服务器
T10091 主从服务器重新连接 主从服务器重新连接
T10092   向主服务器发送PSYNC命令
T10093 向从服务器返回+CONTINUE回复,表示执行部分重同步  
T10094   接收+COTINUE回复,准备执行部分重同步
T10095 向从服务器发送SET kl0087 v10087、SET k10088 V10088、 SET k10089 v10089三个命令  
T10096   接收并执行主服务器传来的三个SET命令
T10097 主从服务器再次完成同步 主从服务器再次完成同步

对比一下SYNC和PSYNC命令处理断线重复制的方法,不难看出,虽然SYNC命令和PSYNC命令都可以让断线的主从服务器重新回到一致的状态,但执行部分重同步所需的资源比起执行SYNC命令所需的资源要少得多,完成同步的速度也快得多。执行SYNC命令需要生成、传送和载入整个RDB文件,而部分重同步只需要将从服务器缺少的写命令发送给从服务器就可以了。图1-6展示了主从服务器在执行部分重同步时的通信过程

图1-6   主从服务器执行部分重同步的过程

部分重同步的实现

在了解了PSYNC命令的由来,以及部分重同步的工作方式止之后,我们来看一下部分重同步的实现细节。部分重同步由以下三个部分构成:

  • 主服务器的复制偏移量(replication offset)和从服务器的复制偏移量
  • 主服务器的复制积压缓冲区(replication backlog)
  • 服务器的运行ID(run ID)

复制偏移量

执行复制的双方——主服务器和从服务器会分别维护一个复制偏移量:

  • 主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N
  • 从服务器每次收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量的值加上N

如图1-7所示的例子中,主从服务器的复制偏移量的值都为10086

图1-7   拥有相同偏移量的主服务器和它的三个从服务器

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

图1-8   更新偏移量之后的主从服务器

通过对比主从服务器的复制偏移量,程序可以很容易地知道主从服务器是否处于一致状态:

  • 如果主从服务器处于一致状态,那么主从服务器两者的偏移量总是相同的
  • 相反,如果主从服务器两者的偏移量并不相同,那么说明主从服务器并未处于一致状态

考虑以下这个例子,假设如图1-7所示,主从服务器当前的复制偏移量都为10086,但时就在主服务器向从服务器传播长度为33字节的数据之前,从服务器A断线了,那么主服务器传播的数据将只有从服务器B和C能收到,在这之后,主服务器、从服务器B和C三个服务器的复制偏移量都将更新为10119,而断线的从服务器A的复制偏移量仍然停留在10086,这说明从服务器A与主服务器并不一致,如题1-9所示

图1-9   因为断线而处于不一致状态的从服务器A

假设从服务器A在断线之后就立即重新连接上主服务器,并且成功,那么接下来,从服务器向主服务器发送PSYNC命令,报告从服务器A当前的复制偏移量为10086,那么这时,主服务器应该对从服务器执行完整重同步还是部分重同步呢?如果执行部分重同步的话,主服务器又如何补偿从服务器A在断线期间丢失的那部分数据呢?答案还是和复制积压缓冲区有关

复制积压缓冲区

复制积压缓冲区是有主服务器维护的一个固定长度(fixed-size)先进先出(FIFO)队列,默认大小为1MB。当主服务器进行命令传播时,它不仅将命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区中,如图1-10所示

图1-10   主服务器向复制积压缓冲区和所有从服务器传播写命令数据

因为,主服务器的复制积压缓冲区会保存着一部分最近传播的写命令,并且复制积压缓冲区会为队列的每个字节记录相应的复制偏移量,就像表1-4展示的那样

表1-4   复制积压缓冲区的构造
偏移量 …… 10087 10088 10089 10090 10091 10092 10093 10094 10095 10096 10097 ……
字节至 …… '*' '3' '\r' '\n' '$' '3' '\r' '\n' 'S' 'E' 'T' ……

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

  • 如果offset偏移量之后的数据(也即是偏移量offset+1开始的数据)仍然存在于复制积压缓冲区中,那么主服务器将对从服务器执行部分同步操作
  • 相反,如果offset偏移量之后的数据已经不存在于复制积压缓冲区,那么主服务器将对从服务器执行完整重同步操作

回到之前图1-9展示的断线后重连接例子:

  • 当从服务器A断线之后,它立即重新连接主服务器,并向主服务器发送PSYNC命令,报告自己的复制偏移量为10086
  • 主服务器收到从服务器发来的PSYNC命令以及偏移量10086之后,主服务器将检查偏移量10086之后的数据是否存在于复制积压缓冲中,结果发现这些数据仍然存在,于是主服务器向从服务器发送+CONTINUE回复,表示数据同步将以部分重同步模式来进行
  • 接着主服务器会将复制积压缓冲区10086偏移量之后的所有数据(偏移量为10087至10119)都发送给从服务器
  • 从服务器只要接收到这33字节的缺失数据,就可以回到与主服务器一致的状态,如图1-11所示

图1-11   主服务器向从服务器发送缺失的数据

Redis为复制积压缓冲区设置的默认大小为1MB,如果主服务器需要执行大量写命令,又或者主从服务器断线后重连接所需的时间比较长,那么这个大小也许并不合适。如果复制积压缓冲区的大小设置得不恰当,那么PSYNC命令的复制重同步模式就不能正常发挥作用,因此,正确估算和设置复制积压缓冲区的大小非常重要。复制积压缓冲区的最小大小可以根据公式second*write_size_per_second来估算:

  • 其中second为从服务器断线后重新连接上主服务器所需的平均时间(以秒计算)
  • 而write_size_per_second则是主服务器平均每秒产生的写命令数据量(协议格式的写命令的长度总和)

例如,如果主服务器平均每秒产生1 MB的写数据,而从服务器断线之后平均要5秒才能重新连接上主服务器,那么复制积压缓冲区的大小就不能低于5MB。为了安全起见,可以将复制积压缓冲区的大小设为2*second*write_size_per_second,这样可以保证绝大部分断线情况都能用部分重同步来处理。至于复制积压缓冲区大小的修改方法,可以参考配置文件中关于repl-backlog-size选项的说明

服务器运行ID

除了复制偏移量和复制积压缓冲区之外,实现部分重同步还需要用到服务器运行ID(run ID):

  • 每个Redis服务器,不论主服务器还是从服务,都会有自己的运行ID
  • 运行ID在服务器启动时自动生成,由40个随机的十六进制字符组成,例如53b9b28df8042fdc9ab5e3fcbbbabff1d5dce2b3

当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器,而从服务器则会将这个运行ID保存起来

当从服务器断线并重新连上一个主服务器时,从服务器将向当前连接的主服务器发送之前保存的运行ID:

  • 如果从服务器保存的运行ID和当前连接的主服务器的运行ID相同,那么说明从服务器断线之前复制的就是当前连接的这个主服务器,主服务器可以继续尝试执行部分重同步操作
  • 相反地,如果从服务器保存的运行ID和当前连接的主服务器的运行ID并不相同,那么说明从服务器断线之前复制的主服务器并不是当前连接的这个主服务器,主服务器将对从服务器执行完整重同步操作

Redis实现之复制(一)的更多相关文章

  1. redis 系列21 复制Replication (上)

    一.   概述 使用和配置主从复制非常简单,每次当 slave 和 master 之间的连接断开时, slave 会自动重连到 master 上,并且无论这期间 master 发生了什么, slave ...

  2. redis学习四 复制

    1,单机创建多实例 一个redis服务器安装多个redis实例,每个实例对应一个端口.默认端口是6379. 将redis.conf配置文件复制一份到另外一个文件夹下,然后修改其中的信息即可.   pi ...

  3. Redis实现之复制(二)

    PSYNC命令的实现 在Redis实现之复制(一)这一章中,我们介绍了PSYNC命令和它的工作机制,但一直没有说明PSYNC命令的参数以及返回值.现在,我们了解了运行ID.复制偏移量.复制积压缓冲区以 ...

  4. Redis的Replication(复制)

    文件并发(日志处理)--队列--Redis+Log4Net 分布式中使用Redis实现Session共享(二) http://www.cnblogs.com/stephen-liu74/archive ...

  5. Redis主备复制

    Redis 支持 Master-Slave(主从)模式,Redis Server 可以设置为另一个 Redis Server 的主机(从机),从机定期从主机拿数据.特殊的,一个从机同样可以设置为一个 ...

  6. redis配置文件之复制

    主从复制使用slaveof将Redis实例作为另一个Redis服务器的副本. 1) Redis复制是异步的,master可以配置成如果它连接的slave没有达到给定的数量,就停止接受写入.2) 如果断 ...

  7. redis 系列22 复制Replication (下)

    一. 复制环境准备 1.1 主库环境(172.168.18.201) 环境 说明 操作系统版本 CentOS  7.4.1708  IP地址 172.168.18.201 网关Gateway 172. ...

  8. 分布式Redis主备复制

    当数据落在不同节点上时,如何保证数据节点之间的一致性是非常关键的 Redis采用主备复制的方式保证一致性,所有节点中,只有一个节点为主节点(master),它对外提供写服务,然后异步的将数据复制到其他 ...

  9. Redis持久化及复制

    一.持久化的两种方式 1.RDB: RDB是在指定时间间隔内生成数据集的时间点快照(point-in-time snapshot)持久化,它是记录一段时间内的操作,一段时间内操作超过多少次就持久化.默 ...

  10. Redis高可用复制集群实现

    redis简单介绍 Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库.Redis 与其他 key - value 缓存产品有以下三个特点: 支持数据的持久化,可以将 ...

随机推荐

  1. Django的Serializers的使用

    Serializer 在这里通过一个验证用户身份的例子说明rest_framework中serializer.Serialize的使用. 编写serializer Serializer的使用不需要依赖 ...

  2. Android学习笔记1——Android开发环境配置

    一.JDK配置 Android是基于Java进行开发的,首先需要在电脑上配置JDK(Java Development Kit).在http://www.androiddevtools.cn/下载对应系 ...

  3. mail客户端POP和IMAP协议

    POP-邮局协议 mail客户端如果使用POP协议,那么线上服务器的邮件将会自动下载到客户端. IMAP-因特网消息访问协议 mail客户端如果使用IMAP协议,邮件服务器上的邮件将不会自动下载到客户 ...

  4. jQuery_3_过滤选择器

    过滤选择器(过滤器)类似于CSS3里的伪类,包含 1. 基本过滤器 2. 内容过滤器 3. 可见性过滤器 4. 子元素过滤器 5. 其他方法 一.  基本过滤器 过滤器名 jQuery语法 注释 :f ...

  5. CentOs7 修复 引导启动

    一.修复MBR: MBR(Master Boot Record主引导记录): 硬盘的0柱面.0磁头.1扇区称为主引导扇区.其中446Byte是bootloader,64Byte为Partition t ...

  6. Python 随笔之Redis

    Python学习记录 ——redis 2018-03-07 Redis是一个开源的使用ANSI C语言编写.支持网络.可基于内存亦可持久化的日志型.Key-Value数据库,并提供多种语言的API.从 ...

  7. 因技术垃圾直接上手groovy的工作感悟

    因为熟悉devops,用IDEA的git完成版本提交,不太喜欢公司的代码控制,从事groovy基本没有代码控制,提交就打个压缩包给指定的人. 没有持续继承,持续部署(CICD). http://xio ...

  8. WCF使用地址去调用服务端的方法

    前面的章节已经讲过了WCF的代码和SVC页面的分离,这里是分离后,客户端调用代码如下: try { var myBinding = new BasicHttpBinding(); var myEndp ...

  9. 【洛谷4149】[IOI2011] Race(点分治)

    点此看题面 大致题意: 给你一棵树,问长度为\(K\)的路径至少由几条边构成. 点分治 这题应该比较显然是点分治. 主要思路 与常见的点分治套路一样,由于\(K≤1000000\),因此我们可以考虑开 ...

  10. 将数据库数据添加到ListView控件中

    实现效果: 知识运用: ListView控件中的Items集合的Clear方法 //从listView控件的数据项集合中移除所有数据项 补充:可以使用Remove或RemoveAt方法从集合中移除单个 ...