Redis 以其卓越的并发能力著称,单节点通常能够应对数万级别的并发请求,这在多数企业应用场景中已能发挥重要作用。然而,对于那些拥有庞大用户群体且并发量极高的应用,如大型电商平台的促销活动、热门社交媒体的高峰时段流量等,单节点的并发能力就显得捉襟见肘。
1.Redis主从
1.1 主从集群结构
在这类场景下,Redis 主从集群应运而生。由于 Redis 数据访问模式通常呈现读多写少的特点,主从集群通过实现读写分离,有效地提高了读操作的并发能力。其基本结构主要包含一个 master 节点与多个 slave 节点。其中,master 节点作为主节点,承担主要的数据写操作职责;而 slave 节点则作为从节点,专注于处理读操作。这种分工协作的模式使得大量的读请求能够分散到多个从节点上并行处理,从而显著提升了整个 Redis 集群的读并发能力。
值得注意的是,为了确保从节点能够正确地处理读请求,主节点会将数据同步至从节点,使得每个从节点都拥有与主节点一致的数据副本。这样,无论客户端从哪个从节点读取数据,都能获取到最新且完整的数据信息,保证了数据的一致性与可用性。
1.2 主从同步原理
在了解了主从集群的结构之后,我们继续深入探究下 Redis 主从同步的原理,包括其同步时机、基本流程以及涉及的诸多关键细节。
1.2.1 基本流程
通常情况下,当主从节点第一次建立关系并实现同步连接时,或者在已经连接好但因某些原因断开后又进行重连时,从节点都会主动发送一个 PSYNC 请求,尝试进行数据同步。
我们通过时序图来详细讲解这个过程,以左边表示 master 节点,右边表示 slave 节点为例。
当 slave 节点主动发送 PSYNC 请求后,master 节点需要先对 slave 的情况进行判断。因为 slave 可能是初次来同步,也可能是之前同步过但因断开重连的情况。
全量同步:若判断 slave 确实是第一次来同步,即同步条件成立,此时 master 会将自身所有的数据发送给 slave。这是因为初次同步意味着 slave 从未获取过 master 的数据,所以需要发送全部数据以实现同步。这种将所有数据进行同步的方式,我们称之为全量同步。
增量同步:反之,若判断条件不成立,表明 slave 并非第一次来同步,而是之前同步过但因某些原因断开连接了。在断开期间,master 仍在接收新的数据,而这些数据 slave 并未获取到。此时,master 无需发送全部数据,只需将 slave 断开期间所缺少的那部分数据发送给它即可,这种只同步部分数据的方式称为部分同步或增量同步。
无论全量同步还是增量同步,其最终目的都是让 master 和 slave 之间的数据保持一致。在完成初次同步或增量同步后,master 会持续接收新的增删改写操作命令,并且在执行这些命令的同时,会将命令传播给所有的 slave,slave 也会执行相同命令。由于命令传播速度极快,通常在毫秒级别,所以在实际应用中,我们会感觉仿佛 master 刚写完一个数据,在 slave 节点查看时就已经有了,呈现出一种实时同步的效果。
1.2.2 主从同步关键细节剖析
如何判断 slave 是否第一次来同步
这一判断依据的是 Redis 节点上的一个重要属性 ——replication ID。在 Redis 当中,每个 master 节点都拥有一个唯一 ID,即不同节点之间的 ID 各不相同。在未建立集群关系时,每个节点都认为自己是 master,都有各自的 ID,我们将其简称为 RIP ID。
不过有意思的是,当在第一次建立主从关系时,master 会生成一个全新的 replication ID,并将此 ID 分享给每个 slave,相当于大家约定好了一个 “暗号”,以此来表明彼此属于同一集群。
所以,当 slave 尝试变成从节点并发送同步请求时,不仅要发送请求,还需带上自身原本的 RIP ID。master 通过比较 slave 的 RIP ID 与自身的 ID,若两者不一致,就证明 slave 确实是第一次来同步,此时 master 便会进行全量同步操作;若两者相同,则说明 slave 是断开重连的情况,进而会进行增量同步操作。
全量同步的数据传输方式
若判断 slave 是第一次来同步,master 需要将自身所有数据发送给 slave,具体操作如下:
首先,master 要执行一个 BG SAVE 命令。BG SAVE 是一个后台执行的命令,当它执行时,会开启一个独立进程,将 Redis 在内存中的所有数据持久化到硬盘中,生成一个 RDB 文件。也就是说,通过这个命令,master 把内存中的所有数据都写入到了 RDB 文件里。
接下来,master 只需将整个 RDB 文件发送给 slave。而 slave 在接收到 RDB 文件后,需要先将本地自己的数据清理掉,然后再把 RDB 文件的数据加载到内存中,这样一来,master 和 slave 之间的数据就完全一致了,从而实现了全量同步。
增量同步时如何确定 slave 缺失的数据
在增量同步过程中,关键在于 master 要知道 slave 缺失了哪些数据,也就是错过了哪些命令。正常情况下,master 与 slave 完成第一次全量同步后,两者数据完全一致,master 在执行各种增删改命令时,会将命令传播给 slave,slave 也执行相同命令,双方数据始终保持一致。
然而,当 slave 出现网络故障与 master 断开连接或直接当机时,master 仍在接收并执行新的命令,但 slave 无法接收这些命令,导致两者之间出现数据差异。当 slave 网络恢复重连后,它会向 master 询问能否把断开期间的那些命令重新发一份。
但问题是,master 在执行命令时,通常只负责将命令通过网络往外发,并不关心 slave 是否收到,所以当 slave 当机未收到命令时,这些命令在 master 这里也没有额外的备份,似乎无法找到曾经错过的命令。
实际上,Redis 对此有相应的设计。在 master 节点当中,存在一个名为 replication backlog 的内存缓冲区,它的作用就是记录所有 master 执行过的命令,实现一种备份效果。
那么这个缓冲区从何时开始记录命令呢?由于 master 无法得知 slave 何时会当机,所以要做好最坏的打算。最坏的情况就是在刚建立完同步后,slave 立马就挂了,后续命令全都没收到。因此,replication backlog 要在第一次建立同步的那一刻就立刻创建,并记录从此之后 master 收到的所有增删改命令。这样一来,无论 slave 在建立同步后的任意时刻断开重连,都能在这个缓存区里找到对应的命令。
同时,为了更精准地确定 slave 缺失的命令,Redis 为每个节点添加了一个新的属性 ——offset。在 master 节点上,offset 代表的含义是 replication backlog 里边写入过的数据长度,即记录了多少命令。master 每接收到一个增删改命令并将其记到 replication backlog 时,offset 值就会增加。
而从节点的 offset 含义有所不同,它是在接收到 master 传过来的命令后,每执行一条,其 offset 值就会不断增大,代表的是 slave 到底同步了主节点传过来的多少命令。
在理想情况下,master 每往 replication backlog 里记录一条命令,其 offset 会增加,同时这条命令又传给了 slave,slave 的 offset 也会增加,最终 slave 的 offset 与 master 的 offset 保持一致,意味着两者的数据完全一致。
但当主从之间的网络出现故障或 slave 直接当机时,master 依然在接收新的命令并记录到 replication backlog,此时 master 的 offset 值会持续增大,而 slave 由于无法处理这些命令,其 offset 不再变化,两者的 offset 就出现了差异。这相差的部分就是 slave 当机时错过的命令,也就是需要进行增量同步的命令。
所以,在进行增量同步时,slave 除了在发送 PSYNC 请求时要携带 replication ID,还应带上自己的 offset 值。master 通过比较 slave 的 offset 与自身的 replication backlog 的 offset,就能确定 slave 缺失了哪部分命令,进而将这些命令发给 slave,实现增量同步。
综上所述,Redis 主从同步的核心在于 replication ID 以及 offset 这两个关键要素。基于 replication ID 可以判断 slave 是否第一次来同步,若是则进行全量同步,否则进行增量同步。全量同步利用 BG SAVE 命令生成 RDB 文件并发送给 slave;增量同步则通过比较 slave 的 offset 与 master 的缓冲区的 offset,找到 slave 错过的命令并发送给 slave,从而实现主从数据的持续一致。
1.2.3 主从同步优化策略
replication backlog 缓冲区特性与潜在问题
replication backlog 作为记录 master 写操作命令的缓冲区,其默认大小仅为 1M。随着时间推移,若不加以限制,写入的命令数量会不断增多。它采用环形数组的结构,类似首尾相接的蛇,数据从数组起始位置开始写入,写满后会继续从起始位置覆盖旧数据进行写入。
在正常情况下,主从节点的 offset 会随着命令的执行和传播而动态变化,slave 的 offset 逐渐追赶 master 的 offset,最终保持一致。此时,即使缓冲区被写满,只要 slave 一直在同步,覆盖的数据是 slave 已经同步过的部分,就不会影响增量同步。
然而,当 slave 出现网络故障或宕机,且短时间内无法恢复时,问题就会出现。master 会继续接收并记录新命令,导致缓冲区不断被写入新数据并可能覆盖旧数据。若覆盖的数据超过了 slave 的 offset 位置,不仅 slave 的 offset 在环形数组中找不到了,而且尚未同步的部分数据也会被覆盖。当 slave 恢复后,由于无法确定与 master 的 offset 差距,就无法实现增量同步,此时只能进行全量同步。
全量同步的性能问题
全量同步需要将内存中的所有数据先写入磁盘,再通过网络传输给 slave,涉及大量的磁盘 I/O 操作。如果 Redis 内存中有十几 G 的数据,全量同步所需的时间会很长,性能较差。相比之下,增量同步只需发送少量未执行的命令,速度会快很多。因此,我们更希望能尽可能采用增量同步,避免全量同步带来的性能损耗。
可以从以下几个方面来优化Redis主从就集群:
在master中配置
repl-diskless-sync yes
启用无磁盘复制,避免全量同步时的磁盘IO。Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO
适当提高
repl_baklog
的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步限制一个master上的slave节点数量,如果实在是太多slave,则可以采用
主-从-从
链式结构,减少master压力
主-从-从
架构图:
1.3 Redis哨兵机制
主从结构中master节点的作用非常重要,slave节点随便挂,master始终都会同步给它,但master一旦故障就会导致集群不可用。那么有什么办法能保证主从集群的高可用性呢?为解决这一问题,Redis 引入了哨兵(Sentinel)机制。本节我们就来深入学习哨兵的工作原理,探究它是如何应对主节点故障,实现主从集群自动故障恢复的。
1.3.1 哨兵的三大作用
Redis 提供的哨兵,英文名为 Sentinel,其核心作用是实现主从集群的自动故障恢复,具体包含以下三个方面:
监控
哨兵作为独立于主从集群之外的服务,通常会形成一个集群(至少三个节点),用于监控主从集群中每个节点的状态。通过这种方式,它能够实时感知是否有节点出现故障。
自动故障切换
当哨兵监控到主节点宕机等故障情况时,为避免集群因无主节点而无法处理写操作,它会立即采取行动。由于主从节点在故障前一直进行数据同步,从节点数据与主节点数据基本一致,所以哨兵会从从节点中选择一个提升为新的主节点。这样一来,集群内依然保持主从角色,能够继续对外接收读写操作,实现了从故障状态的无缝切换。当原来宕机的主节点恢复后,它会认新的主节点为 master,自身转变为从节点,完成主从角色的转换。
通知
主从集群是读写分离的,客户端在访问时会根据读写操作分别访问主节点和从节点。但在发生故障转移后,主从角色发生了变化,如果客户端仍将写操作发给原来的主节点(现已变为从节点),就会出现问题。因此,在集群发生故障转移的同时,哨兵会将最新的节点决策信息推送给所有客户端,告知他们主从关系的变更,使客户端能正确地将写操作发送给新的主节点,确保集群的正常运行。
这三个作用协同配合,共同实现了主从集群的自动故障恢复。不过,在整个故障恢复过程中,还存在许多细节值得深入研究,比如哨兵如何准确判断集群节点的健康状态、如何从众多从节点中选择合适的新主节点以及如何实现节点角色的转换等问题,接下来我们将逐一分析。
1.3.2 哨兵监控节点健康状态的原理
哨兵通过一种心跳机制来监控集群节点的健康状态。具体来说,每隔一秒钟,哨兵就会向集群中的各个实例发送一个 ping 命令。在 Redis 中,接收到 ping 命令会响应一个 pong。然而,如果哨兵发送 ping 命令后,某一实例未在规定时间内响应 pong,即响应超时,哨兵就会认为该实例主观下线。
这里所说的主观下线,是因为仅由发送 ping 命令的这一个哨兵判断该实例下线,不一定意味着该实例真的宕机,可能是出现了网络故障等原因导致响应被阻塞或超时,也许过一会儿就能响应成功。只有当超过指定数量的哨兵都认为该实例主观下线时,才会判定其为客观下线,即真的认为该实例出现故障。
这个指定数量(quorum)是可配置的,在 Redis 配置文件(redis config)中可以设置。默认情况下,推荐的值是哨兵实例数量的一半。例如,若有三个哨兵,一半就是一点五台,超过一半则认为是两台,即当三分之二的哨兵都认为某实例主观下线时,就认定该实例客观下线,这实际上采用了一种投票原则,遵循少数服从多数的逻辑。
1.3.3 从从节点中选主的依据
当哨兵判定主节点客观下线后,需要从从节点中选择一个作为新的主节点。这一选择过程依据多条规则:
断开时间判断
首先要看从节点与主节点之间断开的时间长短。在 Redis 配置文件中有一个值叫 down after milliseconds,用于设置从节点在与主节点断开多长时间后被认为数据可能与主节点差距过大而失去作为新主节点的资格。如果从节点在主节点出现故障后断开时间超过了这个配置值,就意味着其数据可能已经过期较长时间,相对较旧,这种从节点会被直接排除,然后从剩余的从节点中继续挑选。
优先级比较
在排除因断开时间过长的从节点后,接下来会比较从节点的优先级(slave priority)。优先级越小,代表优先级越高。但默认情况下,所有节点的优先级值都是 1,若未进行特殊配置,通过优先级比较就无法区分从节点的优劣,所以通常情况下这个条件在未配置时可暂不考虑。
offset 值对比
在优先级无法有效区分的情况下,会重点关注从节点的 offset 值。我们之前讲过,当从节点同步了主节点发过来的命令时,其 offset 值会增大,它代表着从节点与主节点之间数据同步的进度。显然,offset 值越大,说明从节点的数据与主节点越接近,数据越完整、越新,其优先级也就越高。因为在主节点宕机的情况下,我们更看重从节点数据的完整性和新颖性,所以会优先选择 offset 值大的从节点作为新主节点。
运行 ID 判断
如果所有从节点的 offset 值都一样,此时选择谁作为新主节点就相对随意一些。这时会通过判断从节点的运行 ID(run ID)的大小来决定,运行 ID 越小,其 “运行级别” 越高(这里只是一种相对的说法,表示在这种情况下作为选择依据的一种方式)。运行 ID 是节点在创建时自动生成的,在这种特殊情况下,就相当于随机挑选一个从节点作为新主节点。
综上所述,在整个选主过程中,offset 值是最为重要的因素,只要从节点的 offset 值足够大,就能证明它一直与主节点保持同步,数据是最新的,这对于在主节点故障后维持集群的正常运行至关重要。
1.3.4 实现节点角色转换的过程
当选中了其中一个slave为新的master后(例如slave1),故障的转移的步骤如下:
提升新主节点
首先,哨兵会让被选中的从节点 7002 执行一个命令 “slave of no one”,这个命令的含义是 “永不为奴”,即该节点不再成为任何节点的从节点,从而使其角色转变为新的主节点。
通知其他从节点
新主节点诞生后,需要让其他从节点知晓并认其为主。哨兵会将新主节点的信息(如 7002)通过发送 “slave of” 命令通知给所有其他从节点,使所有从节点执行该命令,从而转换主从关系,都认 7002 为新的主节点。
处理故障节点
不要忘记还有原来宕机的主节点,若它将来恢复,为避免出现多个主节点的混乱情况,哨兵会将故障节点标记为从节点。具体做法是直接修改其配置文件,添加 “slave of 7002” 这样的配置项,这样即使该节点从故障中恢复,它也会自动成为 7002 的从节点,完成主从角色的彻底转换。
通过以上步骤,整个主从切换过程全部完成,实现了在主节点故障时主从集群的平稳过渡和自动恢复。
1.4 牛刀小试-搭建主从集群
主从同步的原理我们已经学会了,但不动手试试怎么行,下来让我们来一起搭建个主从集群吧:
1.4.1 准备工作与容器信息
在着手搭建 Redis 主从集群之前,我们先明确此次搭建所涉及的容器信息。我们将在同一台服务器上部署 3 个 Docker 容器来构建主从集群,具体容器信息如下:
为了高效地构建主从集群,我们编写一个 docker-compose 文件来启动多个 Redis 实例。该文件的具体内容如下:
version: "3.2"
services:
r1:
image: redis
container_name: r1
network_mode: "host"
entrypoint: ["redis-server", "--port", "7001"]
r2:
image: redis
container_name: r2
network_mode: "host"
entrypoint: ["redis-server", "--port", "7002"]
r3:
image: redis
container_name: r3
network_mode: "host"
entrypoint: ["redis-server", "--port", "7003"]
随后,执行命令docker compose up -d
来运行集群。执行结果显示,各个 Docker 容器都已正常启动。
由于我们采用的是host
模式,这种模式下容器直接使用宿主机的网络,无需进行端口映射,所以我们在查看 Docker 容器时看不到常规的端口映射情况。不过,我们可以直接在宿主机通过ps
命令查看到 Redis 进程,以此确认容器已成功启动并运行。
1.4.2 建立集群
尽管我们已经成功启动了 3 个 Redis 实例,但此时它们之间尚未形成主从关系。接下来,我们需要通过特定命令来配置主从关系。
# Redis5.0以前
slaveof <masterip> <masterport>
# Redis5.0以后
replicaof <masterip> <masterport>
在 Redis 5.0 以前,我们使用slaveof <masterip> <masterport>
命令来设置从节点;而在 Redis 5.0 以后,可使用replicaof <masterip> <masterport>
命令进行同样的操作。
这里设置主从关系存在临时和永久两种模式:
永久生效模式:在
redis.conf
文件中利用slaveof
命令指定 master 节点,这样在每次启动 Redis 时,该从节点都会自动以指定的 master 节点为主节点。临时生效模式:直接利用
redis-cli
控制台输入slaveof
命令,指定 master 节点。这种方式设置的主从关系仅在当前会话有效,一旦重启或重新连接,该关系将消失。
在本次测试中,我们采用临时生效模式来建立主从关系。具体操作如下:
首先,连接 r2
容器并使其以 r1
为 master 节点。
# 连接r2
docker exec -it r2 redis-cli -p 7002
# 认r1主,也就是7001
slaveof 10.3.0.5 7001
然后连接r3
,让其以r1
为master
# 连接r3
docker exec -it r3 redis-cli -p 7003
# 认r1主,也就是7001
slaveof 10.3.0.5 7001
完成上述操作后,我们连接 r1 容器来查看集群状态。
# 连接r1
docker exec -it r1 redis-cli -p 7001
# 查看集群状态
info replication
结果如下:
可以看到,当前节点r1:7001
的角色是master
,有两个slave与其连接:
slave0
:port
是7002
,也就是r2
节点slave1
:port
是7003
,也就是r3
节点
这表明我们已经成功建立起了主从集群关系。
1.4.3 测试
依次在r1
、r2
、r3
节点上执行下面命令:
set num 123
get num
你会发现,只有在r1
这个节点上可以执行set
命令(写操作),其它两个节点只能执行get
命令(读操作)。也就是说读写操作已经分离了。
1.5 牛刀小试-搭建哨兵集群
我们深入分析了哨兵的作用和工作原理,了解到它能够监控主从集群中的节点状态,并在主节点宕机时自动实现故障恢复,功能十分强大。现在我们将实际动手搭建一个哨兵集群,来验证这种自动故障恢复的功能。
1.5.1 停止已有主从集群
为避免与接下来要搭建的集群产生冲突,我们首先需要停掉之前搭建的 Redis 主从集群。通过执行 “docker compose down” 命令,就可以轻松完成这一步操作。此时,使用 “docker ps -a” 查看时,会发现 Redis 相关的容器已不存在。
1.5.2 搭建集群
1) 配置文件准备
搭建哨兵集群要用到哨兵的配置文件 “sentinel.conf”,其中包含几个核心配置:
sentinel announce-ip "10.3.0.5"
sentinel monitor hmaster 10.3.0.5 7001 2
sentinel down-after-milliseconds hmaster 5000
sentinel failover-timeout hmaster 60000
说明:
sentinel announce IP:哨兵需要声明自己的 IP 地址。我们将采用在虚拟机里用 Docker 部署的方式,所以这里配置的是虚拟机的 IP 地址,比如 “10.3.0.5”,大家可根据自己的虚拟机 IP 进行修改。
sentinel monitor:这是用于监控的配置项。它需要指定要监控的主从集群的相关信息,包括集群名称(可自行定义,如示例中的 “hmaster ”)、主节点的 IP 地址(这里沿用之前配置的主节点端口 “7001” 对应的 IP)以及 “quorum” 值。“quorum” 值用于确定多少个哨兵认定主节点主观下线后才算客观下线,我们计划使用三台哨兵,按照推荐设置,这里配置为 “2”。
sentinel down after milliseconds:这是一个下线的超时时间配置,即哨兵向节点发送 ping 命令后,若超过该时间(默认 5 秒钟)节点未响应,就认定节点超时。
failover timeout:此为故障恢复的超时时间配置。当哨兵监测到主节点宕机并进行故障恢复操作时,若在恢复过程中出现失败(如网络波动导致连接中断),会进行重试。两次重试之间的超时时间为 60 秒钟,即 60000 毫秒。
2) 配置文件部署
首先创建三个目录 “s1”、“s2”、“s3”,分别代表三个哨兵。
将配置文件 “sentinel.conf” 分别复制到这三个目录里,因为每个哨兵都要有自己独立的配置文件,里面会记录各自不同的数据。
3) 编写 docker-compose 文件
在编写 “docker-compose” 文件时,内容相较于之前有所不同:
主从节点配置:文件中的 “service” 部分先配置了三个主从节点 “r1”、“r2”、“r3”。其配置基本与之前讲的主从集群类似,镜像为 “redis”,容器名分别是 “r1”、“r2”、“r3”,网络模式为 “host”,启动命令为 “redis-server -port 7001/7002/7003”。不同的是,在 “7002” 和 “7003” 的启动命令里直接添加了 “--slave of 7001” 参数,这样在启动时 “r2” 和 “r3” 就会直接成为 “r1” 的从节点,实现主从集群的一步搭建完成。
哨兵节点配置:接着配置了三个哨兵节点 “s1”、“s2”、“s3”。其镜像同样是 “redis”,容器名分别为 “s1”、“s2”、“s3”,网络模式也是 “host”。启动命令变为 “redis-sentinel”,并且后面跟上挂载的配置文件信息,如 “/etc/redis 下的 sentinel.conf”(通过将对应目录挂载到该位置来实现每个哨兵都能使用自己的配置文件),再跟上各自的端口号(为避免冲突,比原来的 Redis 端口大个两万,分别为 27001、27002、27003)。
version: "3.2"
services:
r1:
image: redis
container_name: r1
network_mode: "host"
entrypoint: ["redis-server", "--port", "7001"]
r2:
image: redis
container_name: r2
network_mode: "host"
entrypoint: ["redis-server", "--port", "7002", "--slaveof", "10.3.0.5", "7001"]
r3:
image: redis
container_name: r3
network_mode: "host"
entrypoint: ["redis-server", "--port", "7003", "--slaveof", "10.3.0.5", "7001"]
s1:
image: redis
container_name: s1
volumes:
- /root/redis/s1:/etc/redis
network_mode: "host"
entrypoint: ["redis-sentinel", "/etc/redis/sentinel.conf", "--port", "27001"]
s2:
image: redis
container_name: s2
volumes:
- /root/redis/s2:/etc/redis
network_mode: "host"
entrypoint: ["redis-sentinel", "/etc/redis/sentinel.conf", "--port", "27002"]
s3:
image: redis
container_name: s3
volumes:
- /root/redis/s3:/etc/redis
network_mode: "host"
entrypoint: ["redis-sentinel", "/etc/redis/sentinel.conf", "--port", "27003"]
直接运行命令,启动集群:
docker-compose up -d
我们以s1节点为例,查看其运行日志:
可以看到sentinel
已经联系到了7001
这个节点,并且与其它几个哨兵也建立了链接。
2.Redis分片集群
主从模式可以解决高可用、高并发读的问题。但依然有两个问题没有解决:
海量数据存储
高并发写
评论