分布式存储引擎大厂实战——Redis主从数据库如何实现数据一致性

前言

 Redis具有高可靠性,一般来说,其实指两层意思。一个数据尽量少丢失,二是服务尽量少中断。AOF和RDB保证了前者,而对于后者,Redis的做法是增加副本冗余量,将一份数据同时保存在多个不同的实例上,即使有一个实例出现了故障,需要过一段时间才能恢复,其他实例也可以对外提供服务,这样就不会影响业务使用。
  这么多实例保存同一份数据,听起来挺好。但是我们必须考虑一个问题,这么多副本,它们之间的数据如何保持一致呢?数据读写操作可以发给所有的实例吗?

读写分离

 实际上,Redis提供了主从库模式,以保证数据副本的一致,主从库之间采用的是读写分离模式。

  • 读操作:主库、从库都可以接收
  • 写操作:首先主库执行。然后主库将写操作同步给从库。
    在这里插入图片描述

 那么,为什么要采用读写分离的方式呢?
 如果不管是主库还是从库,都能接收客户端的写操作,那么,就会出现如果客户端对同一个数据(例如k1)前后修改了三次,每一次的修改请求都发送到不同的实例上,在不同的实例上执行。那么,这个数据在这三个实例上的副本就会不一致了(分别是v1,v2和v3)。这样就会造成读取的时候读取到旧的值。
 那么,如果非常保持这个数据在三个实例上一致就要涉及到加锁操作了,实例协商是否修改等操作,这样会造成巨大的开销,也是不能接受的。
  而主从库模式一旦采用了读写分离,所有数据的修改只会在主库上进行,不用再协调三个实例。主库有了最新的数据后会同步从库,这样主从库的数据就保持一致了。

主从库全量复制

 那么,主从数据是一次性传给从库还是分批同步的呢?要是主从库间的网络连接断了,数据还能保证一致吗?下面就来聊聊这两个问题。
 当启动多个Redis实例的时候,它们之间就可以通过replicaof命令形成主库和从库的关系。举个例子,比如现在有实例1(ip:192.168.1.3)和实例2(ip:192.168.1.5),我们在实例2上执行以下命令后,实例2就变成了实例1的从库。
replicaof 192.168.1.3 6379
  接下来我们来看看主从库间数据第一次同步的三个阶段了。请看下图
在这里插入图片描述

第一阶段:主库建立连接

  第一阶段主从库建立连接,协商同步的过程。主要是为全量复制做准备。主库和从库建立连接并告诉主库即将进行同步,主库确认回复后,主从库间开始同步。

  • 从库会给主库发送prsync命令,表示要进行同步,主库根据这个命令参数来启动复制。其中prsync包含了主库的runID和复制进度offset两个参数。runID表示每个实例启动都会自动生成一个随机ID,用来唯一标记这个实例。当主库和从库第一次复制时,因为不知道主库的ID,所以将runID设为“?",offset设为-1,表示第一次复制。
  • 主库收到pync命令后,会用FULLRSYNCNC响应来表示第一次复制是全量复制。 这个时候,从库会复制主库的所有数据。

第二阶段:主库同步所有数据给从库

 这一阶段主库将所有数据同步给从库,从库加载收到的数据,同步的数据主要是根据快照文件RDB。
 主库执行bgsave命令生成RDB文件,然后将该文件发送给从库。从库接收到文件会先清空字的数据库后,再加载RDB文件。这里清空的目的是为了怕之前数据的影响。

第三阶段:主从同步replication buffer数据

  在这里,主库并不是阻塞的同步数据,而是仍然可以正常接受客户端的请求。由于这些数据有可能不在同步的RDB文件里面,为了保证主从数据的一致,主库会有专门的replication buffer用于记录RDB文件生成后收到的写操作。
  因此最后一步,主库会把第二步replication buffer里面的写操作命令发送从库再执行一篇,这样主从就实现了同步。

如何分担主库的全量复制压力

 上一节的分析可以看到,主从全量复制的时候,主库有两步操作,第一步是生成RDB文件,第二步是将RDB文件发送给从库。这里有一个问题,如果从库数量较多又都要和主库进行全量复制的话,就会导致主库一直忙于fork子进程来不断生成RDB文件以及传输RDB文件给从库。fork操作会阻塞主线程处理请求。从而导致主库响应请求速度变慢,此外传输RDB文件本身也会造成大量占用主库的网络带宽。那么该如何分担主库的压力呢?
  这就要采用“主-从-从”模式了,这个模式将主库的压力以级联的方式分散到从库上。
