主从复制概述
有了AOF和RDB,如果Redis发生了宕机,它们可以分别通过回放日志和重新读入RDB文件的方式恢复数据,从而保证尽量少丢失数据,提升可靠性。但是如果Redis实例宕机了,就无法提供服务了。
既然⼀台宕机了⽆法提供服务,那多台呢?是不是就可以解决了。Redis 提供了主从模式,通过主从复制,将数据冗余⼀份复制到其他 Redis 服务器。
前者称为主节点 (master),后者称为从节点 (slave);数据的复制是单向的,只能由主节点到从节点。
默认情况下,每台 Redis 服务器都是主节点;且⼀个主节点可以有多个从节点 (或没有从节点),但⼀个从节点只能有⼀个主节点。
Redis读写分离
多实例保存同一份数据,这么多副本,它们之间的数据如何保持一致呢?数据读写操作可以发给所有的实例吗?
实际上,Redis提供了主从库模式,以保证数据副本的一致,主从库之间采用的是读写分离的方式。
-
读操作:主库、从库都可以接收;
-
写操作:首先到主库执行,然后,主库将写操作同步给从库。
我们可以假设主从库都可以执⾏写指令,假如对同⼀份数据分别修改了多次,每次修改发送到不同的主从实例上,就导致是实例的副本数据不⼀致了。如果为了保证数据⼀致,Redis 需要加锁,协调多个实例的修改,Redis 自然不能这么设计。
怎么搭建主从复制架构
可以通过 replicaof(Redis 5.0 之前使⽤ slaveof)命令形成主库和从库的关系。
在从节点开启主从复制,有 3 种⽅式:
-
配置⽂件在从服务器的配置⽂件中加⼊ replicaof <masterip> <masterport>
-
启动命令redis-server 启动命令后⾯加⼊ --replicaof <masterip> <masterport>
-
客户端命令启动多个 Redis 实例后,直接通过客户端执⾏命令: replicaof <masterip> <masterport> ,则该 Redis 实例成为从节点。⽐如假设现在有实例 1(171.18.88.1)、实例 2(171.18.88.2)和实例 3(171.18.88.3),在实例 2 和实例 3 上分别执⾏以下命令,实例 2 和 实例 3 就成为了实例 1 的从库,实例 1 成为 Master。
主从复制原理
复制分为三种情况:1. 第⼀次主从库全量复制;2. 主从正常运⾏期间的同步;3. 主从库间⽹络断开重连同步。
第⼀次主从库全量复制
主从库第⼀次复制过程⼤体可以分为 3 个阶段:连接建⽴阶段(即准备阶段)、主库同步数据到从库阶段、发送同步期间新写命令到从库阶段;
建立连接
该阶段的主要作⽤是在主从节点之间建⽴连接,为数据全量同步做好准备。从库会和主库建⽴连接,从库执⾏replicaof 并发送 psync 命令并告诉主库即将进⾏同步,主库确认回复后,主从库间就开始同步了。
从节点内部维护了两个字段,masterhost 和 masterport,⽤于存储主节点的 IP 和port 信息。
从库给主库发送psync命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync命令包含了主库的runID和复制进度offset两个参数。
-
runID,是每个Redis实例启动时都会自动生成的一个随机ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的runID,所以将runID设为“?”。
-
offset,此时设为-1,表示第一次复制,全量复制。
主库收到 psync 命令后,会⽤ FULLRESYNC 响应命令带上两个参数:主库 runID 和主库⽬前的复制进度offset,返回给从库。从库收到响应后,会记录下这两个参数。FULLRESYNC 响应表示第⼀次复制采⽤的全量复制,主库会把当前所有的数据都复制给从库。
主库同步数据到从库
主库执行bgsave命令,生成RDB文件,然后将文件发给从库。从库接收到RDB文件后,会先清空当前数据库,然后加载RDB文件。这是因为从库在通过replicaof命令开始和主库同步前,可能保存了其他redis实例的数据。为了避免之前数据的影响,从库需要先把当前数据库清空。
为了保证主从库的数据一致性和主库的写操作不会阻塞,主库会在内存中用专门的replication buffer,记录RDB文件生成后收到的所有写操作。
replication buffer
在 master 端上创建的缓冲区,存放的数据是下⾯三个时间内所有的 master 数据写操作。
-
主库执⾏ bgsave 产⽣ RDB 的期间的写操作;
-
主库发送 rdb 到 从库⽹络传输期间的写操作;
-
从库加载rdb⽂件把数据恢复到内存的期间的写操作。
Redis 和客户端通信也好,和从库通信也好,Redis 都分配⼀个内存 buffer 进⾏数据交互,客户端就是⼀个client,从库也是⼀个 client,我们每个 client 连上Redis 后,Redis 都会分配⼀个专有 client buffer,所有数据交互都是通过这个 buffer 进⾏的。
Master 先把数据写到这个 buffer 中,然后再通过⽹络发送出去,这样就完成了数据交互。不管是主从在增量同步还是全量同步时,master 会为其分配⼀个 buffer ,只不过这个buffer 专⻔⽤来传播写命令到从库,保证主从数据⼀致,我们通常把它叫做replication buffer。
发送同步期间新写命令到从库
从节点加载 RDB 完成后,主节点将 replication buffer 缓冲区的数据发送到从节点,从节点接收并执⾏,从节点同步⾄主节点相同的状态。
主从正常运⾏期间的同步
当主从库完成了全量复制,它们之间就会⼀直维护⼀个⽹络连接,主库会通过这个连接将后续陆续收到的命令操作再同步给从库,这个过程也称为基于⻓连接的命令传播,使⽤⻓连接的⽬的就是避免频繁建⽴连接导致的开销。
在命令传播阶段,除了发送写命令,主从节点还维持着⼼跳机制:PING 和REPLCONF ACK。
主->从:PING每隔指定的时间,主节点会向从节点发送 PING 命令,这个 PING 命令的作⽤,主要是为了让从节点进⾏超时判断。
从->主:REPLCONF ACK在命令传播阶段,从服务器默认会以每秒⼀次的频率,向主服务器发送命令:REPLCONF ACK <replication_offset>
其中 replication_offset 是从服务器当前的复制偏移量。发送 REPLCONF ACK 命令对于主从服务器有三个作⽤:
-
检测主从服务器的⽹络连接状态。
-
辅助实现 min-slaves 选项。
-
检测命令丢失, 从节点发送了⾃身的 slave_replication_offset,主节点会⽤⾃⼰的master_replication_offset
对⽐,如果从节点数据缺失,主节点会从 repl_backlog_buffer 缓冲区中找到并推送缺失的数据。注意,offset 和 repl_backlog_buffer 缓冲区,不仅可以⽤于部分复制,也可以⽤于处理命令丢失等情形;区别在于前者是在断线重连后进⾏的,⽽后者是在主从节点没有断线的情况下进⾏的。
如何确定执⾏全量同步还是部分同步?
-
从节点从未执行过 replicaof,则从节点发送 psync ?-1,向主节点发送全量复制请求;
-
从节点之前执行过 replicaof 则发送 psync<runID><offset>,runlD 是上次复制保存的主节
主服务器判断可以执行部分重同步操作时向从服务器返回+CONTINUE;需要执行完整重同步操作时向从服务器返回“+FULLRESYNC RUN_ID OFFSET”,其中RUN_ID为主服务器的运行ID,OFFSET为复制偏移量。
可以看到,执行部分重同步操作的要求还是比较严格的。
-
RUN_ID必须相等。
-
复制偏移量必须包含在复制缓冲区中。
⼀个从库如果和主库断连时间过⻓,造成它在主库 repl_backlog_buffer 的slave_repl_offset 位置上的数据已经被覆盖掉了,此时从库和主库间将进⾏全量复制。
复制偏移量
执行复制的双方——主服务器和从服务器会分别维护一个复制偏移量:
-
主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N。
-
从服务器每次收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量的值加上N。
如下图所示,主从服务器的复制偏移量的值都为888。
如果这时主服务器向三个从服务器传播长度为33字节的数据,那么主服务器的复制偏移量将更新为888+33=921,而三个从服务器在接收到主服务器传播的数据之后,也会将复制偏移量更新为921,如下图所示。
通过对比主从服务器的复制偏移量,程序可以很容易地知道主从服务器是否处于一致状态:
-
如果主从服务器处于一致状态,那么主从服务器两者的偏移量总是相同的。
-
相反,如果主从服务器两者的偏移量并不相同,那么说明主从服务器并未处于一致状态。
主从服务器当前的复制偏移量都为888,但是就在主服务器要向从服务器传播长度为33字节的数据之前,从服务器A断线了,那么主服务器传播的数据将只有从服务器B和从服务器C能收到,在这之后,主服务器、从服务器B和从服务器C三个服务器的复制偏移量都将更新为921,而断线的从服务器A的复制偏移量仍然停留在888,这说明从服务器A与主服务器并不一致,如下图所示。
假设从服务器A在断线之后就立即重新连接主服务器,并且成功,那么接下来,从服务器将向主服务器发送PSYNC命令,报告从服务器A当前的复制偏移量为888,那么这时,主服务器应该对从服务器执行完整重同步还是部分重同步呢?如果执行部分重同步的话,主服务器又如何补偿从服务器A在断线期间丢失的那部分数据呢?这个和复制积压缓冲区有关。
复制积压缓冲区
复制积压缓冲区是由主服务器维护的一个固定长度(fixed-size)先进先出(FIFO)队列,默认大小为1MB。
固定长度先进先出队列的入队和出队规则跟普通的先进先出队列一样:新元素从一边进入队列,而旧元素从另一边弹出队列。
固定长度先进先出队列的长度是固定的,当入队元素的数量大于队列长度时,最先入队的元素会被弹出,而新元素会被放入队列。
举个例子,如果我们要将'h'、'e'、'l'、'l'、'o'五个字符放进一个长度为3的固定长度先进先出队列里面,那么'h'、'e'、'l'三个字符将首先被放入队列:
['h','e','l']
但是当后一个'l'字符要进入队列时,队首的'h'字符将被弹出,队列变成:
['e','l','l']
接着,'o'的入队会引起'e'的出队,队列变成:
['l','l','o']
以上就是固定长度先进先出队列的运作方式。
当从服务器重新连上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务器,主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作:
-
如果offset偏移量之后的数据(也即是偏移量offset+1开始的数据)仍然存在于复制积压缓冲区里面,那么主服务器将对从服务器执行部分重同步操作。
-
相反,如果offset偏移量之后的数据已经不存在于复制积压缓冲区,那么主服务器将对从服务器执行完整重同步操作。
刚才断线重连的例子:
-
当从服务器A断线之后,它立即重新连接主服务器,并向主服务器发送PSYNC命令,报告自己的复制偏移量为888。
-
主服务器收到从服务器发来的PSYNC命令以及偏移量888之后,主服务器将检查偏移量888之后的数据是否存在于复制积压缓冲区里面,结果发现这些数据仍然存在,于是主服务器向从服务器发送+CONTINUE回复,表示数据同步将以部分重同步模式来进行。
-
接着主服务器会将复制积压缓冲区888偏移量之后的所有数据都发送给从服务器。
如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致。
一般而言,我们可以调整repl_backlog_size这个参数。这个参数和所需的缓冲空间大小有关。
缓冲空间的计算公式是:缓冲空间大小 = 主库写入命令速度 * 操作大小 - 主从库间网络传输命令速度 * 操作大小。
在实际应用中,考虑到可能存在一些突发的请求压力,我们通常需要把这个缓冲空间扩大一倍,即repl_backlog_size = 缓冲空间大小 * 2,这也就是repl_backlog_size的最终值。
举个例子,如果主库每秒写入2000个操作,每个操作的大小为2KB,网络每秒能传输1000个操作,那么,有1000个操作需要缓冲起来,这就至少需要2MB的缓冲空间。否则,新写的命令就会覆盖掉旧操作了。为了应对可能的突发压力,我们最终把repl_backlog_size设为4MB。
读写分离问题
数据过期问题
主从复制的场景下,从节点会删除过期数据么
为了主从节点的数据⼀致性,从节点不会主动删除数据。
Redis 有两种删除策略:
-
惰性删除:当客户端查询对应的数据时,Redis 判断该数据是否过期,过期则删除。
-
定期删除:Redis 通过定时任务删除过期数据。
客户端通过从节点读取数据会不会读取到过期数据
Redis 3.2 开始,通过从节点读取数据时,先判断数据是否已过期。如果过期则不返回客户端,并且删除数据。