面试必问的 Redis:主从复制

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 在分布式环境中,数据副本 (Replica) 和复制 (Replication) 作为提升系统可用性和读写性能的有效手段被大量应用在各种分布式系统中,Redis 也不例外。

前言

 


在分布式环境中,数据副本 (Replica) 和复制 (Replication) 作为提升系统可用性和读写性能的有效手段被大量应用在各种分布式系统中,Redis 也不例外。

 

虽说现在基本不会直接使用主从复制来作为 Redis 的高可用方案,但是无论是哨兵还是集群,都会使用到主从复制,因此,有必要先学习下主从复制的原理。

 

 

正文

 


主从复制实现原理

 

在当前最新的Redis 6.0 中,主从复制的完整过程如下:

 

1、开启主从复制

通常有以下三种方式:

1)在 slave 直接执行命令:slaveof <masterip> <masterport>

2)在 slave 配置文件中加入:slaveof <masterip> <masterport>

3)使用启动命令:--slaveof <masterip> <masterport>

 

注:在Redis 5.0 之后,slaveof 相关命令和配置已经被替换成 replicaof,例如 replicaof <masterip> <masterport>。为了兼容旧版本,通过配置的方式仍然支持 slaveof,但是通过命令的方式则不行了。

 

2、建立套接字(socket)连接

slave 将根据指定的 IP 地址和端口,向master 发起套接字(socket)连接,master 在接受(acceptslave 的套接字连接之后,为该套接字创建相应的客户端状态,此时连接建立完成。

 

3、发送PING命令

slave master 发送一个PING 命令,以检査套接字的读写状态是否正常、 master 能否正常处理命令请求。

 

如果slave 收到 "PONG" 回复,那么表示 master slave 之间的网络连接状态正常,并且 master 可以正常处理命令请求。

 

如果是其他回复或者没有回复,表示 master slave 之间的网络连接状态不佳或者 master 暂时没办法处理 slave 的命令请求,则slave 进入 error 流程:slave 断开当前的连接,之后再进行重试。

 

4、身份验证

如果master slave 都没有设置密码,则无需验证。

 

如果master slave 都设置了密码,并且密码相同,则验证成功。

 

否则,master slave 设置的密码不同、master slave 一个设置密码一个没设置密码都会返回错误。

 

所有错误情况都会令 slave 进入 error 流程:slave断开当前的连接,之后再进行重试。

 

5、发送端口信息

在身份验证通过后后, slave 将向 master 发送自己的监听端口号, master 收到后记录在 slave 所对应的客户端状态的 slave_listening_port 属性中。

 

6、发送IP地址

如果配置了slave_announce_ip,则 slave master 发送slave_announce_ip 配置的 IP 地址, master 收到后记录在slave 所对应的客户端状态的 slave_ip 属性。

 

该配置是用于解决服务器返回内网 IP 时,其他服务器无法访问的情况。可以通过该配置直接指定公网IP

 

7、发送CAPA

CAPA 全称是 capabilities,这边表示的是同步复制的能力。

 

slave 会在这一阶段发送 capa 告诉master 自己具备的(同步)复制能力, master 收到后记录在 slave 所对应的客户端状态的 slave_capa 属性。

 

CAPA 在最新的 Redis 6.0 版本中有两种值:eof psync2

 

eof 表示 slave 支持直接接收从socket 发送过来的 RDB 数据流,也就是无盘加载(diskless_load)。

 

psync2 表示 slave 支持Redis 4.0 引入的部分重同步 v2 版本,这个在下文会详细介绍。

 

8、数据同步

slave 将向 master 发送PSYNC 命令, master 收到该命令后判断是进行部分重同步还是完整重同步,然后根据策略进行数据的同步。

 

1)如果是 slave 第一次执行复制,则向 master 发送PSYNC ? -1 master 返回 +FULLRESYNC <replid> <offset> 执行完整重同步

 

