【redis学习篇】主从&哨兵&集群架构详解

一、Redis主从架构

【redis学习篇】主从&哨兵&集群架构详解_第1张图片

1.1 redis主从架构搭建

1、复制一份redis.conf文件

2、将相关配置修改为如下值:

port 6380
pidfile /var/run/redis_6380.pid  # 把pid进程号写入pidfile配置的文件
logfile "6380.log"
dir /usr/local/redis-5.0.3/data/6380  # 指定数据存放目录

# 需要注释掉bind
# bind 127.0.0.1(bind绑定的是自己机器网卡的ip,如果有多块网卡可以配多个ip,代表允许客户端通过机器的哪些网卡ip去访问,内网一般可以不配置bind,注释掉即可)

3、配置主从复制

replicaof 192.168.0.60 6379   # 从本机6379的redis实例复制数据,Redis 5.0之前使用slaveof
replica-read-only yes  # 配置从节点只读

4、启动从节点

redis-server redis.conf

5、连接从节点

redis-cli -p 6380

6、测试在6379实例上写数据,6380实例是否能及时同步新修改数据

7、可以自己再配置一个6381的从节点

1.2 主从同步数据原理

主从库模式一旦采用了读写分离,所有数据的写操作只会在主库上进行,不用协调三个实例。

主库有了最新的数据后,会同步给从库,这样,主从库的数据就是一致的。

那么主从库同步是如何完成的呢?主库数据是一次性传给从库,还是分批同步?正常运行中又怎么同步呢?要是主从库间的网络断连了,重新连接后数据还能保持一致吗?

同步分为三种情况:

  1. 第一次主从库全量复制
  2. 主从正常运行期间的同步
  3. 主从库间网络断开重连同步

redis2.8 版本之前的同步流程

  1. 如果你为master配置了一个slave,不管这个slave是否是第一次连接上Master,它都会发送一个 PSYNC 命令给master请求复制数据。

  2. master收到PSYNC命令后,会在后台进行数据持久化通过 bgsave 生成最新的rdb快照文件

  3. 持久化期间,master会继续接收客户端的请求,它会把这些可能修改数据集的请求 缓存在内存

  4. 当持久化进行完毕以后,master会把这份rdb文件数据集发送给slave,slave会把接收到的数据进行持久化生成rdb,然后再加载到内存中。

  5. 然后,master再将之前缓存在内存中的命令通过二进制的方式发送给slave,slave会重新执行一遍

  6. 当master与slave之间的连接由于某些原因而断开时,slave能够自动重连Master,如果master收到了多个slave并发连接请求,它只会进行 一次持久化,而不是一个连接一次,然后再把这一份持久化的数据发送给多个并发连接的slave。

流程图

【redis学习篇】主从&哨兵&集群架构详解_第2张图片
在断线后重复制的情况下,在 2.8 版本之前,会再次执行同步(sync 命令)和命令传播。

如果说,在断线期间,主服务器(已有上万键值对)只执行了几个写命令,为了让从服务器弥补这几个命令,却要重新执行 sync 来生成新的 rdb 文件,这也是非常低效的。

为了解决这个问题,2.8 开始就使用 psync 命令来代替 sync 命令去执行同步操作,从此就有了全量同步和增量同步两种方式来保障主从节点的数据一致性。

全量同步

先从主从库间第一次同步说起吧。
主从库第一次复制过程大体可以分为 3 个阶段:连接建立阶段(即准备阶段)、主库同步数据到从库阶段、发送同步期间新写命令到从库阶段;

直接上图,从整体上有一个全局观的感知,后面具体介绍。
【redis学习篇】主从&哨兵&集群架构详解_第3张图片

第一阶段 (建立连接)

该阶段的主要作用是在主从节点之间建立连接,为数据全量同步做好准备。从库会和主库建立连接,从库执行 replicaof 并发送 psync 命令并告诉主库即将进行同步,主库确认回复后,主从库间就开始同步了。

从库怎么知道主库信息并建立连接的呢?

  • 在从节点的配置文件中的 replicaof 配置项中配置了主节点的 IP 和 port 后,从节点就知道自己要和那个主节点进行连接了。
  • 从节点内部维护了两个字段,masterhost 和 masterport,用于存储主节点的 IP 和 port 信息。

从库执行 replicaof 并发送 psync 命令,表示要执行数据同步,主库收到命令后根据参数启动复制。命令包含了主库的 runID 和 复制进度 offset 两个参数。

