单机 Redis 存在单点风险问题,也就是如果 Redis 节点出现宕机,那么会导致大量请求直接到 MySQL 数据库,严重的情况下可能会让 MySQL 数据库宕机
为了解决单台 Redis 可能宕机的问题,提出了副本的概念,也就是复制多个 Redis 的副本,部署在不同的服务器上,如果其中一台宕机,有备用 Redis 可以顶上,这就是故障转移
这也是 Redis 具有高可用的基础,原 Redis 节点被称为主节点 (master) ,复制的 Redis 节点被称为从节点 (slave)。每个从节点只能有一个主节点,但每个主节点可以用多个从节点 (1 -> n)
理论情况下,主从节点中的数据应该无时无刻都具有一致性。但是复制的数据流是单向的,只能从主节点复制到从节点,所以只有主节点可以读写数据,而从节点只可以读数据,否则就会导致主从节点不一致
Redis 主从节点的关系如下图所示:
注意:如果没有任何其它的关系存在,每个节点都是主节点
建立主从复制关系,这个过程会删除从节点当前所有数据,然后对新主节点进行复制操作,使主从数据一致
# 6380 成为 6379 的从节点,换句话就是 6379 变成 6380 的主节点
127.0.0.1:6380> slaveof 127.0.0.1 6379
断开主动复制关系,这个过程会断开与主节点的复制关系,重新变回主节点
xxxxxxxxxx
# 6380 不再是 6379 的从节点,而会变回主节点
127.0.0.1:6380> slaveof on one
如果 6380 已经是某主节点的从节点,再执行slaveof 127.0.0.1 6379
命令会经历四个阶段,也就是切换主节点的流程:
断开和旧主节点的复制关系
与新主节点建立复制关系
删除从节点当前所有数据
对新主节点进行复制操作
可以通过info replication
命令查看当前节点与复制相关的信息
上一部分从客户端的角度介绍了如何配置才能让两个 Redis 节点建立主从复制关系。本部分更近一步,介绍当客户端输入slaveof ip port
命令后,Redis 节点之间会经历的过程,流程如下图:
保存主节点信息:从节点保存主节点的地址信息就直接返回,此时建立复制流程还没有开始
主从建立 socket 连接:从节点内部通过每秒运行的定时任务,发现存在新主节点后会尝试和它建立网络连接,专门用于接收主节点发送的复制命令。如果连接失败,定时任务会无限重试直至成功
发送 ping 命令:发送 ping 请求进行首次通信,检测主从之间 socket 是否可用,以及检测主节点是否可以接收处理命令
权限验证:主节点可以通过requirepass
设置密码,如果设置了该参数,需要进行密码验证
同步数据集:主从节点可以正常通信后,进行首次复制操作,主节点会把持有的数据全部发送给从节点,这部分也是耗时最长的步骤
命令持续复制:同步完成后,主节点依旧会执行客户端的写命令,为了主从一致性会持续将写命令发送给从节点
在上一部分的六个步骤中,只有同步数据集是最耗时的,因为它耗时,所以就会对它不停的改进优化,所以就变成了最麻烦的一步。下面分别从三个时间点来讨论同步方案的迭代过程
在 Redis 2.8 之前都是基于 sync 命令的全量复制,整个步骤如下:
从节点向主节点发送 sync 命令请求启动复制流程
主节点收到 sync 命令后执行 bgsave 命令生成 RDB 文件。bgsave 会调用fork()
创建一个子进程来生成 RDB 文件,不会阻塞主进程,更详细可见 RDB
主节点将生成的 RDB 文件发送给从节点
从节点收到 RDB 文件后会先清空自身旧数据,然后开始加载 RDB 文件
主节点在执行 bgsave 命令后会对接受的写命令缓存起来,然后发送给从节点,从节点执行这些写命令同步为主节点的最新状态
注意:主节点会为每一个从节点单独开辟一块 replication buffer (复制缓冲区) 来记录执行 bgsave 命令后主节点接收的写命令
从上面的步骤可以看出 sync 命令是一个非常耗费资源的操作,体现在如下方面:
主节点执行 bgsave 生成 RDB 文件,虽然创建子进程去执行,但依旧会耗费主服务器大量的 CPU、内存、磁盘 IO 资源
主节点通过网络将 RDB 发送给从节点,如果 RDB 过大,会消耗大量的网络带宽和流量
从节点收到 RDB 文件后需要加载,并且在加载期间因为阻塞无法对外提供读服务
重点:如果由于网络等原因导致主从节点断开连接,会造成从节点数据丢失。当从节点再次连接上主节点,需要重新进行上述的全量复制过程,极其耗费资源
这里抛出一个面试问题:为什么全量复制选择 RDB 而不是 AOF?这个其实是想让你比较 RDB 和 AOF 的区别,更具体点是比较优缺点,更详细可见 RDB 和 AOF 比较
可以从三个方面回答:文件大小、数据恢复速度、AOF 刷盘策略 (如果策略选择不当,会影响 Redis 正常运行,更详细可见 AOF 文件同步)
psync 命令相比于 sync 多了两个参数
xxxxxxxxxx
# runId : 节点运行 id
# offset : 复制偏移量
psync runId offset
复制偏移量:主从节点都维护了一个复制偏移量,在执行完命令后,会将命令的字节累加到复制偏移量中。主节点中同时保存了自身和从节点的复制偏移量,可以通过偏移量判断主从节点数据是否一致
不仅如此,psync 还新增了复制积压缓冲区,在主节点中保存了一个固定长度的队列,默认 1MB,当有从节点连接主节点时被创建,所有从节点共用一个复制积压缓冲区
在完成全量复制后,主节点只需要持续传播命令给从节点即可。在传播的过程中,不仅需要将命令发送给从节点,还需要加入到复制积压缓冲区,如果缓冲区满了,就遵循 FIFO 原则弹出最久入队的命令
如果主从节点断开连接导致部分数据丢失,先根据偏移量去复制积压缓冲区找,如果存在,就执行部分复制,只需要将复制积压缓冲区对应的命令发送给从节点即可,如果不存在,就执行全量复制
注意:复制缓冲区在全量复制阶段使用,每个从节点都有一个;复制积压缓冲区在命令持续传播阶段使用,所有从节点共享一个
上图的右边,如果主节点返回+fullresync {runId} {offset}
表示从节点将触发全量复制;如果主节点返回+continue
表示从节点将触发部分复制
如果主节点返回-err
表示主节点版本低于 Redis 2.8,无法识别 psync 命令,从节点将发送旧版的 sync 命令触发全量复制
根据上面的流程图可以知道有两种情况会触发全量复制,判断复制积压缓冲区是否存在相应命令以及是否为首次复制,前者比较好判断,那后者如何判断呢?
psync 的第一个参数 runId,如果没有则默认值为?
,第二个参数 offset,如果是第一次参与复制则默认值为 -1。所以如果发送命令psync ? -1
可以判断为首次复制
还有另外一种情况,如果 runId 和当前主节点 runId 不一致,表示从节点中存的数据为另外一个主节点的,也需要进行全量复制
到此为止分析了 psync 和 sync 的区别,以及触发全量复制和部分复制的情况,下面给出两种复制的流程图:
虽然 psync 相比于 sync 新增了部分复制来减少资源的消耗,但下面两种本应该可以部分复制的情况却只能全量复制:
从节点宕机或重启,runId 和 offset 全部丢失,必须全量复制
主节点宕机,故障转移使新主节点 runId 和旧主节点 runId 不一样,必须全量复制
psync2.0 针对于上一部分最后提出的两个缺陷都有了相应的解决办法,使得可以进行部分复制。针对于缺陷一,从节点生成的 RDB 文件中包含了主从复制的相关信息
对于缺陷二,psync2.0 舍弃了 runId 的概念,取而代之的是 replid 和 replid2
对于主节点来说,replid 和自己的 runId 一样。没有发现主从切换之前,replid2 为空;发生之后,新主节点的 replid2 是旧主节点的 replid
对于从节点来说,replid 是当前主节点的 replid,repli2 是旧主节点的 replid
还有两个和偏移量有关的字段
master_repl_offset:当前的复制偏移量
second_replid_offset:没有发生主从切换前,该偏移量为 -1;发生之后,新主节点的 second_replid_offset 是旧主节点的 master_repl_offset
全量复制:从节点发送的 replid 和当前主节点的 replid 不同 && (从节点发送的 replid 和当前主节点的 replid2 不同 || 从节点发送的偏移量和当前主节点的 second_replid_offset 不一致)
从节点的过期删除动作由主节点控制:
主节点删除一个过期键后,会显式的向所有从服务器发送一条 del 命令,告知从服务器删除这个键
从服务器在执行客户端的读命令时,就算遇到了过期键也不做任何处理,否则会导致主从不一致
在 Redis 3.2 之前,客户端对从节点执行读命令可能会返回过期数据;在 Redis 3.2 及之后,从节点返回数据前会先检查是否过期,如果过期就返回空值,所以不会读到过期数据
注意:在 Redis 3.2 及之后不会返回过期数据,但也不会删除过期数据,只是在返回前判断一波是否过期
当主节点宕机后,需要进行故障转移,这个过程需要人工干预,不过还好有哨兵 (Sentinel) 可以帮助自动化故障转移 (高可用)
主从复制在高并发场景下能力有限,如果数据量过大,主从复制也无法满足要求,毕竟主从都是一样的,起不到太大作用,这个时候集群 (cluster) 就出现了
Redis 主从复制中从节点可以有多个,这些从节点副本可以用于读写分离、故障转移、实时备份等场景,但在实际应用复制功能时,也存在一些坑!!
由于主节点可以执行读写命令,而从节点只能执行读命令,而且从节点时主节点的副本,所以可以实现读写分离,让读命令都去从节点上执行,而写命令都去主节点上执行,可以大大减少主节点的压力
可是使用读写分离时,也可能会遇到以下问题:
数据延迟:由于复制操作是异步,无法实时地保持主从一致,可能出现主节点写入一个数据,可是从节点中暂时读不到的情况
读到过期数据:由于 Redis 对过期键采取定时+惰性删除的策略,无法保证过期数据实时地被删除,可能读到过期数据。在 Redis 3.2 版本中,每次读到数据后都会检查数据是否过期,规避了这个问题
从节点故障问题:需要对从节点监控,当从节点故障时需要转移到其它从节点上
主从只允许持久化配置不一致,例如:对写命令并发高的场景,主节点使用 RDB,从节点使用 AOF
对于内存相关的配置必须保持一致,因为从节点是主节点的一个副本,如果内存不一致,容易导师从节点数据丢失
当对数据量比较大且流量高的主节点添加从节点时,尽量在低峰时进行操作,因为添加节点会触发第一次全量复制
复制积压缓冲区不足,会导致从节点断开重连后偏移量不在该缓冲区中,触发全量复制。建议根据系统实际情况,设置合适大小的复制积压缓冲区
单主节点复制风暴:主节点重启,从节点会发起全量复制,如果从节点过多,会导致网络带宽流量开销过大
单机器复制风暴:由于 Redis 是单线程,会在同一台机器上部署多个 Redis 实例。如果都是主节点,会导致大量从节点的复制,消耗大量的资源,建议主节点分布在不同的机器上
Redis 设计与实现
Redis 开发与运维