2)如果不是第一次执行复制,则向 master 发送 PSYNC replid offset,其中replid master 的复制 ID,而offset slave 当前的复制偏移量。master 根据replid offset 来判断应该执行哪种同步操作。

 

如果是完整重同步,则返回 +FULLRESYNC <replid> <offset>;如果是部分重同步,则返回 +CONTINUE <replid>,此时slave 只需等待 master 将自己缺少的数据发送过来就可以。

 

9、命令传播

当完成了同步之后,就会进人命令传播阶段,这时 master 只要一直将自己执行的写命令发送给 slave ,而slave 只要一直接收并执行 master 发来的写命令,就可以保证 master slave 一直保持一致了。

 

在命令传播阶段, slave 默认会以每秒一次的频率,向 master 发送命令:REPLCONF ACK <reploff>,其中 reploff slave 当前的复制偏移量。

 

发送REPLCONF ACK 命令对于主从服务器有三个作用:

 

1)检测 master slave 的网络连接状态。

 

2)汇报自己的复制偏移量,检测命令丢失,master 会对比复制偏移量,如果发现 slave 的复制偏移量小于自己,会向 slave 发送未同步的数据。

 

3)辅助实现 min-slaves 配置,用于防止 master 在不安全的情况下执行写命令。

 

例如以下配置表示:当延迟时间小于10秒的 slave 数量小于3个,则会拒绝执行写命令。而这边的延迟时间,就是以 slave 最近一次发送 ACK 时间和当前时间作对比。

min-slaves-to-write 3min-slaves-max-lag 10

 

以部分重同步为例,主从复制的核心步骤流程图如下:

 image.png

image.png

 

相关源码在replication.c,核心方法是:replicationSetMasterconnectWithMastersyncWithMaster

 

 

旧版同步:SYNC

 

Redis 2.8 之前的数据同步通过 SYNC 命令完成,完整流程如下:

 

1slave master 发送SYNC 命令。

 

2master 收到 SYNC 命令后执行BGSAVE 命令,fork 子进程生成 RDB 文件,同时会使用一个缓冲区记录从现在开始执行的所有写命令。

 

Redis 在这边使用了 COWcopy-on-write)的特性,这边简单介绍一下。

 

fork 子进程时,一种比较愚蠢的做法是将父进程的整个地址空间拷贝一份给子进程,但这是非常耗时的,因此一般不会这么做。

 

另一种做法是,fork之后,父子进程共用父进程已有的地址空间,只有当父子进程要进行写操作时,才将要修改的内容复制一份,再进行写操作,这也是 copy-on-write 名字的由来。

 

回到本文,这边当主进程 fork 出子进程时,因为 COW 的关系,可以认为在fork 的这一刻,快照已经生成了,只是还没写到 RDB 文件。

 

那这边就有一个问题,RDB 文件是 fork 这一刻的数据,从 fork 这一刻到 master RDB 文件发送给 slave 之间,主进程还在继续执行写命令,这期间的写命令slave 怎么获得?

 

这就用到上面同时会使用一个缓冲区记录从现在开始执行的所有写命令,这个缓冲区会记录 fork 之后的所有写命令。

 

后面当master RDB 文件发送给 slave 后,master会继续将缓冲区中的写命令发送给 slave,也就是下面的第4步,从而保证slave 的数据是完整的。

 

3、当 BGSAVE 命令执行完毕,master 会将生成的RDB 文件发送给 slaveslave 接收RDB 文件,并载入到内存,将数据库状态更新至 master 执行 BGSAVE 时的数据库状态。

 

这边发送RDB 文件的方式有两种:1socketmaster RDB 文件流通过 socket 直接发送到 slave2diskmaster RDB 文件先持久化到磁盘,再发送到 slave

 

默认使用方式为disk,可以通过以下配置来使用 socket 方式。

repl-diskless-sync yes

同时,相关的参数配置还有 diskless-sync-delay:该参数表示等待一定时长再开始复制,这样可以等待多个slave 节点重新连接上来。

 