每个 Redis 实例启动都会自动生成一个 唯一标识 ID,第一次主从复制的时,从库不知道主库 runID是多少,所以第一次复制设置为 -1,表示第一次复制,记录复制进度偏移量。

主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset,返回给从库。从库收到响应后,会记录下这两个参数。

FULLRESYNC 响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库。

第二阶段 (主库同步数据给从库)

master 执行 bgsave命令生成 RDB 文件,并将文件发送给从库,同时主库为每一个 slave 开辟一块 replication buffer 缓冲区记录从生成 RDB 文件开始收到的所有写命令。

从库收到 RDB 文件后保存到磁盘,并清空当前数据库的数据,再加载 RDB 文件数据到内存中。

第三阶段 (发送新写命令到从库)

从节点加载 RDB 完成后,主节点将 replication buffer 缓冲区的数据发送到从节点(二进制执行命令),Slave 接收并执行,从节点同步至主节点相同的状态。

主库将数据同步到从库过程中,可以正常接受请求么?

主库不会被阻塞,在生成 RDB 文件之后的写操作并没有记录到刚刚的 RDB 文件中,为了保证主从库数据的一致性,所以主库会在内存中使用一个叫 replication buffer 记录 RDB 文件生成后的所有写操作。

replication buffer 到底是什么玩意?

一个在 master 端上创建的缓冲区,存放的数据是下面三个时间内所有的 master 数据写操作。

1)master 执行 bgsave 产生 RDB 的期间的写操作;

2)master 发送 rdb 到 slave 网络传输期间的写操作;

3)slave load rdb 文件把数据恢复到内存的期间的写操作。

Redis 和客户端通信也好,和从库通信也好,Redis 都分配一个内存 buffer 进行数据交互,客户端就是一个 client,从库也是一个 client,我们每个 client 连上 Redis 后,Redis 都会分配一个专有 client buffer,所有数据交互都是通过这个 buffer 进行的。

Master 先把数据写到这个 buffer 中,然后再通过网络发送出去,这样就完成了数据交互。

不管是主从在增量同步还是全量同步时,master 会为其分配一个 buffer ,只不过这个 buffer 专门用来传播写命令到从库,保证主从数据一致,我们通常把它叫做 replication buffer。

replication buffer 太小会引发的问题

replication buffer 由 client-output-buffer-limit slave 设置,当这个值太小会导致主从复制连接断开。

1)当 master-slave 复制连接断开,master 会释放连接相关的数据。replication buffer 中的数据也就丢失了,此时主从之间重新开始复制过程。

2)还有个更严重的问题,主从复制连接断开,导致主从上出现重新执行 bgsave 和 rdb 重传操作无限循环。

当主节点数据量较大,或者主从节点之间网络延迟较大时,可能导致该缓冲区的大小超过了限制,此时主节点会断开与从节点之间的连接;

这种情况可能(具体看offset的位置在缓冲区是否被覆盖)引起全量复制 -> replication buffer 溢出导致连接中断 -> 重连 -> 全量复制 -> replication buffer 缓冲区溢出导致连接中断……的循环。

具体详情:[top redis headaches for devops – replication buffer] 因而推荐把 replication buffer 的 hard/soft limit 设置成 512M。

config set client-output-buffer-limit “slave 536870912 536870912 0”

主从库复制为何不使用 AOF 呢?相比 RDB 来说,丢失的数据更少。

  1. RDB 文件是二进制文件,网络传输 RDB 和写入磁盘的 IO 效率都要比 AOF 高。
  2. 从库进行数据恢复的时候,RDB 的恢复效率也要高于 AOF。

主从库间的网络断了咋办?断开后要重新全量复制么?

在 Redis 2.8 之前,如果主从库在命令传播时出现了网络闪断,那么,从库就会和主库重新进行一次全量复制,开销非常大。

从 Redis 2.8 开始,网络断了之后,主从库会采用增量复制的方式继续同步。

增量同步