在这里插入图片描述

  什么意思呢?其实简单来说就是手动选择一个从库来级联其它从库。然后再选择一些从库, 执行如下命令让它们同我们之前手动选择的从库建立起主从关系。

replicaof  所选从库的IP 6379

 这些从库不再需要和主库建立关系,只和级联的从库建立关系就行。从而减轻主库的压力。一旦主从库完成了全量同步,它们之间就会一直维护一个长连接。主库会基于这个连接将后续收到的命令继续同步给从库。这个过程叫做基于长连接的命令传播,  建立长连接的好处其实就是避免了建立断开连接的开销。那么问题又来了,如果网络断开了,主从之间的无法进行命令传播了,主从数据这个时候就无法保证一致了。怎么办?

主从库增量复制

 这个问题要根据Redis的版本来分析,在2.8之间,主从之间网络断开,从库和主库就会重新进行一次全量复制,这样的问题是会造成开销非常大。
  从 Redis 2.8 开始,网络断了之后,主从库会采用增量复制的方式继续同步。它只会把主从库网络断开期间,主库收到的命令同步给从库。
  增量复制之间,主从库之间保持同步是基于repl_backlog_buffer这个缓冲区。它是为了从库断开之后,如何找到主从差异数据而设计的缓冲区。所有从库共享这个缓冲区。repl_backlog_buffer是一个环形缓冲区,主库会记录自己写入的位置,从库则会记录自己已经读到的位置。
  开始的时候,主库的写位置和从库的读位置在一起。随着主库不断接受新的写操作,不断偏离起始位置。对主库来说,对应的偏移量用 master_repl_offset表示。随着主库收到的写操作不断增多,这个偏移量也不不断增大。
  同样,从库复制完写操作命令后,它在缓冲区的读位置也开始偏离起始位置。一般这个偏移量用 slave_repl_offset表示。正常情况下这两个偏移量应该基本相等。
  主从库的连接恢复后,从库首先会向主库发送 psync 命令,并把自己当前的 slave_repl_offset 发给主库。主库会判断自己的 master_repl_offset 和收到的 slave_repl_offset 之间的差距。
  一般来说,由于断开网络的时候,由于主库还会继续收到新的写操作,所以master_repl_offset的值应该比slave_repl_offset大。
  在这里有一点要注意,如果主从库断开太久,由于repl_backlog_buffer是一个环形缓存区,因此如果缓存区写满了,当主库仍然接受写操作,主库还是会继续写缓冲区。如果从库读取的速度比较慢将导致新的写操作覆盖了从库还未读取的操作,在成主从库数据不一致。
在这里插入图片描述

repl_blacklog_size该如何设置

  为了上述问题。我们需要设置另一参数repl_blacklog_size。它用来设置环形缓冲区大小。缓存区大小公式如下:缓冲空间大小 = 主库写入命令速度 * 操作大小 - 主从库间网络传输命令速度 * 操作大小。但是考虑突发情况,在实际应用中一般最好把缓存区空间扩大。举个例子,比如主库每秒写入3000个操作,每个操作大小4KB。网络每秒能传输1000个操作,那么就有1000个操作需要缓存起来。这就需要4MB的缓冲区。否则会造成新写的操作覆盖掉旧的。为了应对有些突发情况,最终我们还是把缓存区设置为8MB。
  如果并发请求量非常大,两倍的缓冲空间都存不下新操作请求的话,主从库数据仍然可能会不一致。这个时候可以根据 Redis 所在服务器的内存资源再适当增加 repl_backlog_size 值,比如说设置成原缓冲空间大小的 4倍,此外,还以考虑使用切片集群来分担单个主库的请求压力。

总结

  全量复制虽然耗时,但是对于从库来说如果是第一次同步,只有通过全量复制模式。建议实例数据最好不要太大。这样的好处是可以减少RDB文件的生成、传输和从库重新加载的开销。为了减少主库的压力,可以采用“主-从-从”的级联模式。主从库正常运行后的常规同步时通过基于长连接的命令传播。
  这期间如果遇到了网络断开,增量复制就派上用场了。这时得特别 repl_backlog_size 这个配置参数。如果它配置得过小,在增量复制阶段,会造成被主库的写命令覆盖了,那么从库连上主库后只能乖乖地进行一次全量同步,进而导致从库重新进行全量复制。所以,通过调大这个参数,可以减少从库在网络断开时全量复制的风险。

Guess you like

Origin blog.csdn.net/songguangfan/article/details/117216010