socket(无磁盘)方式适合磁盘读写速度慢但网络带宽非常高的环境。

 

另外,这边主进程检查子进程 BGSAVE 是否执行完毕是通过时间事件定时检查的。

 

4master 将记录在缓冲区里面的所有写命令发送给slaveslave 执行这些命令,将数据库状态更新至 master 当前所处的状态。

 

SYNC 存在的问题slave 每次断线重连都需要使用完整重同步,效率低下。

 

 

新版同步:SYNC

 

为了解决slave 每次断线重连都需要使用完整重同步,redis 2.8 版本引入了PSYNCPSYNC 包含完整重同步和部分重同步。

 

1、完整重同步:和 SYNC 命令基本一致。

 

2、部分重同步:slave 只需要接收和同步断线期间丢失的写命令即可,不需要进行完整重同步。

 

为了实现部分重同步,Redis 引入了复制偏移量、复制积压缓冲区和运行ID 三个概念。

 

复制偏移量(offset

 

执行主从复制的双方都会分别维护一个复制偏移量,master 每次向 slave 传播N 个字节,自己的复制偏移量就增加 N;同理 slave 接收 N 个字节,复制偏移量也增加N。通过对比主从之间的复制偏移量就可以知道主从间的同步状态。

 

 

复制积压缓冲区(replication backlog buffer

 

复制积压缓冲区是 master 维护的一个固定长度的 FIFO 队列,默认大小为 1MB

 

master 进行命令传播时,不仅将写命令发给 slave 还会同时写进复制积压缓冲区,因此 master 的复制积压缓冲区会保存一部分最近传播的写命令。

 

slave 重连上 master 时会将自己的复制偏移量通过 PSYNC 命令发给mastermaster 检查自己的复制积压缓冲区,如果发现这部分未同步的命令还在自己的复制积压缓冲区中的话就可以利用这些保存的命令进行部分同步,反之如果断线太久这部分命令已经不在复制缓冲区中了,那没办法只能进行全量同步。

 

 

运行 IDrunid

 

每个Redis server 都会有自己的运行 ID,由 40 个随机的十六进制字符组成。当 slave 初次复制master 时,master 会将自己的运行 ID 发给slave 进行保存,这样 slave 重连时再将这个运行 ID 发送给重连上的master master 会接受这个 ID 并与自身的运行ID 比较进而判断是否是同一个 master

 

 

引入这三个概念后,数据同步过程如下:

 

1slave 通过 PSYNC runid offset 命令,将正在复制的 runid offset 发送给master

 

2master 判断 runid 和自己的runid 相同,并且 offset 还在复制积压缓冲区,则进行部分重同步:通过复制积压缓冲区将slave 缺失的命令发送给 slaveslave 执行命令,将数据库状态更新至 master 所处的状态。

 

3)否则,如果 master 判断 runid 不相同,或者offset 已经不在复制积压缓冲区,则执行完整重同步。

 

PSYNC 的完整流程如下图:

 image.png

image.png

 

PSYNC 存在的问题

 

通过上述流程,我们可以看出,PSYNC 执行部分重同步需要满足两个条件:1master runid 不变;2)复制偏移量在 master 复制积压缓冲区中。一旦不满足这两个条件,则仍然需要进行完整重同步,例如以下场景。

 

1slave 重启,缓存的 master runid offset 都会丢失,slave 需进行完整重同步。

 

2redis 发生故障切换,故障切换后 master runid 发生了变化,slave需进行完整重同步。

 

slave 维护性重启、master 故障切换都是redis 运维常见场景,因此,PSYNC 的这两个问题出现概率还是非常高的。

 

相关源码在replication.c,核心方法是:syncCommandreadSyncBulkPayloadreplicationFeedSlavesbackgroundSaveDoneHandlerslaveTryPartialResynchronization

 

 

PSYNC2

 