用于网络中断等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效。

  1. 断开重连增量复制的实现奥秘就是 repl_backlog_buffer 缓冲区,不管在什么时候 master 都会将写指令操作记录在 repl_backlog_buffer 中

  2. 因为内存有限, repl_backlog_buffer 是一个定长的环形数组,如果数组内容满了,就会从头开始覆盖前面的内容。

  3. master 使用 master_repl_offset 记录自己写到的位置偏移量,slave 则使用 slave_repl_offset 记录已经读取到的偏移量。

  4. master 收到写操作,偏移量则会增加。从库持续执行同步的写指令后,在 repl_backlog_buffer 的已复制的偏移量 slave_repl_offset 也在不断增加。

  5. 正常情况下,这两个偏移量基本相等。在网络断连阶段,主库可能会收到新的写操作命令,所以 master_repl_offset会大于 slave_repl_offset。
    【redis学习篇】主从&哨兵&集群架构详解_第4张图片

当主从断开重连后,slave 会先发送 psync 命令给 master,同时将自己的 runIDslave_repl_offset 发送给 master。

master 只需要把 master_repl_offset与 slave_repl_offset之间的命令同步给从库即可。

增量复制执行流程如下图:
【redis学习篇】主从&哨兵&集群架构详解_第5张图片

repl_backlog_buffer 太小的话从库还没读取到就被 Master 的新写操作覆盖了咋办?

我们要想办法避免这个情况,一旦被覆盖就会执行全量复制。我们可以调整 repl_backlog_size 这个参数用于控制缓冲区大小。计算公式:

repl_backlog_buffer = second * write_size_per_second
second:从服务器断开重连主服务器所需的平均时间;
write_size_per_second:master 平均每秒产生的命令数据量大小(写命令和数据大小总和);

例如,如果主服务器平均每秒产生 1 MB 的写数据,而从服务器断线之后平均要 5 秒才能重新连接上主服务器,那么复制积压缓冲区的大小就不能低于 5 MB。

为了安全起见,可以将复制积压缓冲区的大小设为2 * second * write_size_per_second,这样可以保证绝大部分断线情况都能用部分重同步来处理。

基于长连接的命令传播

完成全量同步后,正常运行过程如何同步呢?

当主从库完成了全量复制,它们之间就会一直维护一个网络连接,主库会通过这个连接将后续陆续收到的命令操作再同步给从库,这个过程也称为基于长连接的命令传播,使用长连接的目的就是避免频繁建立连接导致的开销。

在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING 和 REPLCONF ACK。

主->从:PING

每隔指定的时间,主节点会向从节点发送 PING 命令,这个 PING 命令的作用,主要是为了让从节点进行超时判断。

从->主:REPLCONF ACK

在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:

REPLCONF ACK <replication_offset>

其中 replication_offset 是从服务器当前的复制偏移量。发送 REPLCONF ACK 命令对于主从服务器d 的作用:

  1. 检测主从服务器的网络连接状态。
  2. 检测命令丢失, 从节点发送了自身的 slave_replication_offset,主节点会用自己的 master_replication_offset 对比,如果从节点数据缺失,主节点会从 repl_backlog_buffer 缓冲区中找到并推送缺失的数据。

注意:offset 和 repl_backlog_buffer 缓冲区,不仅可以用于部分复制,也可以用于处理命令丢失等情形;区别在于前者是在断线重连后进行的,而后者是在主从节点没有断线的情况下进行的。

如何确定执行全量同步还是部分同步?

在 Redis 2.8 及以后,从节点可以发送 psync 命令请求同步数据,此时根据主从节点当前状态的不同,同步方式可能是全量复制或部分复制。本文以 Redis 2.8 及之后的版本为例。

关键就是 psync的执行
【redis学习篇】主从&哨兵&集群架构详解_第6张图片

  1. 从节点根据当前状态,发送 psync命令给 master:

    • 如果从节点从未执行过 replicaof ,则从节点发送 psync ? -1,向主节点发送全量复制请求;
    • 如果从节点之前执行过 replicaof 则发送 psync , runID 是上次复制保存的主节点 runID,offset 是上次复制截至时从节点保存的复制偏移量。
  2. 主节点根据接受到的psync命令和当前服务器状态,决定执行全量复制还是部分复制:

    • runID 与从节点发送的 runID 相同,且从节点发送的 slave_repl_offset之后的数据在 repl_backlog_buffer缓冲区中都存在,则回复 CONTINUE,表示将进行部分复制,从节点等待主节点发送其缺少的数据即可;
    • runID 与从节点发送的 runID 不同,或者从节点发送的 slave_repl_offset 之后的数据已不在主节点的 repl_backlog_buffer缓冲区中 (在队列中被挤出了),则回复从节点 FULLRESYNC 表示要进行全量复制
    • 其中 runID 表示主节点当前的 runID,offset 表示主节点当前的 offset,从节点保存这两个值,以备使用。
  3. 一个从库如果和主库断连时间过长,造成它在主库 repl_backlog_buffer 的 slave_repl_offset 位置上的数据已经被覆盖掉了,此时从库和主库间将进行全量复制。

