主从复制及新旧版本同步原理比较

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

Redis的高可靠体现在两方面,一个是数据尽可能少丢失,用AOF和RDB混用的持久化机制来实现,一个是服务尽量减少中断,也就是Redis即使宕机了也能对外提供服务,可以通过主从复制配合哨兵机制和集群来实现。

1.读写分离

其实当一个Redis实例中断后,我们最开始的想法就是直接增加Redis实例,这样一个Redis宕机后,其他实例可以对外提供服务。

但是有个问题,多个Redis实例如何保证数据一致,如果数据的读写操作可以发给所有实例的话,数据一致性要想得到保证需要很大劲:如果客户端对同一个数据先后修改了三次,每一次修改请求都发送到不同的实例上执行,那么这个数据在这三个实例上的副本就不一致了,读取这个数据的时候就有可能读到旧数据,要想保证数据一致性,就涉及到加锁,实例之间协商是否完成修改等一系列操作,就会带来巨大开销。

所以Redis的主从模式采用读写分离,也就是主库先执行写操作,然后将写操作的数据同步给从库,这样以后,主从库都可以接收读操作了。总结起来就是:

  • 主库可以执行读操作和写操作
  • 从库只能执行读操作

2.复制步骤

向从服务器发送 slaveof 命令,可以实现让一个从服务器去复制主服务器。

例如给从服务器127.0.0.1发送,会实现以下步骤

SLAVEOF 127.0.0.1 6379
复制代码

2.1 设置主服务器的地址和端口

从服务器将客户端给定的主服务器的IP地址和端口就保存在服务器的masterhost属性和masterport属性里面

而设置完之后,从服务器将向发送slaveof命令的客户端返回OK,返回后才真正开始执行

2.2 建立套接字连接

从服务器创建连向主服务器的套接字,此时从服务器可以看成主服务器的客户端,从服务器向主服务器发送命令请求的形式进行

2.3 发送 ping 命令

收到pong说明成功,他有两个作用:

  1. 检查套接字的读写状态是否正常
  2. 检查主服务器是否能正常处理命令请求

2.4 身份验证

说白了就是设置密码,如果主从服务器都没有设置密码,直接进入下一步,如果密码相同则下一步,如果不同或者一个有密码一个没有,则重新试试

2.5 发送端口信息

从服务器向主服务器发送监听端口

2.6 同步

同步之前,都是从服务器是主服务器的客户端,而同步之后,主服务器也是从服务器的客户端

如果执行的是完整重同步,那么主服务器是从服务器的客户端,才能将保存在缓冲区的写命令发送给从服务器执行

如果执行的是部分重同步,那么主服务器需要成为从服务器的客户端才能向从服务器发送保存在复制积压缓冲区里面的写命令

2.7 命令传播

完成同步之后,主从服务器就进入命令传播阶段,主服务器一直将自己执行的命令发给从服务器,而从服务器一直接收并执行主服务器发送来的写命令

3 同步原理

前面我们讲到,主从模式通过读写分离,主库执行完写操作后把写操作的数据同步给从库,那这个同步是具体如何执行的呢?主库数据一次性传给从库呢还是分批次同步呢?要是主库的网络断开连接了,数据还能保持一致吗?主从同步的原理(旧版和新版的实现)和应对网络断连风险的方案

3.1 旧版功能实现

分为两步:同步sync和命令传播command propagate

同步:从服务器将数据库状态更新至主服务器所处的状态

命令传播:主服务器状态被修改后,让主从服务器状态重新回到一致

3.1.1同步sync

从服务器向主服务器发送sync命令来完成,具体执行步骤

  1. 从服务器向主服务器发送sync命令
  2. 主服务器收到sync命令,执行bgsave命令,在fork子进程然后生成一个RDB文件,此时会有一个缓冲区记录从现在开始执行的所有写命令
  3. basave命令执行完成后,主服务器将生成的RDB文件发送给从服务器,从服务器载入RDB文件,从而实现数据库状态同步实际执行过程真实场景下,很可能主服务器执行basave命令期间主服务器依然在执行写操作,这时候这个写操作被记录在缓冲区中,在RDB完成之后将缓冲区的所有命令发送给从服务器,然后从服务器执行这些命令

3.1.2命令传播

同步完成之后,主服务器一旦被修改,两个数据库状态又不一致了,这时候采用命令传播,也就是主服务器将向从服务器发送相同命令,从服务器执行完这个命令之后,主从服务器再次回到一致状态

