使用 AOF 和 RDB,如果 redis 发生了宕机,可以分别通过回放日志和重新读入 RDB 快照来恢复数据,保证尽量少丢失数据,提升可靠性。但如果只有一个 redis 实例,这个 redis 不幸宕机了,在它恢复期间,就无法服务新来的数据存取请求。
因此,redis 提供了主从库模式,以保证数据副本的一致,主从库之间采用的是读写分离的方式。
采用读写分离的原因:如果不管主库还是从库,都能接收客户端的写操作,那么可能客户端对同一个数据前后修改了三次,每次修改的请求都发送到不同的实例上,在不同的实例上执行,那么这个数据在这三个实例上的副本就不一致了。这个时候读取这个数据,就可能读取到旧的值。如果采取加锁的方式来完成修改一致性,就会带来巨额的开销。所以采用读写分离的方式,所有数据的修改只在主库上进行,再同步给从库,这样,主从库的数据就是一致的。
当我们启动多个 redis 实例的时候,它们之间可以通过 replicaof(redis5.0之前使用slaveof)命令形成主从库的关系,之后会按照三个阶段完成数据的第一次同步。如下图所示:
第一阶段是主从库间建立连接、协商同步的工程,主要是为全量复制做准备。在这一步,主从库建立起连接,并告诉主库即将进行同步,主库确认回复后,主从库间就可以开始同步了。
从库给主库发送 psync 命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync 命令包括了主库的 runID 和复制进度 offet 两个参数。
主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset,返回给从库。从库收到响应后,会记录下这两个参数。FULLRESYNC 表示第一次复制采用全量复制,主库会把当前所有的数据复制给从库。
第二阶段主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载。这个过程依赖 RDB 快照文件。
主库执行 bgsave 命令,生成 RDB 文件,将文件发给从库。从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。这是因为从库在通过 replicaof 命令开始和主库同步前,可能保存了其他数据。为了避免之前数据的影响,从库需要先把当前数据库清空。
在主库将数据同步给从库的过程中,主库不会被阻塞,仍然可以正常接收请求。但是这些请求中的写操作并没有记录到刚刚生成的 RDB 文件。为了保证主从库的数据一致性,主库会在内存中用专门的 replication buffer,记录 RDB 文件生成后收到的所有写操作。
第三阶段,主库把第二阶段新收到的写命令,再发送给从库。当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发送给从库,从库再重新执行这些操作。这样,主从库就实现了同步。
一次全量复制,对于主库来说,需要完成两个耗时的操作:生成 RDB 文件和传输 RDB 文件。
如果从库的数量很多,都要和主库进行全量复制的话,就会导致主库忙于 fork 子进程生成 RDB 文件,进行数据全量同步。fork 这个操作会阻塞主线程处理正常的请求,从而导致主库响应请求变慢。此外,传输 RDB 文件也会占用主库的网络带宽,同样会给主库的资源带来压力。那么,有没有好的解决方法分担主库压力呢?
这就是“主 - 从 - 从”模式。
在部署主从集群的时候,手动选择一个从库(内存资源配置比较高的),然后选择一些从库,让它们和刚才所选的从库建立起主从关系。这样,这些从库就不用再和主库进行交互了,只要和级联的从库进行写操作同步就可以了。如下图所示:
一旦主从库完成了全量复制,它们之间就会一致维护一个网络连接,主库通过这个连接将后续收到的命令同步给从库,这个过程称为基于长连接的命令传播,可以避免频繁建立连接的开销。
但这个过程存在的风险点就是网络断连或阻塞。如果网络断连,主从库之间就无法进行命令传播,从库的数据自然就没办法和主库保持一致了,客户端就有可能从从库读到旧数据。
在 redis2.8 之前,主从库网络断连后,从库和主库之间会进行一次全量复制,开销非常大。
redis2.8 之后,网络断了后,主从库采用增量复制的方式继续同步。
当主从库断连后,主库会把断连期间收到的写操作命令,写入 replication buffer,同时也会把这些操作命令写入 repl_backlog_buffer 这个缓冲区。
repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。
刚开始时,主从库的写读位置在一起,随着主库不断接收新的写操作,它在缓冲区的写位置会逐步偏离起始位置,对应的偏移量就是 master_repl_offset。主库新写操作越多,这个值就会越大。同样从库在复制完写操作命令后,它在缓冲区的读位置也开始逐步偏移起始位置,对应的偏移量 slave_repl_offset。
主从库连接恢复后,从库首先给主库发送 psync 命令,并把自己当前的 slave_repl_offset 发给主库,主库判断自己的 master_repl_offset 和 salve_repl_offset 之间的差距,然后把这之间不同的操作命令发送给从库就行。
不过由于 repl_backlog_buffer 是一个环形缓冲区,在写满后继续写入的话,就会覆盖之前写入的操作。如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被新库的写操作覆盖了,这会导致主从库间的数据不一致。
要想避免这一情况,我们可以调整 repl_backlog_size 这个参数值,一般调为缓冲空间的2倍。缓冲空间计算:主库命令写入速度 * 操作大小 - 主从间网络命令传输速度 * 操作大小。比如,如果主库每秒写入 2000 个操作,每个操作大小为 2kb,网络每秒能传输 1000 个操作,那么就有 1000 个操作需要缓冲起来,这就至少需要 2MB 的缓冲空间。为了应对突来的压力,最终把 repl_backlog_size 设为 4MB。