总结

每个从库会记录自己的 slave_repl_offset,每个从库的复制进度也不一定相同。

在和主库重连进行恢复时,从库会通过 psync 命令把自己记录的 slave_repl_offset发给主库,主库会根据从库各自的复制进度,来决定这个从库可以进行增量复制,还是全量复制。

replication buffer 和 repl_backlog

  1. replication buffer 对应于每个 slave,通过 config set client-output-buffer-limit slave设置。
  2. repl_backlog_buffer是一个环形缓冲区,整个 master 进程中只会存在一个,所有的 slave 公用。

总的来说,replication buffer 是主从库在进行全量复制时,主库上用于和从库连接的客户端的 buffer,而 repl_backlog_buffer 是为了支持从库增量复制,主库上用于持续保存写操作的一块专用 buffer。

  1. repl_backlog_buffer是一块专用 buffer,在 Redis 服务器启动后,开始一直接收写操作命令,这是所有从库共享的。主库和从库会各自记录自己的复制进度,所以,不同的从库在进行恢复时,会把自己的复制进度(slave_repl_offset)发给主库,主库就可以和它独立同步。

如图所示:
【redis学习篇】主从&哨兵&集群架构详解_第7张图片

主从复制的场景下,从节点会删除过期数据么?

为了主从节点的数据一致性,从节点不会主动删除数据。我们知道 Redis 有两种删除策略:

  1. 惰性删除:当客户端查询对应的数据时,Redis 判断该数据是否过期,过期则删除。
  2. 定期删除:Redis 通过定时任务删除过期数据。

那客户端通过从节点读取数据会不会读取到过期数据?

Redis 3.2 开始,通过从节点读取数据时,先判断数据是否已过期。如果过期则不返回客户端,并且删除数据。

总结

  1. 主从复制的作用:AOF 和 RDB 二进制文件保证了宕机快速恢复数据,尽可能的防止丢失数据。但是宕机后依然无法提供服务,所以便演化出主从架构、读写分离。
  2. 主从复制虽然解决或缓解了数据冗余、故障恢复、读负载均衡等问题,但其缺陷仍很明显:故障恢复无法自动化;写操作无法负载均衡;存储能力受到单机的限制;这些问题的解决,需要哨兵和集群的帮助

注意:如果有很多从节点,为了缓解主从复制风暴(多个从节点同时复制主节点导致主节点 压力过大 ),可以做如下架构,让部分从节点与从节点(与主节点同步)同步数据

【redis学习篇】主从&哨兵&集群架构详解_第8张图片

管道(Pipeline)

  1. 客户端可以一次性发送多个请求而不用等待服务器的响应,待所有命令都发送完后再一次性读取服务的响应,这样可以极大的降低多条命令执行的网络传输开销,管道执行多条命令的网络开销实际上只相当于一次命令执行的网络开销。

  2. 需要注意到是用pipeline方式打包命令发送,redis必须在处理完所有命令前先缓存起所有命令的处理结果。

  3. 打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。

  4. pipeline中发送的每个command都会被server立即执行,如果执行失败,将会在此后的响应中得到信息;

  5. 也就是pipeline并不是表达 “所有command都一起成功” 的语义,管道中前面命令失败,后面命令不会有影响,继续执行。

Pipeline pl = jedis.pipelined();
for (int i = 0; i < 10; i++) {
    pl.incr("pipelineKey");
    pl.set("zhuge" + i, "zhuge");
    //模拟管道报错
    // pl.setbit("zhuge", -1, true);
}
List<Object> results = pl.syncAndReturnAll();
System.out.println(results);

二、Redis哨兵高可用架构