为了解决PSYNC slave 重启和故障切换导致完整重同步的问题,Redis 4.0 版本中对 PSYNC 进行了优化,我们称为 PSYNC2

 

PSYNC2 进行了以下2个主要改动:

 

1、引入两组 replid offset 替换原来的 runid offset

 

第一组:replid master_repl_offset

对于master,表示为自己的复制 ID 和复制偏移量;

对于slave,表示为自己正在同步的 master 的复制 ID 和复制偏移量。

可以认为这一组的两个字段就是对应原来的 runid offset

 

第二组:replid2 second_repl_offset

对于master slave,都表示自己的上一个 master 的复制ID 和复制偏移量;主要用于故障切换时支持部分重同步。

 

值得注意的是,runid并不是在引入 replid 之后就不存在了。在 4.0 之前,redis使用 runid 来作为主从复制的标识,而在 4.0 后引入了replid 来作为主从复制的标识,但是,runid redis 中的功能不仅仅是作为主从复制的标识,runid 仍然有其他的功能,例如:用于作为 redis 服务器的唯一标识。

 

 

2slave 也会开启复制积压缓冲区

 

slave 开启复制积压缓冲区,主要是用于故障切换后,当某个slave 升级为 master,该 slave 仍然可以通过复制积压缓冲区继续支持部分重同步功能。

 

如果slave 不开启复制积压缓冲区,当该 slave 升级为 master 后,复制积压缓冲区是空的,就没法支持部分重同步了。

 

 

接下来,让我们看看 Redis 是如何针对 PSYNC 的两个问题来进行优化。

 

 

优化场景1slave 重启后导致完整同步

 

