Redis核心技术04-数据同步
# Redis核心技术04-数据同步
Redis在AOF和RDB的帮助下,即使发生宕机,也可以分别通过回放日志和重新读入RDB文件的方式来恢复数据,从而保证数据的可靠性。
但是由于在恢复的过程中比较缓慢,如果我们只有一台Redis实例,意味着在恢复期间,是无法服务新来的数据存取请求的。所以在AOF和RDB保证数据尽量少丢失
下,我们还需要使用主从库来保证服务尽量少中断
,以此来达到高可靠性。
# 主从库模式
实际上,Redis 提供了主从库模式,以保证数据副本的一致,主从库之间采用的是读写分离的方式。
- 读操作:主库、从库都可以执行。
- 写操作:首先到主库执行,然后,主库将写操作同步给从库。
为何采用读写分离的方式呢
试想一个问题,如果服务器上有三个Redis的实例(一个主库,两个从库),客户端发起写请求,对某一个key进行了三次修改,而这三次修改又是在不同实例上进行,那么这个数据在三个实例上的副本就不一致了。以后在读取这个数据的时候,就可能读取到旧的数值。
如果在上诉情况下要强行保证这个数据在三个实例上一致,就要涉及到加锁的问题,这样就会带来额外的开销,对于快速的Redis是不能接受的。
而主从库模式一旦采用了读写分离,所有数据的修改只会在主库上进行,不用协调三个实例。主库有了最新的数据后,会同步给从库,这样,主从库的数据就是一致的。
# 数据同步
对于数据同步我们要思考两个问题:
- 同步的过程是这么样的?是一次性同步,还是分批同步。
- 如果同步过程中,网络发生故障,数据还能保持一致吗?
# 如何同步
当我们启动多个 Redis 实例的时候,它们相互之间就可以通过 replicaof(Redis 5.0 之前 使用 slaveof)命令形成主库和从库的关系,之后会按照三个阶段完成数据的第一次同步。
第一阶段
第一阶段是主从库建立连接、协商同步的过程,主要是为第二阶段的全量复制
做准备。
从库给主库发送psync命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync命令包含了主库的 runID 和复制进度 offset 两个参数。
- runID:是每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的 runID,所以将 runID 设为?。
- offset:此时设为 -1,表示第一次复制。
主库收到psync命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset,返回给从库。从库收到响应后,会记录下这两个参数。
FULLRESYNC 响应表示第一次复制采用的全量复制,也就是说, 主库会把当前所有的数据都复制给从库
第二阶段
主库将所有数据同步给从库,从库收到数据后,在本地完成数据加载。
主库执行bgsave命令,生成RDB文件,然后主库将RDB文件同步给从库。从库接受到文件后,先清空现有数据,然后加载RDB文件。
为什么要清除数据
这是因为从库在和主库同步之前,可能保持了其他的数据,为了避免之前数据的影响,从库需要把当前数据库清空。
第三阶段
在主库将数据同步给从库的过程中,主库不会阻塞,仍然可以接收请求。但是由于传递给从库的是RDB文件,相当于是同步那一刻的快照,那么新请求的写操作并没有记录到刚刚生成的RDB文件中。为了保证主从库的数据一致性主库会在内存中用专门的 replication buffer,记录在 RDB 文件生成后收到的所有写操作。
在第三阶段主库会把第二阶段执行过程中新接收的写命令,在发送给从库。具体的操作是,当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。
# 主从级联模式
在主从第一次数据同步过程中要进行一次全量复制,这对于主库来说需要完成两个耗时的操作:生成RDB文件和创建RDB文件。
生成RDB文件是需要fork子进程的,由于fork这个操作会阻塞主线程处理正常请求,从而导致主库响应应用程序的请求速度变慢。
传输RDB文件是需要占用主库的网络带宽的,这也会给主库的资源使用带来压力。
为了分担主库的压力,就需要引入主-从-从模式:
主从库模式中,所有的从库都是和主库连接,所有的全量复制也都是和主库进行的。现在,我们可以通过主 - 从 - 从模式将主库生成 RDB 和传输 RDB 的压力, 以级联的方式分散到从库上。
通过这个方式,某一些从库在同步的时候,就不用再和主库进行交互了,只要和级联的从库进行写操作同步就行了,这样也减轻主库上的压力。
主从库完成了全量复制,它们之间就会一直维护一个网络连接,主库会通过这个连接将后续陆续收到的命令操作再同步给从库,这个过程也称为基于长连接的命令传播,可以避免频繁建立连接的开销。
# 主从网络断连
但是在这个连接中存在一个隐患,最常见的就是网络断连或阻塞。如果网络断连,主从库之间就无法进行命令传播了,从库的数据自然也就没办法和主库保持一致了,客户端就可能从从库读到旧数据。
在主从网络断连后,主从库会采用增量复制的方式继续同步。
- 全量复制:同步所有数据。
- 增量复制:只会把主从库网络断连期间主库收到的命令,同步给从库。
增量复制的时候,主从库之间是通过缓冲区来保持同步的。
当主从库断连后,主库会把断连期间收到的写操作命令写入 repl_backlog_buffer 这个缓冲区。
repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己 已经读到的位置。
刚开始的时候,主从库写读的位置相同。随着主库不断接受新的写操作,它的写位置就会逐步偏离起始位置。我们通常用偏移量来衡量这个偏移距离的大小,对主库来说,对应的偏移量就是 master_repl_offset。主库接收的新写操作越多,这个值就会越大。
同理,从库在复制写操作命令之后,从库的读位置也会发生偏移,从库的偏移量 slave_repl_offset 也在不断增加。正常情况下,这两个偏移量基本相等。但是在主从断连的时候,由于主库还在不断接受请的请求,因此它的偏移量仍然在增大,而从库无法进行读,所以偏移量不会动,这就会导致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_backlog_size这个参数。缓冲空间的计算公式是:缓冲空间大小 = 主库写入命令速度 * 操作大小 - 主从库间网络传输命令速度 * 操作大小。在实际应用中,考虑到可能存在一些突发的请求压力,我们通常需要把这个缓冲空间扩大一倍,即 repl_backlog_size = 缓冲空间大小 * 2,这也就是 repl_backlog_size 的最终值。