3.1.3缺陷

当完成主从复制后,从服务器挂掉后,从服务器经过一段时间连上主服务器后,因为这时候主从服务器的状态不再一致了,所以从服务器将向主服务器再次发送 sync 同步上一次同步完成后到这一次挂掉这段时间的命令。

但是其实主从不同步的数据只有主服务器断开后的数据,所以断开之前的数据是同步的,sync的时候多同步了上一次sync命令同步之后的命令传播的数据,随着命令的增加,sync命令就会变得越来越耗资源

PSSycn 消耗资源,没有必要的时候不要执行,尽可能减少sync命令的使用:

  1. 主服务器需要执行Bgsave命令生成RDB文件,涉及fork指令,消耗大量CPU,内存和磁盘IO
  2. 主服务器将自己生成的RDB文件发给从服务器,这个发送消耗大量带宽和流量等网络资源,并且会对主服务器响应命令请求的时间产生影响
  3. 接收到RDB文件的从服务器需要载入主服务器发来的RDB文件,并且载入期间,从服务器是阻塞的,无法处理命令请求

3.2 新版本实现

2.8以后,就采用 psync 命令代替sync执行复制时的同步操作,分为完整重同步部分重同步

完整重同步:类似sync的第一步,主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区的写命令进行同步

部分重同步:从服务器出现宕机后重连,如果条件允许,主服务器可以将连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态

实现原理

当从服务器和主服务器连接上以后,从服务器向主服务器发送 psync命令,这个命令包含了主库的runID(用来表示这个实例)、复制进度offset(-1表示第一次复制)。主服务器根据复制进度来决定对服务器执行何种同步。

  • 如果offset偏移量之后的数据仍然存在于复制积压缓冲区,说明从库在断开期间,主库有执行写操作,那么主库对从库执行部分重同步
  • 如果从库偏移量之后的数据没有存在缓冲区,就说明是第一次,那么执行完整重同步

完整例子

假设两个redis服务器作为主从机,主服务器地址为127.0.0.1:6379,从服务器的地址为127.0.0.1:12345

然后客户端向从服务器发送命令 SLAVEOF 127.0.0.1 6379,并且假设从服务器是第一次执行复制,那么从服务器和主服务器建立完连接后,从服务器向主服务器发送 PSYNC ? -1命令,请求完整重同步

主库将所有数据同步给从库,也就是利用RDB文件实现同步,主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。从库接收到RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。这是因为从库在通过 replicaof命令开始和主库同步前,可能保存了其他数据。为了避免之前数据的影响,从库需要先把当前数据库清空。完整重同步执行完成后,主库向从库返回 +FULLRESYNC nfweknfwlenflwqndq2134124324jndwnf 10086,其中nfweknfwlenflwqndq2134124324jndwnf 是主服务器的运行ID, 10086是主服务器当前的复制偏移量

完成完整重同步后,主从服务器正常运行,数据库状态一致,如果在20000的时候主从库断开了,这时候从库需要重新连接主库进行复制。因为之前已经完成过完整重同步,所以从服务器向主服务器发送 PSYNC nfweknfwlenflwqndq2134124324jndwnf 20000请求,主服务器对比运行ID ,确定运行ID相同,然后对比偏移量,发现2000之后的数据存在于复制积压缓冲区里面,所以主服务器向从服务器返回 +CONTINUE,表示执行部分重同步操作

4 主从级联模式

4.1 风险问题

上面复制过程中,全量复制有两个很耗时的操作:生成RDB文件和传输RDB文件

耗时的原因:

  • 生成RDB文件:如果从库过多,主库需要频繁fork子进程生成RDB文件,fork指令会阻塞主进程处理正常请求
  • 传输RDB文件:会占用主库的网络带宽

4.2 解决方案

上述的问题的原因是全量复制都是主库进行的,如果我们把主库的压力分散到从库,效果会好很多,所以有了 “主-从-从”的级联模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。

就是部署主从集群的时候,手动选择一个从库,用于级联其他的主库,这样,只要和级联的从库进行写操作的同步就可以了。

参考

《Redis设计与实现》

《Redis核心技术与实战》(极客时间)

mp.weixin.qq.com/s?__biz=MzI…

猜你喜欢

转载自juejin.im/post/7102313041312055327