此内容参考蒋德钧老师将讲义
Redis具有高可靠性,具有两层含义:一是数据尽量少丢失,二是服务尽量少中断。AOF和RDB保证了前者,而对于后者,redis的做法是增加副本冗余,将一份数据同时保存在多份实例上。即使有一个实例出现了宕机,需要一段时间才能恢复,其他实例也可以对外提供服务,不会影响业务使用。
多实例保存同一份数据,听起来好像不错,但是,必须要考虑一个问题:这么多副本,他们之间的数据一致性如何保持?数据读写操作可以发给所有实例吗?
实际上redis提供了主从库模式,以保证数据副本的一致性,主从库之间采用的是读写分离的方式。
- 读操作:主库、从库都可以接受
- 写操作:首先到主库执行,然后,主库将写操作同步给从库。
主从库如何进行第一次同步
当我们启动多个redis实例的时候,他们相互之间就可以通过slaveof 命令形成主库和从库关系,之后会按照三个阶段完成数据的第一次同步。
例如,现有实例1(ip:172.16.19.3)和实例2(ip:172.16.19.5),在实例2上执行以下命令,实例2就变成了实例1的从库,并从实例1上复制数据
slaveof 172.16.19.3 6379
接下来,就要学习主从库间数据第一次同步的三个阶段了。
第一阶段是主从库间建立连接、协商同步的过程,主要是为全量复制做准备。在这一步,从库和主库建立连接,并告诉主库即将建立连接,主库确认回复后,主从库间就可以开始同步了。
具体来说,从库发送psync命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync命令包含了主库的runID和复制进度offset两个参数
- runID:每个redis实例启动时都会自动生成一个随机的runID,用来唯一标记这个实例。当从库和主库第一个复制时,因为不知道主库的runID,所以设置为?。
- offset,此时为-1,表示第一次复制。
主库收到psync命令后,会用fullresync 响应命令带上两个参数:主库runID和主库目前的复制进度offset,返回给从库。从库收到响应后,会记录下这两个参数。
这里有个地方需要注意,fullresync响应表示第一次复制采用的时全量复制,也就是说,主库会把当前所有的数据都复制给从库
在第二阶段,主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载。这个过程依赖于内存快照RDB。
具体来说,主库执行bgsave命令,生成RDB文件,接着将文件发给从库。从库接收到RDB文件后,会先情况当前数据库,然后加载RDB文件。这是因为从库在执行slaveof命令之前,可能保存了其他数据。为了避免之前数据的影响,从库需要先把当前数据库清空。
在主库将数据同步给从库的过程中,主库不会阻塞,仍然可以正常接收请求。否则,redis的服务就被中断了。但是,这些请求中的写操作并没有记录到刚刚生成的RDB文件中。为了保证主从库的数据一致性,主库会在内存中用专门的replication buffer,记录RDB文件生成收到的所有写操作。
最后,也就是第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成RDB文件发送后,就会把此时replocation buffer中修改操作发送给从库,从库再执行这些操作。这样一来,主从库就实现同步了。
之后的同步不出现意外,都是采用增量复制完成
主从级联模式分担全量复制时主库压力
通过分析主从库间第一次数据同步的过程,可以看见,一次增量复制中,对于主库来说,需要完成两个耗时的操作:生成RDB文件和传输RDB文件。
如果从库数量很多,而且都要和主库进行全量复制的话,就会导致主库忙于fork子进程生成RDB文件,进行数据全量同步。fork这个操作会阻塞主线程处理正常请求,从而导致主库响应应用程序的请求速度变慢。此外,传输RDB文件也会占用主库的网络带宽,同样会给主库的资源使用带来压力。那么,有没有好的解决方案可以分担主库压力呢?
其实是有的,这就是“主-从-从”模式。
在刚才介绍的主从库模式中,所有的从库都是和主库连接,所有的全量复制也都是和主库进行的。现在,可以通过主-从-从 模式将主库生成RDB和传输RDB文件的压力,以级联的方式分散到从库上。
简单来说,就是在部署主从集群的时候,可以手动选择一个从库(比如选择内存资源配置较高的从库),用于级联其他的从库。然后,可以再选择一些从库(例如三分之一的从库),在这个从库上执行如下命令,让它们和刚才所选的从库,建立起主从关系。
slaveof 所选从库ip 6379
这样一来,这些从库就会知道,在进行同步时,不用再和主库进行交互了,只要和级联的从库进行写操作同步就行了,这样可以减轻主库的压力。如下图所示:
主从库间网络断了怎么办(增量复制)
在redis2.8之前,如果主从库在命令传播时出现了网络闪断,那么,主库就会和从库重新进行一次全量复制,开销非常大。
在redis2.8之后,网络断了之后,主从库会采用增量复制的方式继续同步。
那么,增量复制时,主从库间具体是怎么保持同步的呢?这里的奥妙就在于repl_backlog_buffer这个缓存区。我们先来看下他是如何用于增量命令的同步的。
当主从库断连后,主库会把断联期间收到的写操作命令,写入replication buffer,同时也会把这些操作写入repl_backlog_buffer 这个缓存区。
repl_backlog_buffer是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己读到的位置
刚开始的时候,主库和从库的写读位置是在一起的,这算是他们的起始位置。随着主库不断接收新的写操作,它在缓冲区中的写位置会逐步偏离起始位置,我们通常用偏移量来衡量这个偏移距离的大小,对主库来说,对应的偏移量就是master_repl_offset。主库收到新的写操作越多,这个值就会越大。
同样,从库在复制完写操作命令后,他在缓冲区中的读位置也开始逐步便宜刚才的起始位置,此时,从库已复制的偏移量slave_repl_offset也在不断增加。正常情况下,这两个偏移量基本相等。
主从库的连接恢复后,从库首先会给主库发送psync,并把runID和自己当前的offset(slave_repl_offset)发送给主库,主库会判断自己的offset(master_repl_offset)和slave_repl_offset之间的差距。
- 注意 如果这里发送过去的runID 不是此时的主库ID,这就说明可能原来的主库下线,哨兵重新选举了主库,那么此时就需要进行全量复制
- 增量复制不止是网络断联才用,在进行完第一次全量复制之后,主从之间就是进行增量复制。当从节点发送的runID与主节点一致时,说明之前进行过同步,可以进行增量复制
在网络断联阶段,主库可能会收到新的写操作命令,所以,一般来说,master_repl_offset会大于slave_repl_offset。此时,主库只用把master_repl_offset和slave_repl_offset之间的命令操作同步给从库就行。
就像上面示意图中间部分,主库和从库之间相差了put d e 和 put d f两个操作,在增量复制时,主库只需要把他们同步给从库就行了。
不过需要注意的是repl_backlog_buffer是一个环形缓冲区,所以在缓冲区写满后,主库还会继续写入,此时,就会覆盖掉之前的写入的操作。如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这就会导致主从数据不一致
此时可以增大缓冲区解决问题。如果还是会出现出从offset超过缓冲区大小,那么就不能进行增量复制(这样会丢失数据),转而使用全量复制。