在这里插入图片描述

  1. sentinel哨兵是特殊的redis服务,不提供读写服务,主要用来监控redis实例节点。

  2. sentinel实时监视主从集群,能实时知道哪个节点是主节点,哪些是从节点,哨兵架构下client端 第一次 会访问sentinel,sentinel会将master信息推送给客户端,后续就直接访问redis的主节点,不会每次都通过sentinel代理访问redis的主节点

  3. 当redis的主节点挂了,sentinel会在从节点中选取一个主节点 ,并且将新的redis主节点推送给client端(这里面redis的client端一般都实现了订阅功能,订阅sentinel发布的节点变动消息)

2.1 哨兵leader选举流程

  1. 当一个master服务器被某sentinel视为下线状态后,该sentinel会与其他sentinel协商选出sentinel的leader进行故障转移工作。

  2. 每个发现master服务器进入下线的sentinel都可以要求其他sentinel选自己为sentinel的leader,选举是先到先得。同时每个sentinel每次选举都会自增配置纪元(选举周期),每个纪元中只会选择一个sentinel的leader。

  3. 如果所有超过一半的sentinel选举某sentinel作为leader。之后该sentinel进行故障转移操作,从存活的slave中选举出新的master,这个选举过程跟集群的master选举很类似。

  4. 哨兵集群只有一个哨兵节点,redis的主从也能正常运行以及选举master,如果master挂了,那唯一的那个哨兵节点就是哨兵leader了,可以正常选举新master。

  5. 不过为了高可用一般都推荐至少部署三个哨兵节点。为什么推荐奇数个哨兵节点原理跟集群奇数个master节点类似。

2.2 redis哨兵架构搭建步骤

1、复制一份sentinel.conf文件

cp sentinel.conf sentinel-26379.conf

2、将相关配置修改为如下值:

port 26379
daemonize yes
pidfile "/var/run/redis-sentinel-26379.pid"
logfile "26379.log"
dir "/usr/local/redis-5.0.3/data"
# sentinel monitor <master-redis-name> <master-redis-ip> <master-redis-port> <quorum>
# quorum是一个数字,指明当有多少个sentinel认为一个master失效时(值一般为:sentinel总数/2 + 1),master才算真正失效
sentinel monitor mymaster 192.168.0.60 6379 2   # mymaster这个名字随便取,客户端访问时会用到

3、启动sentinel哨兵实例

src/redis-sentinel sentinel-26379.conf

4、查看sentinel的info信息

src/redis-cli -p 26379
127.0.0.1:26379>info

可以看到Sentinel的info里已经识别出了redis的主从

5、可以自己再配置两个sentinel,端口26380和26381,注意上述配置文件里的对应数字都要修改

6、sentinel集群都启动完毕后,会将哨兵集群的元数据信息写入所有sentinel的配置文件里去(追加在文件的最下面),我们查看下如下配置文件sentinel-26379.conf,如下所示:

sentinel known-replica mymaster 192.168.0.60 6380 #代表redis主节点的从节点信息
sentinel known-replica mymaster 192.168.0.60 6381 #代表redis主节点的从节点信息
sentinel known-sentinel mymaster 192.168.0.60 26380 52d0a5d70c1f90475b4fc03b6ce7c3c56935760f  #代表感知到的其它哨兵节点
sentinel known-sentinel mymaster 192.168.0.60 26381 e9f530d3882f8043f76ebb8e1686438ba8bd5ca6  #代表感知到的其它哨兵节点

7、当redis主节点如果挂了,哨兵集群会重新选举出新的redis主节点,同时会修改所有sentinel节点配置文件的集群元数据信息,比如6379的redis如果挂了,假设选举出的新主节点是6380,则sentinel文件里的集群元数据信息会变成如下所示:

sentinel known-replica mymaster 192.168.0.60 6379 #代表主节点的从节点信息
sentinel known-replica mymaster 192.168.0.60 6381 #代表主节点的从节点信息
sentinel known-sentinel mymaster 192.168.0.60 26380 52d0a5d70c1f90475b4fc03b6ce7c3c56935760f  #代表感知到的其它哨兵节点
sentinel known-sentinel mymaster 192.168.0.60 26381 e9f530d3882f8043f76ebb8e1686438ba8bd5ca6  #代表感知到的其它哨兵节点

8、同时还会修改sentinel文件里之前配置的mymaster对应的6379端口,改为6380

sentinel monitor mymaster 192.168.0.60 6380 2

9、当6379的redis实例再次启动时,哨兵集群根据集群元数据信息就可以将6379端口的redis节点作为从节点加入集群

2.3 整合spring boot 测试