产生该问题的根本原因是 slave 重启后,复制 ID(运行ID复制偏移量丢失了。解决办法其实很简单,就是在关闭服务器前将这两个变量存下来即可。

 

Redis 的做法如下:slave 在正常关闭前会调用 rdbSaveInfoAuxFields 函数把当前的复制 IDreplid和复制偏移量(master_repl_offset)作为辅助字段保存到 RDB 文件中,后面该 slave 重启的时候,就可以从 RDB 文件中读取复制 ID 和复制偏移量,然后使用这两个变量来进行部分重同步。

 

 

优化场景2master 故障切换后导致完整重同步

 

产生该问题的根本原因是故障切换后出现了新的 master,而新 master 的复制ID(运行 ID)发生改变导致没法进行部分重同步。

 

在正常同步的情况下,新 master 的数据跟老 master 理论上是完全一致的,包括复制积压缓冲区的数据。

 

因此理论上slave 是可以进行部分重同步的,现在仅仅是因为复制 ID 变了而没法进行。所以,我们的目标就是想办法让新master 和其他 slave 可以串联起来。

 

master 和其他没有晋升的 slave 的共同点是故障切换前的 master 是相同的,因此很容易想到的做法是:利用故障切换前的 master 来串联新 master 和剩余slave

 

Redis 的做法如下:当节点从 slave 晋升为master 后,会将原来自己保存的第一组复制 ID 和复制偏移量(也就是老 master 的),移动到第二组复制 ID 和复制偏移量,然后将第一组复制 ID 重新生成一个新的,也就是属于自己的复制 ID

 

相当于,slave晋升为 master 后,replid 保存了自己的复制 IDreplid2 保存了老 master 的复制ID

 

这样,新master 就可以通过 replid2 来判断 slave 是否之前跟自己从是从同一个 master 复制数据,如果是的话,则尝试使用部分重同步。

 

PSYNC2 的完整流程如下,可以看出和 PSYNC 很类似,主要区别在于紫色框部分。

image.png

image.png


相关源码基本同PSYNC

 

 

主从复制的演变

 

Redis 2.* 到现在,开发人员对主从复制流程进行逐步的优化,以下是演进过程:

 

12.8 版本之前 Redis 复制采用SYNC 命令,无论是第一次复制还是断线重连后的复制都采用完整重同步,成本高。

 

22.8 ~ 4.0 之间复制采用 PSYNC 命令,主要优化了 Redis 在断线重连时候可通过 runid offset 信息使用部分重同步。

 

34.0 版本之后对 PSYNC 进行了优化,通常称为 PSYNC2,主要优化了 PSYNC slave 重启和故障切换时的完整重同步问题。

 

 

最后

 


当你的才华还撑不起你的野心的时候,你就应该静下心来学习,愿你在我这里能有所收获。

 

如果你觉得本文写的还不错,对你有帮助,请通过【点赞】让我知道,支持我写出更好的文章。

 

 

推荐阅读


面试必问的 RedisRDBAOF、混合持久化

面试必问的 Spring,你懂了吗?

如何写一份让 HR 眼前一亮的简历(附模板)

字节、美团、快手核心部门面试总结(真题解析)

面试阿里,HashMap 这一篇就够了

面试必问的 MySQL,你懂了吗?

面试必问的线程池,你懂了吗?

4 Java 经验,阿里网易拼多多面试总结、心得体会

跳槽,如何选择一家公司

如何准备好一场大厂面试

MySQL 8.0 MVCC 核心原理解析(核心源码)

921天,咸鱼到阿里的修仙之路

复习2个月拿下美团offer,我都做了些啥

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
打赏
0
0
0
0
14
分享
相关文章
《docker高级篇(大厂进阶):1.Docker复杂安装详说》包括:安装mysql主从复制、安装redis集群
《docker高级篇(大厂进阶):1.Docker复杂安装详说》包括:安装mysql主从复制、安装redis集群
136 14
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
阿里面试:亿级 redis 排行榜,如何设计?
本文由40岁老架构师尼恩撰写,针对近期读者在一线互联网企业面试中遇到的高频面试题进行系统化梳理,如使用ZSET排序统计、亿级用户排行榜设计等。文章详细介绍了Redis的四大统计(基数统计、二值统计、排序统计、聚合统计)原理和应用场景,重点讲解了Redis有序集合(Sorted Set)的使用方法和命令,以及如何设计社交点赞系统和游戏玩家排行榜。此外,还探讨了超高并发下Redis热key分治原理、亿级用户排行榜的范围分片设计、Redis Cluster集群持久化方式等内容。文章最后提供了大量面试真题和解决方案,帮助读者提升技术实力,顺利通过面试。
面试官:Redis 大 key 多 key,你要怎么拆分?
本文介绍了在Redis中处理大key和多key的几种策略,包括将大value拆分成多个key-value对、对包含大量元素的数据结构进行分桶处理、通过Hash结构减少key数量,以及如何合理拆分大Bitmap或布隆过滤器以提高效率和减少内存占用。这些方法有助于优化Redis性能,特别是在数据量庞大的场景下。
面试官:Redis 大 key 多 key,你要怎么拆分?
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试一线互联网企业时遇到了关于Redis分布式锁过期及自动续期的问题。尼恩对此进行了系统化的梳理,介绍了两种核心解决方案:一是通过增加版本号实现乐观锁,二是利用watch dog自动续期机制。后者通过后台线程定期检查锁的状态并在必要时延长锁的过期时间,确保锁不会因超时而意外释放。尼恩还分享了详细的代码实现和原理分析,帮助读者深入理解并掌握这些技术点,以便在面试中自信应对相关问题。更多技术细节和面试准备资料可在尼恩的技术文章和《尼恩Java面试宝典》中获取。
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
Redis常见面试题:ZSet底层数据结构,SDS、压缩列表ZipList、跳表SkipList
String类型底层数据结构,List类型全面解析,ZSet底层数据结构;简单动态字符串SDS、压缩列表ZipList、哈希表、跳表SkipList、整数数组IntSet
面试题:Redis如何实现分布式锁!
面试题:Redis如何实现分布式锁!
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等