哨兵的Spring Boot整合Redis连接代码见示例项目:redis-sentinel-cluster

  1. 引入依赖
<dependency>
   <groupId>org.springframework.bootgroupId>
   <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

<dependency>
   <groupId>org.apache.commonsgroupId>
   <artifactId>commons-pool2artifactId>
dependency>
  1. springboot项目核心配置
server:
  port: 8080

spring:
  redis:
    database: 0
    timeout: 3000
    sentinel:    #哨兵模式
      master: mymaster #主服务器所在集群名称
     nodes: 192.168.0.60:26379,192.168.0.60:26380,192.168.0.60:26381
   lettuce:
      pool:
        max-idle: 50
        min-idle: 10
        max-active: 100
        max-wait: 1000
  1. 测试代码
@RestController
public class IndexController {

    private static final Logger logger = LoggerFactory.getLogger(IndexController.class);

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 测试节点挂了哨兵重新选举新的master节点,客户端是否能动态感知到
     * 新的master选举出来后,哨兵会把消息发布出去,客户端实际上是实现了一个消息监听机制,
     * 当哨兵把新master的消息发布出去,客户端会立马感知到新master的信息,从而动态切换访问的masterip
     *
     * @throws InterruptedException
     */
    @RequestMapping("/test_sentinel")
    public void testSentinel() throws InterruptedException {
        int i = 1;
        while (true){
            try {
                stringRedisTemplate.opsForValue().set("zhuge"+i, i+"");
                System.out.println("设置key:"+ "zhuge" + i);
                i++;
                Thread.sleep(1000);
            }catch (Exception e){
                logger.error("错误:", e);
            }
        }
    }
}

三、Redis高可用集群架构

3.1 Redis集群原理分析

  1. Redis Cluster 将所有数据划分为 16384 个 slots(槽位),每个节点负责其中一部分槽位。槽位的信息存储于每个节点中。

  2. 当 Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息并将其缓存在客户端本地

  3. 这样当客户端要查找某个 key 时,会对 key 值使用 crc16 算法进行hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位可以直接定位到目标节点。

  4. 同时因为槽位的信息可能会存在客户端与服务器不一致的情况,还需要纠正机制来实现槽位信息的校验调整。

3.2 槽位定位算法

【redis学习篇】主从&哨兵&集群架构详解_第9张图片

Cluster 默认会对 key 值使用 crc16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位。

HASH_SLOT = CRC16(key) mod 16384

3.3 跳转重定位

  1. 当客户端向一个错误的节点发出了指令,该节点会发现指令的 key 所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连这个节点去获取数据。

  2. 客户端收到指令后除了跳转到正确的节点上去操作,还会同步更新纠正本地的槽位映射表缓存,后续所有 key 将使用新的槽位映射表。

在这里插入图片描述

3.4 Redis集群节点间的通信机制

redis cluster节点间采取gossip协议进行通信

集中式

  1. 优点在于元数据的更新和读取,时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其他节点读取的时候立即就可以立即感知到;

  2. 不足在于所有的元数据的更新压力全部集中在一个地方,可能导致元数据的存储压力。 可以借助zookeeper集中式存储元数据。

gossip

【redis学习篇】主从&哨兵&集群架构详解_第10张图片
gossip协议包含多种消息,包括ping,pong,meet,fail等等。

meet:某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信;

ping:每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据(类似自己感知到的集群节点增加和移除,hash slot信息等); 同时其他节点接收到ping消息之后返回pong消息

pong: 对ping和meet消息的返回,包含自己的状态和其他信息,也可以用于信息广播和更新;

fail: 某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了。

  1. gossip协议的优点在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力;

  2. 缺点在于元数据更新有延时可能导致集群的一些操作会有一些滞后。

gossip通信的10000端口

  1. 每个节点都有一个专门用于节点间gossip通信的端口,就是自己提供服务的端口号+10000,比如7001,那么用于节点间通信的就是17001端口。

  2. 每个节点 每隔一段时间 都会往另外几个节点发送ping消息,同时其他节点接收到ping消息之后返回pong消息。

3.5 网络抖动

  1. 真实世界的机房网络往往并不是风平浪静的,它们经常会发生各种各样的小问题。比如网络抖动就是非常常见的一种现象,突然之间部分连接变得不可访问,然后很快又恢复正常。

  2. 为解决这种问题,Redis Cluster 提供了一种选项 cluster-node-timeout,表示当某个节点持续 timeout 的时间失联时,才可以认定该节点出现故障,需要进行主从切换。

  3. 如果没有这个选项或者配置的时间太短的话,稍微有点网络抖动就会导致主从频繁切换 (数据的重新复制)。

3.6 Redis集群选举原理分析

当slave发现自己的master变为FAIL状态时,便尝试进行Failover,以期成为新的master。由于挂掉的master可能会有多个slave,从而存在多个slave竞争成为master节点的过程 其过程如下:

  1. slave发现自己的master变为FAIL,会发送fail给其他节点,通知其他节点master宕机了

  2. 将自己记录的集群currentEpoch加1,并广播 FAILOVER_AUTH_REQUEST 信息

  3. 其他节点收到该信息,只有master响应,判断请求者的合法性,并发送 FAILOVER_AUTH_ACK,对每一个epoch(一轮选举周期)只会对先来的请求发送一次ack

  4. 尝试failover的slave收集master返回的FAILOVER_AUTH_ACK

  5. slave收到超过集群主节点的 半数 的ack后变成新Master(这里解释了集群为什么至少需要三个主节点,如果只有两个,当其中一个挂了,只剩一个主节点是不能选举成功的)

  6. 然后选举成master的slave会广播Pong消息通知其他集群节点。

注意:如果当前这轮的选举周期结束时所有的slave的收到的ack次数相同,则接着currentEpoch加1,再走一次选举

为了规避反复的重新选举的情况,从节点并不是在主节点一进入 FAIL 状态就马上尝试发起选举,而是有一定 延迟,一定的延迟确保我们等待FAIL状态在集群中传播,slave如果立即尝试选举,其它masters或许尚未意识到FAIL状态,可能会拒绝投票

延迟计算公式

DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms

SLAVE_RANK表示此slave已经从master复制数据的总量的rank。Rank越小代表已复制的数据越新。这种方式下,持有最新数据的slave将会首先发起选举(理论上)。

3.7 集群脑裂数据丢失问题

  1. redis集群没有过半机制会有脑裂问题,网络分区导致脑裂后多个主节点对外提供写服务,一旦网络分区恢复,会将其中一个主节点变为从节点,这时会有大量数据丢失。

  2. 规避方法可以在redis配置里加上参数,写数据成功最少同步的slave数量这个数量可以模仿大于半数机制配置,比如集群总共三个节点可以配置1,加上leader就是2,超过了半数(这种方法不可能百分百避免数据丢失,参考集群leader选举机制):

min-replicas-to-write 1  //写数据成功最少同步的slave数量,这个数量可以模仿大于半数机制配置,比如集群总共三个节点可以配置1,加上leader就是2,超过了半数

注意: 这个配置在一致性得到了提升,但是一定程度上会影响集群的可用性,比如slave要是少于1个,这个集群就算leader正常也不能提供服务了,需要具体场景权衡选择。

3.8 集群是否完整才能对外提供服务

当redis.conf的配置cluster-require-full-coverage为no时,表示当负责一个插槽的主库下线且没有相应的从库进行故障恢复时,整个集群仍然可用,如果为yes则集群不可用。

3.9 Redis集群为什么至少需要三个master节点,并且推荐节点数为奇数?

  1. 因为新master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个master节点,当其中一个挂了,是达不到选举新master的条件的。

  2. 奇数个master节点可以在满足选举该条件的基础上节省一个节点,比如三个master节点和四个master节点的集群相比,大家如果都挂了一个master节点都能选举新master节点,如果都挂了两个master节点都没法选举新master节点了,所以奇数的master节点更多的是从节省机器资源角度出发说的。

3.10 Redis集群对批量操作命令的支持

对于类似mset,mget这样的多个key的原生批量操作命令,redis集群只支持所有key落在同一slot的情况,如果有多个key一定要用mset命令在redis集群上操作,则可以在key的前面加上{XX},这样参数数据分片hash计算的只会是大括号里的值,这样能确保不同的key能落到同一slot里去,示例如下:

mset {user1}:1:name zhuge {user1}:1:age 18

假设name和age计算的hash slot值不一样,但是这条命令在集群下执行,redis只会用大括号里的 user1 做hash slot计算,所以算出来的slot值肯定相同,最后都能落在同一slot。

你可能感兴趣的:(Redis,redis,学习,架构,java,缓存)