【5. Redis的高并发高可用】

Redis的高并发高可用

复制

​ 在分布式系统中为了解决单点问题,通常会把数据复制多个副本部署到其他机器,满足故障恢复和负载均衡等需求。Redis也是如此,它为我们提供了复制功能,实现了相同数据的多个Redis 副本。复制功能是高可用Redis的基础,后面章节的哨兵和集群都是在复制的基础上实现高可用的。

配置

建立复制

​ 参与复制的Redis实例划分为主节点( master)和从节点(slave)。默认情况下,Redis都是主节点。每个从节点只能有一个主节点,而主节点可以同时具有多个从节点。复制的数据流是单向的,只能由主节点复制到从节点。配置复制的方式有以下三种

  1. 在配置文件中加入slaveof {masterHost } {masterPort}随 Redis启动生效。

  2. 在redis-server启动命令后加入–slaveof {masterHost} {masterPort }生效。

  1. 直接使用命令:slaveof {masterHost} { masterPort}生效。

​ 综上所述,slaveof命令在使用时,可以运行期动态配置,也可以提前写到配置文件中。

​ 例如本地启动两个端口为6880和6881的Redis节点,当然因为是在同一台机器上启动两个redis,所以至少6881的从节点需要修改配置文件slavefor6880_6881.conf:

port 6881
logfile "/home/redis/redis-6.2.4/log/6881.log"
dbfilename dump-6881.rdb
dir "/home/redis/redis-6.2.4/data/"

在127.0.0.1:6881执行如下命令:

slaveof 127.0.0.1 6880

​ slaveof配置都是在从节点发起,这时6880作为主节点,6881作为从节点。复制关系建立后可以看到:

​ 从节点6881上:

在这里插入图片描述

在这里插入图片描述

主节点6880上:
在这里插入图片描述

在这里插入图片描述

​ slaveof本身是异步命令,执行slaveof命令时,节点只保存主节点信息后返回,后续复制流程在节点内部异步执行,具体细节见之后。主从节点复制成功建立后,可以使用info replication命令查看复制相关状态。

断开复制

​ slaveof命令不但可以建立复制,还可以在从节点执行slaveof no one来断开与主节点复制关系。例如在6881节点上执行slaveof no one来断开复制。

​ 断开复制主要流程:

  1. 断开与主节点复制关系。
  1. 从节点晋升为主节点。

​ 从节点断开复制后并不会抛弃原有数据,只是无法再获取主节点上的数据变化。

在这里插入图片描述

在这里插入图片描述

​ 通过slaveof命令还可以实现切主操作,所谓切主是指把当前从节点对主节点的复制切换到另一个主节点。执行slaveof { newMasterIp} { newMasterPort}命令即可,例如把6881节点从原来的复制6880节点变为复制6879节点。

切主内部流程如下:

  1. 断开与旧主节点复制关系。
  1. 与新主节点建立复制关系。

  2. 删除从节点当前所有数据。

  3. 对新主节点进行复制操作。

只读

​ 默认情况下,从节点使用slave-read-only=yes配置为只读模式。由于复制只能从主节点到从节点,对于从节点的任何修改主节点都无法感知,修改从节点会造成主从数据不一致。因此建议线上不要修改从节点的只读模式。

传输延迟

​ 主从节点一般部署在不同机器上,复制时的网络延迟就成为需要考虑的问题,Redis为我们提供了repl-disable-tcp-nodelay参数用于控制是否关闭TCP_NODELAY,默认关闭,说明如下:

​ 当关闭时,主节点产生的命令数据无论大小都会及时地发送给从节点,这样主从之间延迟会变小,但增加了网络带宽的消耗。适用于主从之间的网络环境良好的场景,如同机架或同机房部署。

​ 当开启时,主节点会合并较小的TCP数据包从而节省带宽。默认发送时间间隔取决于Linux的内核,一般默认为40毫秒。这种配置节省了带宽但增大主从之间的延迟。适用于主从网络环境复杂或带宽紧张的场景,如跨机房部署。

拓扑

​ Redis 的复制拓扑结构可以支持单层或多层复制关系,根据拓扑复杂性可以分为以下三种:一主一从、一主多从、树状主从结构,下面分别介绍。

一主一从结构

​ 一主一从结构是最简单的复制拓扑结构,用于主节点出现宕机时从节点提供故障转移支持。

在这里插入图片描述

当应用写命令并发量较高且需要持久化时,可以只在从节点上开启 AOF ,这样既保证数据安全性同时也避免了持久化对主节点的性能干扰。但需要注意的是,当主节点关闭持久化功能时,如果主节点脱机要避免自动重启操作。

​ 因为主节点之前没有开启持久化功能自动重启后数据集为空,这时从节点如果继续复制主节点会导致从节点数据也被清空的情况,丧失了持久化的意义。安全的做法是在从节点上执行slaveof no one断开与主节点的复制关系,再重启主节点从而避免这一问题。

一主多从结构

​ 一主多从结构(又称为星形拓扑结构)使得应用端可以利用多个从节点实现读写分离。
在这里插入图片描述

​ 对于读占比较大的场景,可以把读命令发送到从节点来分担主节点压力。同时在日常开发中如果需要执行一些比较耗时的读命令,如:keys、sort等,可以在其中一台从节点上执行,防止慢查询对主节点造成阻塞从而影响线上服务的稳定性。对于写并发量较高的场景,多个从节点会导致主节点写命令的多次发送从而过度消耗网络带宽,同时也加重了主节点的负载影响服务稳定性。

树状主从结构

​ 树状主从结构(又称为树状拓扑结构)使得从节点不但可以复制主节点数据,同时可以作为其他从节点的主节点继续向下层复制。通过引入复制中间层,可以有效降低主节点负载和需要传送给从节点的数据量。
在这里插入图片描述

​ 数据写入节点A后会同步到B和C节点,B节点再把数据同步到D和E节点,数据实现了一层一层的向下复制。当主节点需要挂载多个从节点时为了避免对主节点的性能干扰,可以采用树状主从结构降低主节点压力。

复制原理

复制过程

​ 在从节点执行slaveof命令后,复制过程便开始运作。

1、保存主节点(master)信息

​ 执行slaveof后从节点只保存主节点的地址信息便直接返回,这时建立复制流程还没有开始。

2、从节点(slave)内部通过每秒运行的定时任务维护复制相关逻辑,当定时任务发现存在新的主节点后,会尝试与该节点建立网络连接。

从节点会建立一个socket套接字,专门用于接受主节点发送的复制命令。从节点连接成功后打印日志。

​ 如果从节点无法建立连接,定时任务会无限重试直到连接成功或者执行slaveof no one取消复制

​ 关于连接失败,可以在从节点执行info replication查看master_link_down_since_seconds 指标,它会记录与主节点连接失败的系统时间。从节点连接主节点失败时也会每秒打印日志。

3、发送ping命令。

连接建立成功后从节点发送ping请求进行首次通信,ping 请求主要目的:检测主从之间网络套接字是否可用、检测主节点当前是否可接受处理命令。

​ 从节点发送的ping命令成功返回,Redis打印日志,并继续后续复制流程:

Master replied to PING,replication can continue. . .

4、权限验证。

​ 如果主节点设置了requirepass参数,则需要密码验证,从节点必须配置masterauth参数保证与主节点相同的密码才能通过验证;如果验证失败复制将终止,从节点重新发起复制流程。

5、同步数据集。

​ 主从复制连接正常通信后,对于首次建立复制的场景,主节点会把持有的数据全部发送给从节点,这部分操作是耗时最长的步骤。Redis在2.8版本以后采用新复制命令 psync进行数据同步,原来的sync命令依然支持,保证新旧版本的兼容性。新版同步划分两种情况:全量同步和部分同步。

6、命令持续复制。

​ 当主节点把当前的数据同步给从节点后,便完成了复制的建立流程。接下来主节点会持续地把写命令发送给从节点,保证主从数据一致性。

数据同步

​ Redis在2.8及以上版本使用psync命令完成主从数据同步,同步过程分为:全量复制和部分复制。

​ 全量复制:一般用于初次复制场景,Redis早期支持的复制功能只有全量复制,它会把主节点全部数据一次性发送给从节点,当数据量较大时,会对主从节点和网络造成很大的开销。

​ 部分复制:用于处理在主从复制中因网络闪断等原因造成的数据丢失场景,当从节点再次连上主节点后,如果条件允许,主节点会补发丢失数据给从节点。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销。

​ **部分复制是对老版复制的重大优化,有效避免了不必要的全量复制操作。**psync命令运行需要以下支持:

​ 主从节点各自复制偏移量。主节点复制积压缓冲区。主节点运行id。

1.复制偏移量

​ 参与复制的主从节点都会维护自身复制偏移量。主节点( master)在处理完写入命令后,会把命令的字节长度做累加记录,统计信息在info relication中的master_repl_offset指标中

​ 从节点( slave)每秒钟上报自身的复制偏移量给主节点,因此主节点也会保存从节点的复制偏移量。

​ 从节点在接收到主节点发送的命令后,也会累加记录自身的偏移量。统计信息在inforelication中的slave_repl_offset指标中:通过对比主从节点的复制偏移量,可以判断主从节点数据是否一致。

2.复制积压缓冲区

​ 复制积压缓冲区是保存在主节点上的一个固定长度的队列,默认大小为1MB,当主节点有连接的从节点(slave)时被创建,这时主节点( master)响应写命令时,不但会把命令发送给从节点,还会写入复制积压缓冲区。

​ 由于缓冲区本质上是先进先出的定长队列,所以能实现保存最近已复制数据的功能,用于部分复制和复制命令丢失的数据补救。复制缓冲区相关统计信息保存在主节点的info replication中:

repl_backlog_active : 1 //开启复制缓冲区

repl_backlog_size:1048576//缓冲区最大长度

repl_backlog_first_byte_offset : 7479//起始偏移量,计算当前缓冲区可用范围

repl_backlog_histlen: 1048576//已保存数据的有效长度。

3.主节点运行ID

每个Redis节点启动后都会动态分配一个40位的十六进制字符串作为运行ID。**运行ID的主要作用是用来唯一识别Redis节点,**比如从节点保存主节点的运行ID识别自己正在复制的是哪个主节点。如果只使用ip+port的方式识别主节点,那么主节点重启变更了整体数据集(如替换RDB/AOF文件),从节点再基于偏移量复制数据将是不安全的,因此当运行ID变化后从节点将做全量复制。可以运行info server命令查看当前节点的运行ID:

​ run_id:545f7c76183d0798a327591395b030000ee6def9

​ 需要注意的是Redis关闭再启动后,运行ID会随之改变。

4.psync命令

​ 从节点使用psync命令完成部分复制和全量复制功能,命令格式: psync {runId}{ offset},参数含义如下:

runId:从节点所复制主节点的运行id。

offset:当前从节点已复制的数据偏移量。

流程说明:

  1. 从节点(slave)发送psync命令给主节点,参数runId是当前从节点保存的主节点运行ID,如果没有则默认值为?,参数offset是当前从节点保存的复制偏移量,如果是第一次参与复制则默认值为-1。
  1. 主节点(master)根据psync参数和自身数据情况决定响应结果:

如果回复+FULLRESYNC { runId }{ offset},那么从节点将触发全量复制流程。

如果回复+CONTINUE,从节点将触发部分复制流程。

如果回复+ERR,说明主节点版本低于Redis 2.8,无法识别psync命令,从节点将发送旧版的sync命令触发全量复制流程。

全量复制

​ 全量复制是Redis最早支持的复制方式,也是主从第一次建立复制时必须经历的阶段。触发全量复制的命令是sync和psync。

​ psync全量复制流程,它与2.8以前的sync全量复制机制基本一致。

流程说明:

  1. 发送psync命令进行数据同步,由于是第一次进行复制,从节点没有复制偏移量和主节点的运行ID,所以发送psync ? -1。
  1. 主节点根据psync ? -1解析出当前为全量复制,回复 +FULLRESYNC响应。
  1. 从节点接收主节点的响应数据保存运行ID和偏移量offset,并打印日志。
  1. 主节点执行bgsave保存RDB 文件到本地。

  2. 主节点发送RDB文件给从节点,从节点把接收的RDB文件保存在本地并直接作为从节点的数据文件,接收完RDB后从节点打印相关日志,可以在日志中查看主节点发送的数据量。

​ 需要注意,对于数据量较大的主节点,比如生成的RDB文件超过6GB 以上时要格外小心。传输文件这一步操作非常耗时,速度取决于主从节点之间网络带宽,通过细致分析Full resync和 MASTER<-> SLAVE这两行日志的时间差,可以算出RDB文件从创建到传输完毕消耗的总时间。如果总时间超过repl-timeout所配置的值(默认60秒),从节点将放弃接受RDB文件并清理已经下载的临时文件,导致全量复制失败。

​ 针对数据量较大的节点,建议调大repl-timeout参数防止出现全量同步数据超时。例如对于千兆网卡的机器,网卡带宽理论峰值大约每秒传输100MB,在不考虑其他进程消耗带宽的情况下,6GB的RDB文件至少需要60秒传输时间,默认配置下,极易出现主从数据同步超时。

  1. 对于从节点开始接收RDB快照到接收完成期间,主节点仍然响应读写命令,因此主节点会把这期间写命令数据保存在复制客户端缓冲区内,当从节点加载完RDB文件后,主节点再把缓冲区内的数据发送给从节点,保证主从之间数据一致性。如果主节点创建和传输RDB的时间过长,对于高流量写入场景非常容易造成主节点复制客户端缓冲区溢出。默认配置为:
client-output-buffer-limit slave 256MB 64MB 60

​ 意思是如果60秒内缓冲区消耗持续大于64MB或者直接超过256MB时,主节点将直接关闭复制客户端连接,造成全量同步失败。

​ 对于主节点,当发送完所有的数据后就认为全量复制完成,打印成功日志,但是对于从节点全量复制依然没有完成,还有后续步骤需要处理。

  1. 从节点接收完主节点传送来的全部数据后会清空自身旧数据
  1. 从节点清空数据后开始加载RDB文件,对于较大的RDB文件,这一步操作依然比较耗时。
  2. 从节点成功加载完RDB后,如果当前节点开启了AOF持久化功能,它会立刻做bgrewriteaof操作,为了保证全量复制后AOF持久化文件立刻可用。

​ 通过分析全量复制的所有流程,读者会发现全量复制是一个非常耗时费力的操作。它的时间开销主要包括:

主节点bgsave时间。

RDB文件网络传输时间。

从节点清空数据时间。

从节点加载RDB的时间。

可能的AOF重写时间。

​ 例如我们线上数据量在6G左右的主节点,从节点发起全量复制的总耗时在2分钟左右。因此当数据量达到一定规模之后,由于全量复制过程中将进行多次持久化相关操作和网络数据传输,这期间会大量消耗主从节点所在服务器的CPU、内存和网络资源。所以除了第一次复制时采用全量复制在所难免之外,对于其他场景应该规避全量复制的发生。正因为全量复制的成本问题,Redis实现了部分复制功能。

部分复制

​ 部分复制主要是Redis针对全量复制的过高开销做出的一种优化措施,使用psync{runId} {offset}命令实现。当从节点(slave)正在复制主节点(master)时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区内存在这部分数据则直接发送给从节点,这样就可以保持主从节点复制的一致性。补发的这部分数据一般远远小于全量数据,所以开销很小。

流程说明:

  1. 当主从节点之间网络出现中断时,如果超过repl-timeout时间,主节点会认为从节点故障并中断复制连接,打印日志。如果此时从节点没有宕机,也会打印与主节点连接丢失日志。
  1. 主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,不过主节点内部存在的复制积压缓冲区,依然可以保存最近一段时间的写命令数据,默认最大缓存1MB。

  2. 当主从节点网络恢复后,从节点会再次连上主节点,打印日志。

  3. 当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行ID。因此会把它们当作psync参数发送给主节点,要求进行部分复制操作。

  4. 主节点接到psync命令后首先核对参数runId是否与自身一致,如果一致,说明之前复制的是当前主节点;之后根据参数offset在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送+CONTINUE响应,表示可以进行部分复制。如果不在,则退化为全量复制。

  5. 主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。发送的数据量可以在主节点的日志,传递的数据远远小于全量数据。

心跳

​ 主从节点在建立复制后,它们之间维护着长连接并彼此发送心跳命令。

主从心跳判断机制:

  1. 主从节点彼此都有心跳检测机制,各自模拟成对方的客户端进行通信,通过client list命令查看复制相关客户端信息,主节点的连接状态为flags=M,从节点连接状态为flags=S。

  2. 主节点默认每隔10秒对从节点发送ping命令,判断从节点的存活性和连接状态。可通过参数repl-ping-slave-period控制发送频率。

  3. 从节点在主线程中每隔1秒发送replconf ack {offset}命令,给主节点上报自身当前的复制偏移量。replconf命令主要作用如下:

    实时监测主从节点网络状态;

    上报自身复制偏移量,检查复制数据是否丢失,如果从节点数据丢失,再从主节点的复制缓冲区中拉取丢失数据

    主节点根据replconf命令判断从节点超时时间,体现在info replication统计中的lag信息中,lag表示与从节点最后一次通信延迟的秒数,正常延迟应该在0和1之间。如果超过repl-timeout配置的值((默认60秒),则判定从节点下线并断开复制客户端连接。即使主节点判定从节点下线后,如果从节点重新恢复,心跳检测会继续进行。

异步复制

​ **主节点不但负责数据读写,还负责把写命令同步给从节点。**写命令的发送过程是异步完成,也就是说主节点自身处理完写命令后直接返回给客户端,并不等待从节点复制完成。

​ 由于主从复制过程是异步的,就会造成从节点的数据相对主节点存在延迟(ap系统)。具体延迟多少字节,我们可以在主节点执行info replication命令查看相关指标获得。

​ 在统计信息中可以看到从节点slave信息,分别记录了从节点的ip和 port,从节点的状态,offset表示当前从节点的复制偏移量,master_repl_offset表示当前主节点的复制偏移量,两者的差值就是当前从节点复制延迟量。Redis 的复制速度取决于主从之间网络环境,repl-disable-tcp-nodelay,命令处理速度等。正常情况下,延迟在1秒以内。

哨兵Redis Sentinel

​ Redis 的主从复制模式下,一旦主节点由于故障不能提供服务,需要人工将从节点晋升为主节点,同时还要通知应用方更新主节点地址,对于很多应用场景这种故障处理的方式是无法接受的。Redis 从 2.8开始正式提供了Redis Sentinel
[ˈsentɪnl](哨兵)架构来解决这个问题。

主从复制的问题

​ Redis 的主从复制模式可以将主节点的数据改变同步给从节点,这样从节点就可以起到两个作用:第一,作为主节点的一个备份,一旦主节点出了故障不可达的情况,从节点可以作为后备“顶”上来,并且保证数据尽量不丢失(主从复制是最终一致性)。第二,从节点可以扩展主节点的读能力,一旦主节点不能支撑住大并发量的读操作,从节点可以在一定程度上帮助主节点分担读压力。

但是主从复制也带来了以下问题:

1、一旦主节点出现故障,需要手动将一个从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令其他从节点去复制新的主节点,整个过程都需要人工干预。

2、主节点的写能力受到单机的限制。

3、主节点的存储能力受到单机的限制。

​ **其中第一个问题就是Redis 的高可用问题,**将在下一个小节进行分析。第二、三个问题属于Redis的分布式问题,需要使用Redis Cluster,后面将会说明。

Redis Sentinel的高可用性

名称解释

​ 由于对Redis 的许多概念都有不同的名词解释,所以在介绍 Redis Sentinel之前,我们统一下几个名词的定义。

主节点(master): Redis主服务,一个独立的Redis进程

从节点(slave): Redis从服务,一个独立的Redis进程

Redis 数据节点: 主节点和从节点

Sentinel节点: 监控Redis数据节点,一个独立的Sentinel进程

Sentinel节点集合: 若干 Sentinel节点的组合

Redis Sentinel: Redis高可用实现方案,Sentinel节点集合和 Redis数据节点集合

应用方: 泛指一个或多个客户端一个或者多个客户端进程或者线程

可用性分析

​ Redis主从复制模式下,一旦主节点出现了故障不可达,需要人工干预进行故障转移,无论对于Redis的应用方还是运维方都带来了很大的不便。对于应用方来说无法及时感知到主节点的变化,必然会造成一定的写数据丢失和读数据错误,甚至可能造成应用方服务不可用。对于Redis的运维方来说,整个故障转移的过程是需要人工来介入的,故障转移实时性和准确性上都无法得到保障。

​ 当主节点出现故障时,Redis Sentinel 能自动完成故障发现和故障转移,并通知应用方,从而实现真正的高可用。

​ Redis Sentinel是一个分布式架构,其中包含若干个Sentinel节点和Redis数据节点,每个Sentinel节点会对数据节点和其余Sentinel节点进行监控,当它发现节点不可达时,会对节点做下线标识。如果被标识的是主节点,它还会和其他Sentinel节点进行“协商”,当大多数Sentinel节点都认为主节点不可达时,它们会选举出一个Sentinel节点来完成自动故障转移的工作,同时会将这个变化实时通知给Redis应用方。整个过程完全是自动的,不需要人工来介入,所以这套方案很有效地解决了Redis的高可用问题。

​ 注这里的分布式是指:Redis数据节点、Sentinel节点集合、客户端分布在多个物理节点的架构。

​ Redis Sentinel与 Redis 主从复制模式只是多了若干Sentinel节点,所以Redis Sentinel并没有针对Redis节点做了特殊处理。

​ 从逻辑架构上看,Sentinel节点集合会定期对所有节点进行监控,特别是对主节点的故障实现自动转移。

​ 下面以1个主节点、2个从节点、3个 Sentinel节点组成的Redis Sentinel为例子进行说明。

整个故障转移的处理逻辑有下面4个步骤:

1)主节点出现故障,此时两个从节点与主节点失去连接**,主从复制失败。**

2)每个Sentinel节点通过定期监控发现主节点出现了故障。

3)多个Sentinel节点对主节点的故障达成一致,选举出sentinel-3节点作为领导者负责故障转移。

4)Sentinel领导者节点执行了自动化故障转移,包括通知客户端,重新选择主节点,建立新的复制关系等等。

​ 通过上面介绍的Redis Sentinel逻辑架构以及故障转移的处理,可以看出Redis Sentinel具有以下几个功能:

监控: Sentinel节点会定期检测Redis 数据节点、其余Sentinel节点是否可达。

通知: Sentinel节点会将故障转移的结果通知给应用方。

主节点故障转移:实现从节点晋升为主节点并维护后续正确的主从关系。

配置提供者:在Redis Sentinel结构中,客户端在初始化的时候连接的是Sentinel节点集合,从中获取主节点信息。

​ 同时看到,Redis Sentinel包含了若个 Sentinel节点,这样做也带来了两个好处:对于节点的故障判断是由多个Sentinel节点共同完成,这样可以有效地防止误判。Sentinel节点集合是由若干个Sentinel节点组成的,这样即使个别Sentinel节点不可用,整个 Sentinel节点集合依然是健壮的。

​ 但是Sentinel节点本身就是独立的Redis节点,只不过它们有一些特殊,它们不存储数据,只支持部分命令。

安装和部署

部署拓扑结构

我们以以3个 Sentinel节点、1个主节点、2个从节点组成一个Redis Sentinel进行说明。

master: 127.0.0.1 6885主节点

slave-1: 127.0.0.1 6886 slave-1节点

slave-2: 127.0.0.1 6887 slave-2节点

sentinel-1: 127.0.0.1 26885 sentinel-1节点

sentinel-2: 127.0.0.1 26886 sentinel-2节点

sentinel-3: 127.0.0.1 26887 sentinel-3节点
在这里插入图片描述

集群结构:
在这里插入图片描述

部署Redis 数据节点
启动主节点

配置:

sen_master_6885.conf

port 6885

daemonize yes

logfile "/home/redis/redis-6.2.4/log/6885.log"

dbfilename dump-6885.rdb

dir "/home/redis/redis-6.2.4/data/"

启动主节点:

./redis-server ../conf/sen_master_6885.conf

确认是否启动。一般来说只需要ping命令检测一下就可以,确认 Redis数据节点是否已经启动。

[root@localhost src]# ./redis-cli -p 6885 
127.0.0.1:6885> keys
(error) ERR wrong number of arguments for 'keys' command
127.0.0.1:6885> keys *
(empty array)
127.0.0.1:6885> quit
[root@localhost src]# ./redis-cli -p 6885 ping
PONG
启动两个从节点配置
  1. 两个从节点的配置是完全一样的,下面以一个从节点为例子进行说明,和主节点的配置,不一样的是添加了slaveof配置。
sen_slave_6886.conf

port 6886

daemonize yes

logfile "/home/redis/redis-6.2.4/log/6886.log"

dbfilename dump-6886.rdb

dir "/home/redis/redis-6.2.4/data/"

slaveof 127.0.0.1 6885
  1. 启动两个从节点:
./redis-server ../conf/sen_slave_6886.conf

./redis-server ../conf/sen_slave_6887.conf

验证:

[root@localhost src]# ./redis-cli -p 6886 ping
PONG
[root@localhost src]# ./redis-cli -p 6887 ping
PONG
  1. 确认主从关系

​ 主节点的视角,它有两个从节点,分别是127.0.0.1:6886和127.0.0.1:6887

./redis-cli -p 6885 info replication

[root@localhost src]# ./redis-cli -p 6885 info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6886,state=online,offset=126,lag=1
slave1:ip=127.0.0.1,port=6887,state=online,offset=126,lag=1
master_failover_state:no-failover
master_replid:f5603cdcc7326c9d39cac434224e6c022dfc53dd
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:126
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:126 

​ 从节点的视角,它的主节点是127.0.0.1:6885

./redis-cli -p 6886 info replication

[root@localhost src]# ./redis-cli -p 6886 info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6885
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_read_repl_offset:210
slave_repl_offset:210
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:f5603cdcc7326c9d39cac434224e6c022dfc53dd
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:210
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:210
部署Sentinel节点

​ 3个Sentinel节点的部署方法是完全一致的(端口不同),下面以sentinel-26885节点的部署为例子进行说明。

配置Sentinel节点

sentinel_26885.conf

port 26885
daemonize yes
logfile "/root/redis-6.2.6/log/26885.log"
dbfilename dump-26885.rdb
dir "/root/redis-6.2.6/data/"
sentinel monitor mymaster 127.0.0.1 6885 2
sentinel down-after-milliseconds mymaster 3000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 1000

1 ) Sentinel节点的默认端口是26379,我们改为26885。

2 )sentinel monitor mymaster 127.0.0.1 6885 2配置代表sentinel-26885节点需要监控127.0.0.1:6885 这个主节点,2代表判断主节点失败至少需要2个Sentinel节点同意,mymaster是主节点的别名,其余Sentinel配置将在下一节进行详细说明。

启动Sentinel节点

Sentinel节点的启动方法有两种:

方法一,使用redis-sentinel命令:

./redis-sentinel ../conf/sentinel_26885.conf

方法二,使用redis-server命令加–sentinel参数:

./redis-server ../conf/sentinel_26885.conf --sentinel

两种方法本质上是—样的。

[root@localhost src]# ./redis-cli -p 26885 ping 
PONG 
确认

​ Sentinel节点本质上是一个特殊的Redis节点,所以也可以通过info命令来查询它的相关信息,从下面info的Sentinel片段来看,Sentinel节点找到了主节点127.0.0.1:6885,发现了它的两个从节点。

./redis-cli -p 26885 info Sentinel
[root@localhost src]# ./redis-cli -p 26885 info Sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:6885,slaves=2,sentinels=3

​ 当三个 Sentinel节点都启动后,同时发现Redis Sentinel一共有3个Sentinel节点。这里这里我们可以看到Sentinel节点能够彼此感知到对方,同时能够感知到Redis数据节点。
在这里插入图片描述

至此Redis Sentinel已经搭建起来了,整体上还是比较容易的,但是有2点需要强调一下:

  1. 生产环境中建议Redis Sentinel的所有节点应该分布在不同的物理机上。

  2. Redis Sentinel中的数据节点和普通的Redis数据节点在配置上没有任何区别,只不过是添加了一些Sentinel节点对它们进行监控。

配置说明
参数变化

​ 当所有节点启动后,配置文件中的内容会发生变化,体现在三个方面:

​ Sentinel节点自动发现了从节点、其余Sentinel节点。

去掉了默认配置,例如 parallel-syncs、failover-timeout参数。

​ 添加了配置纪元相关参数。

启动之前:

port 26885
daemonize yes
logfile "/root/redis-6.2.6/log/26885.log"
dbfilename dump-26885.rdb
dir "/root/redis-6.2.6/data/"
sentinel monitor mymaster 127.0.0.1 6885 2
sentinel down-after-milliseconds mymaster 3000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 1000         

启动之后:

port 26885
daemonize yes
logfile "/root/redis-6.2.6/log/26885.log"
dbfilename "dump-26885.rdb"
dir "/root/redis-6.2.6/data"
sentinel monitor mymaster 127.0.0.1 6885 2
sentinel down-after-milliseconds mymaster 3000

sentinel failover-timeout mymaster 1000
# Generated by CONFIG REWRITE
protected-mode no
pidfile "/var/run/redis.pid"
user default on nopass ~* &* +@all
sentinel myid 1e57be59952991c9c1270d25539cddcba6544986
sentinel config-epoch mymaster 0
sentinel leader-epoch mymaster 0
sentinel current-epoch 0
sentinel known-replica mymaster 127.0.0.1 6886
sentinel known-replica mymaster 127.0.0.1 6887
sentinel known-sentinel mymaster 127.0.0.1 26887 9175a26e2248b1db1e588f2c4f69bef2c44f3a37
sentinel known-sentinel mymaster 127.0.0.1 26886 0befaca85c7d715d53b6c7ed87eb84559e7a9bf3

配置文件会自动增加

# Generated by CONFIG REWRITE
protected-mode no
pidfile "/var/run/redis.pid"
user default on nopass ~* &* +@all
sentinel myid 1e57be59952991c9c1270d25539cddcba6544986
sentinel config-epoch mymaster 0
sentinel leader-epoch mymaster 0
sentinel current-epoch 0
sentinel known-replica mymaster 127.0.0.1 6886
sentinel known-replica mymaster 127.0.0.1 6887
sentinel known-sentinel mymaster 127.0.0.1 26887 9175a26e2248b1db1e588f2c4f69bef2c44f3a37
sentinel known-sentinel mymaster 127.0.0.1 26886 0befaca85c7d715d53b6c7ed87eb84559e7a9bf3
常见配置说明
sentinel monitor
sentinel monitor host-name ip 端口 quorum

​ Sentinel节点会定期监控主节点,所以从配置上必然也会有所体现,本配置说明Sentinel节点要监控的是一个名字叫做,ip地址和端口为 的主节点。代表要判定主节点最终不可达所需要的票数。但实际上Sentinel节点会对所有节点进行监控,但是在Sentinel节点的配置中没有看到有关从节点和其余Sentinel节点的配置,那是因为Sentinel节点会从主节点中获取有关从节点以及其余Sentinel节点的相关信息。

​ 参数用于故障发现和判定,例如将quorum配置为2,代表至少有2个Sentinel节点认为主节点不可达,那么这个不可达的判定才是客观的。对于设置的越小,那么达到下线的条件越宽松,反之越严格。一般建议将其设置为Sentinel节点的一半加1。

​ 同时还与Sentinel节点的领导者选举有关,至少要有max(quorum,num(sentinels)/2+1)个Sentinel节点参与选举,才能选出领导者Sentinel,从而完成故障转移。例如有5个 Sentinel节点,quorum=4,那么至少要有max(quorum, num (sentinels)/2 +1)=4个在线Sentinel节点才可以进行领导者选举。

sentinel down-after-milliseconds
sentinel down-after-milliseconds host-name times

​ 每个Sentinel节点都要通过定期发送ping命令来判断Redis数据节点和其余Sentinel节点是否可达,如果超过了down-after-milliseconds配置的时间且没有有效的回复,则判定节点不可达,(单位为毫秒)就是超时时间。这个配置是对节点失败判定的重要依据。

​ down-after-milliseconds虽然以为参数,但实际上对Sentinel 节点、主节点、从节点的失败判定同时有效。

sentinel parallel-syncs
sentinel parallel-syncs <host-name> <nums>

​ 当Sentinel节点集合对主节点故障判定达成一致时,Sentinel领导者节点会做故障转移操作,选出新的主节点,原来的从节点会向新的主节点发起复制操作,parallel-syncs就是用来限制在一次故障转移之后,每次向新的主节点发起复制操作的从节点个数。

​ 如果这个参数配置的比较大,那么多个从节点会向新的主节点同时发起复制操作,尽管复制操作通常不会阻塞主节点,但是同时向主节点发起复制,必然会对主节点所在的机器造成一定的网络和磁盘IO开销。

sentinel failover-timeout
sentinel failover-timeout <host-name> <times>

failover-timeout通常被解释成故障转移超时时间,但实际上它作用于故障转移的各个阶段:

a)选出合适从节点。

b)晋升选出的从节点为主节点。

c)命令其余从节点复制新的主节点。

d)等待原主节点恢复后命令它去复制新的主节点。

failover-timeout的作用具体体现在四个方面:

  1. 如果Redis Sentinel对一个主节点故障转移失败,那么下次再对该主节点做故障转移的起始时间是failover-timeout的2倍。
  1. 在b)阶段时,如果Sentinel节点向a)阶段选出来的从节点执行slaveof no one一直失败(例如该从节点此时出现故障),当此过程超过failover-timeout时,则故障转移失败。

  2. 在b)阶段如果执行成功,Sentinel节点还会执行info命令来确认a)阶段选出来的节点确实晋升为主节点,如果此过程执行时间超过failover-timeout时,则故障转移失败。

  3. 如果c)阶段执行时间超过了failover-timeout(不包含复制时间),则故障转移失败。注意即使超过了这个时间,Sentinel节点也会最终配置从节点去同步最新的主节点。

监控多个主节点

Redis Sentinel可以同时监控多个主节点。

​ 配置方法也比较简单,只需要指定多个masterName来区分不同的主节点即可,例如下面的配置监控monitor master-1(10.10.xx.1:6379)和monitor master-2 (10.10.xx.2:6379)两个主节点:

#监控master-business-1

sentinel monitor master-1 10.10.xx.1 6379 2

sentinel down-after-milliseconds master-1 60000

sentinel failover-timeout master-1 180000

sentinel parallel-syncs master-1 1

#监控master-business-2

sentinel monitor master-2 10.16.xx.2 6380 2

sentinel down-after-milliseconds master-business-210000

sentinel failover-timeout master-business-2180000

sentinel parallel-syncs master-business-21
部署建议

1 ) Sentinel节点不应该部署在一台物理“机器”上。

​ 这里特意强调物理机是因为一台物理机做成了若干虚拟机或者现今比较流行的容器,它们虽然有不同的IP地址,但实际上它们都是同一台物理机,同一台物理机意味着如果这台机器有什么硬件故障,所有的虚拟机都会受到影响,为了实现Sentinel节点集合真正的高可用,请勿将Sentinel节点部署在同一台物理机器上。

2)部署至少三个且奇数个的Sentinel节点。

​ 3个以上是通过增加 Sentinel节点的个数提高对于故障判定的准确性,因为领导者选举需要至少一半加1个节点,奇数个节点可以在满足该条件的基础上节省一个节点。这是因为:

A、在节点数量是奇数个的情况下, 集群总能对外提供服务(即使损失了一部分节点);如果节点数量是偶数个,会存在集群不能用的可能性(脑裂成两个均等的子集群的时候)。

B、假如集群1 ,有3个节点,3/2=1.5 , 即集群想要正常对外提供服务(即leader选举成功),至少需要2个节点是正常的。换句话说,3个节点的集群,允许有一个节点宕机。

​ 假如集群2,有4个节点,4/2=2 ,即想要正常对外提供服务(即leader选举成功),至少需要3个节点是正常的。换句话说,4个节点的集群,也允许有一个节点宕机。

​ 那么问题就来了, 集群1与集群2都有允许1个节点宕机的容错能力,但是集群2比集群1多了1个节点。在相同容错能力的情况下,本着节约资源的原则,集群的节点数维持奇数个更好一些。

4)只有一套Sentinel,还是每个主节点配置一套 Sentinel ?

Sentinel节点集合可以只监控一个主节点,也可以监控多个主节点。

​ 那么在实际生产环境中更偏向于哪一种部署方式呢,下面分别分析两种方案的优缺点。

方案一,一套Sentinel,很明显这种方案在一定程度上降低了维护成本,因为只需要维护固定个数的Sentinel节点,集中对多个Redis数据节点进行管理就可以了。但是这同时也是它的缺点,如果这套 Sentinel节点集合出现异常,可能会对多个Redis数据节点造成影响。还有如果监控的Redis数据节点较多,会造成Sentinel节点产生过多的网络连接,也会有一定的影响。

方案二,多套Sentinel,显然这种方案的优点和缺点和上面是相反的,每个Redis主节点都有自己的Sentinel节点集合,会造成资源浪费。但是优点也很明显,每套Redis Sentinel都是彼此隔离的。

​ 如果Sentinel节点集合监控的是同一个业务的多个主节点集合,那么使用方案一、否则一般建议采用方案二。

监控API

Sentinel节点是一个特殊的Redis 节点,它有自己专属的API。

1、sentinel masters

展示所有被监控的主节点状态以及相关的统计信息在这里插入图片描述

2.sentinel master

展示指定的主节点状态以及相关的统计信息
在这里插入图片描述

3.sentinel slaves

展示指定的从节点状态以及相关的统计信息
在这里插入图片描述

4.sentinel sentinels

展示指定的 Sentinel节点集合(不包含当前Sentinel节点)
在这里插入图片描述

5.sentinel get-master-addr-by-name

​ 返回指定主节点的IP地址和端口

6.sentinel reset

​ 当前Sentinel节点对符合 (通配符风格)主节点的配置进行重置,包含清除主节点的相关状态(例如故障转移),重新发现从节点和 Sentinel节点。

7.sentinel failover

​ 对指定主节点进行强制故障转移(没有和其他Sentinel节点“协商”),当故障转移完成后,其他Sentinel节点按照故障转移的结果更新自身配置,这个命令在Redis Sentinel的日常运维中非常有用。

8.sentinel ckquorum

​ 检测当前可达的 Sentinel节点总数是否达到的个数。例如 quorum=3,而当前可达的Sentinel节点个数为2个,那么将无法进行故障转移,Redis Sentinel的高可用特性也将失去。
在这里插入图片描述

9.sentinel flushconfig

​ 将Sentinel节点的配置强制刷到磁盘上,这个命令Sentinel节点自身用得比较多,对于开发和运维人员只有当外部原因(例如磁盘损坏)造成配置文件损坏或者丢失时,这个命令是很有用的。

10.sentinel remove

​ 取消当前Sentine 节点对于指定主节点的监控。

11.sentinel monitor

这个命令和配置文件中的含义是完全一样的,只不过是通过命令的形式来完成Sentinel节点对主节点的监控。

12.sentinel set

​ 动态修改Sentinel节点配置选项。

13.sentinel is-master-down-by-addr

​ Sentinel节点之间用来交换对主节点是否下线的判断,根据参数的不同,还可以作为Sentinel领导者选举的通信方式。

实现原理

​ Redis Sentinel的基本实现中包含以下: Redis Sentinel 的定时任务、主观下线和客观下线、Sentinel领导者选举、故障转移等等知识点,学习这些可以让我们对Redis Sentinel的高可用特性有更加深入的理解和认识。

三个定时监控任务

​ 一套合理的监控机制是Sentinel节点判定节点不可达的重要保证,Redis Sentinel通过三个定时监控任务完成对各个节点发现和监控:

  1. 每隔10秒,每个Sentinel节点会向主节点和从节点发送info命令获取最新的拓扑结构,Sentinel节点通过对上述结果进行解析就可以找到相应的从节点。

这个定时任务的作用具体可以表现在三个方面:

通过向主节点执行info命令,获取从节点的信息,这也是为什么Sentinel节点不需要显式配置监控从节点。

当有新的从节点加入时都可以立刻感知出来。

节点不可达或者故障转移后,可以通过info命令实时更新节点拓扑信息。

  1. 每隔2秒,每个Sentinel节点会向Redis数据节点的_sentinel_:hello频道上发送该Sentinel节点对于主节点的判断以及当前Sentinel节点的信息,同时每个Sentinel节点也会订阅该频道,来了解其他Sentinel节点以及它们对主节点的判断,所以这个定时任务可以完成以下两个工作:

发现新的Sentinel节点:通过订阅主节点的__sentinel__:hello了解其他的Sentinel节点信息,如果是新加入的Sentinel节点,将该Sentinel节点信息保存起来,并与该 Sentinel节点创建连接。

Sentinel节点之间交换主节点的状态,作为后面客观下线以及领导者选举的依据。

  1. 每隔1秒,每个Sentinel节点会向主节点、从节点、其余Sentinel节点发送一条ping命令做一次心跳检测,来确认这些节点当前是否可达。

通过上面的定时任务,Sentinel节点对主节点、从节点、其余Sentinel节点都建立起连接,实现了对每个节点的监控,这个定时任务是节点失败判定的重要依据。

在这里插入图片描述

主观下线和客观下线
主观下线

​ 上一小节介绍的第三个定时任务,每个Sentinel节点会每隔1秒对主节点、从节点、其他Sentinel节点发送ping命令做心跳检测,当这些节点超过down-after-milliseconds没有进行有效回复,**Sentinel节点就会对该节点做失败判定,这个行为叫做主观下线。**从字面意思也可以很容易看出主观下线是当前Sentinel节点的一家之言,存在误判的可能。

客观下线

​ 当Sentinel主观下线的节点是主节点时,**该Sentinel节点会通过sentinel is-master-down-by-addr命令向其他Sentinel节点询问对主节点的判断,**当超过个数,Sentinel节点认为主节点确实有问题,这时该Sentinel节点会做出客观下线的决定,这样客观下线的含义是比较明显了,也就是大部分Sentinel节点都对主节点的下线做了同意的判定,那么这个判定就是客观的。

领导者Sentinel节点选举

​ **假如Sentinel节点对于主节点已经做了客观下线,那么是不是就可以立即进行故障转移了?**当然不是,实际上故障转移的工作只需要一个Sentinel节点来完成即可,所以 Sentinel节点之间会做一个领导者选举的工作,选出一个Sentinel节点作为领导者进行故障转移的工作。Redis使用了Raft算法实现领导者选举,Redis Sentinel进行领导者选举的大致思路如下:

1)每个在线的Sentinel节点都有资格成为领导者,当它确认主节点主观下线时候,会向其他Sentinel节点发送sentinel is-master-down-by-addr命令,要求将自己设置为领导者。

2)收到命令的Sentinel节点,如果没有同意过其他Sentinel节点的sentinel is-master-down-by-addr命令,将同意该请求,否则拒绝。

3)如果该Sentinel节点发现自己的票数已经大于等于max (quorum,num(sentinels)/2+1),那么它将成为领导者。

4)如果此过程没有选举出领导者**,将进入下一次选举。**

选举的过程非常快,基本上谁先完成客观下线,谁就是领导者。

Raft算法的具体说明,以下链接:

https://github.com/maemual/raft-zh_cn/blob/master/raft-zh_cn.md

具体的实现可以参考蚂蚁金服开源的生产级 Java Raft 算法库:

https://github.com/sofastack/sofa-jraft

故障转移

领导者选举出的Sentinel节点负责故障转移,具体步骤如下:

1)在从节点列表中选出一个节点作为新的主节点,选择方法如下:

a)过滤:“不健康”(主观下线、断线)、5秒内没有回复过Sentinel节点 ping响应、与主节点失联超过down-after-milliseconds*10秒。

b)选择slave-priority(从节点优先级)最高的从节点列表,如果存在则返回,不存在则继续。

c)选择复制偏移量最大的从节点(复制的最完整),如果存在则返回,不存在则继续。

d)选择runid最小的从节点。

在这里插入图片描述

2 ) Sentinel领导者节点会对第一步选出来的从节点执行slaveof no one命令让其成为主节点。

3 ) Sentinel领导者节点会向剩余的从节点发送命令,让它们成为新主节点的从节点,复制规则和parallel-syncs参数有关。

4 ) Sentinel节点集合会将原来的主节点更新为从节点,并保持着对其关注,当其恢复后命令它去复制新的主节点。

在这里插入图片描述

在这里插入图片描述

高可用读写分离

  1. 从节点一般可以起到两个作用:第一,当主节点出现故障时,作为主节点的后备“顶”上来实现故障转移,Redis Sentinel已经实现了该功能的自动化,实现了真正的高可用。第二,扩展主节点的读能力,尤其是在读多写少的场景非常适用。

但上述模型中,从节点不是高可用的,如果slave-1节点出现故障,首先客户端client-1将与其失联,其次Sentinel节点只会对该节点做主观下线,因为Redis Sentinel的故障转移是针对主节点的。所以很多时候,Redis Sentinel中的从节点仅仅是作为主节点一个热备,不让它参与客户端的读操作,就是为了保证整体高可用性,但实际上这种使用方法还是有一些浪费,尤其是在有很多从节点或者确实需要读写分离的场景,所以如何实现从节点的高可用是非常有必要的。

  1. Redis Sentinel读写分离设计思路参考

​ Redis Sentinel在对各个节点的监控中,如果有对应事件的发生,都会发出相应的事件消息,其中和从节点变动的事件有以下几个:

+switch-master:切换主节点(原来的从节点晋升为主节点),说明减少了某个从节点。

+convert-to-slave :切换从节点(原来的主节点降级为从节点),说明添加了某个从节点。

**+sdown 😗*主观下线,说明可能某个从节点可能不可用(因为对从节点不会做客观下线),所以在实现客户端时可以采用自身策略来实现类似主观下线的功能。

**+reboot:**重新启动了某个节点,如果它的角色是slave,那么说明添加了某个从节点。

​ 所以在设计Redis Sentinel的从节点高可用时,只要能够实时掌握所有从节点的状态,把所有从节点看做一个资源池,无论是上线还是下线从节点,客户端都能及时感知到(将其从资源池中添加或者删除),这样从节点的高可用目标就达到了。

客户端连接

​ 如果主节点挂掉了,虽然Redis Sentinel可以完成故障转移,但是客户端无法获取这个变化,那么使用Redis Sentinel的意义就不大了,所以各个语言的客户端需要1对Redis Sentinel进行显式的支持。

Redis Sentinel的客户端

​ Sentinel节点集合具备了监控、通知、自动故障转移、配置提供者若干功能,也就是说实际上最了解主节点信息的就是Sentinel节点集合,而各个主节点可以通过进行标识的,所以,无论是哪种编程语言的客户端,如果需要正确地连接Redis Sentinel,必须有Sentinel节点集合和masterName两个参数。

实现一个Redis Sentinel客户端一般来说需要:

1)遍历Sentinel节点集合获取一个可用的Sentinel节点,Sentinel节点之间可以共享数据,所以从任意一个Sentinel节点获取主节点信息都是可以的。

2)通过sentinel get-master-addr-by-name host-name这个API来获取对应主节点的相关信息。

3)验证当前获取的“主节点”是真正的主节点,这样做的目的是为了防止故障转移期间主节点的变化。

4)保持和 Sentinel节点集合的“联系”,时刻获取关于主节点的相关“信息”。

​ 我们依然使用Jedis 作为Redis 的 Java客户端,Jedis能够很好地支持Redis Sentinel,并且使用Jedis连接Redis Sentinel也很简单,按照Redis Sentinel的原理,需要有masterName和Sentinel节点集合两个参数。Jedis针对Redis Sentinel给出了一个 JedisSentinelPool。

示例代码

依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.3</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
    <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>3.6.3</version>
    </dependency>
  </dependencies>

properties配置

# Redis哨兵服务器地址
redis.sentinel=aliyun:26885,aliyun:26886,aliyun:26887
redis.master=mymaster
# Redis服务器连接密码(默认为空)
redis.password=null
redis.timeout=30000
# 连接池最大连接数(使用负值表示没有限制)
redis.maxTotal=30
# 连接池中的最大空闲连接
redis.maxIdle=10
redis.numTestsPerEvictionRun=1024
redis.timeBetweenEvictionRunsMillis=30000
redis.minEvictableIdleTimeMillis=1800000
redis.softMinEvictableIdleTimeMillis=10000
# 连接池最大阻塞等待时间(使用负值表示没有限制)
redis.maxWaitMillis=1500
redis.testOnBorrow=true
redis.testWhileIdle=true
redis.blockWhenExhausted=false
redis.JmxEnabled=true

JedisPoolConfig配置

@Configuration
@PropertySource("classpath:application.properties")
public class RedisSentinelConfig {
    @Value("${redis.sentinel}")
    private String hosts;

    @Value("${redis.master}")
    private String master;

    @Value("${redis.timeout}")
    private int timeout;

    @Value("${redis.maxIdle}")
    private int maxIdle;

    @Value("${redis.maxWaitMillis}")
    private int maxWaitMillis;

    @Value("${redis.blockWhenExhausted}")
    private Boolean blockWhenExhausted;

    @Value("${redis.JmxEnabled}")
    private Boolean JmxEnabled;

    @Bean
    public JedisPoolConfig jedisPoolConfigFactory() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
        // 连接耗尽时是否阻塞, false报异常,true阻塞直到超时, 默认true
        jedisPoolConfig.setBlockWhenExhausted(blockWhenExhausted);
        // 是否启用pool的jmx管理功能, 默认true
        jedisPoolConfig.setJmxEnabled(JmxEnabled);

        jedisPoolConfig.setTestOnBorrow(true);
        jedisPoolConfig.setTestOnReturn(true);
        return jedisPoolConfig;
    }

    @Bean
    public JedisSentinelPool JedisSentinelPoolFactory(JedisPoolConfig jedisPoolConfig) {

        Set<String> nodeSet = new HashSet<>();
        //获取到节点信息
        String nodeString = hosts;
        //判断字符串是否为空
        if (nodeString == null || "".equals(nodeString)) {
            throw new RuntimeException("RedisSentinelConfiguration initialize error nodeString is null");
        }
        String[] nodeArray = nodeString.split(",");
        //判断是否为空
        if (nodeArray.length == 0) {
            throw new RuntimeException("RedisSentinelConfiguration initialize error nodeArray is null");
        }
        //循环注入至Set中
        for (String node : nodeArray) {
            System.out.println("Read node : " + node);
            nodeSet.add(node);
        }
        //创建连接池对象
        return new JedisSentinelPool(master, nodeSet, jedisPoolConfig, timeout);
    }
}

使用

/**
 * 操作字符串类型
 */
@Component
public class RedisString {

    public final static String RS_STR_NS = "rs:";

    @Autowired
    private JedisSentinelPool jedisSentinelPool;

    /**
     * 向Redis中存值,永久有效
     */
    public String set(String key, String value) {
        try (Jedis jedis = jedisSentinelPool.getResource()) {
            return jedis.set(RS_STR_NS + key, value);
        } catch (Exception e) {
            throw new RuntimeException("向Redis中存值失败!");
        }
    }

    /**
     * 批量向Redis中存值,永久有效
     */
    public String msetRaw(String... keysvalues) {
        try (Jedis jedis = jedisSentinelPool.getResource()) {
            return jedis.mset(keysvalues);
        } catch (Exception e) {
            throw new RuntimeException("批量向Redis中存值失败!");
        }
    }

    /**
     * 根据传入Key获取指定Value
     */
    public String get(String key) {
        try (Jedis jedis = jedisSentinelPool.getResource()) {
            return jedis.get(RS_STR_NS + key);
        } catch (Exception e) {
            throw new RuntimeException("获取Redis值失败!");
        }
    }

}

​ 但是注意,JedisSentinel的实现是不支持读写分离的,所有的连接都是连接到Master上面,Slave就完全当成Master的备份,存在着性能浪费。因此如果想支持读写分离,需要自行实现。可以参考

https://www.jack-yin.com/coding/spring-boot/2683.html

Redis集群

​ Redis Cluster是Redis的分布式解决方案,在3.0版本正式推出,有效地解决了Redis分布式方面的需求。当遇到单机内存、并发、流量等瓶颈时,可以采用Cluster架构方案达到负载均衡的目的。之前,Redis分布式方案一般有两种:

客户端分区方案,优点是分区逻辑可控,缺点是需要自己处理数据路由、高可用、故障转移等问题。

代理方案,优点是简化客户端分布式逻辑和升级维护便利,缺点是加重架构部署复杂度和性能损耗。

​ 现在官方为我们提供了专有的集群方案:Redis Cluster,它非常优雅地解决了Redis集群方面的问题,因此理解应用好 Redis Cluster将极大地解放我们使用分布式Redis 的工作量。

数据分布

数据分布理论

​ 分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集。

需要重点关注的是数据分区规则。常见的分区规则有哈希分区和顺序分区两种,

​ 哈希分区离散度好数据分布业务无关无法顺序访问

​ 顺序分区离散度易倾斜数据分布业务相关可顺序访问。

由于Redis Cluster 采用哈希分区规则,这里我们重点讨论哈希分区,常见的哈希分区规则有几种,下面分别介绍。

节点取余分区

​ 使用特定的数据,如Redis的键或用户ID,再根据节点数量N使用公式: hash(key)%N计算出哈希值,用来决定数据映射到哪一个节点上。这种方案存在一个问题:当节点数量变化时,如扩容或收缩节点,数据节点映射关系需要重新计算,会导致数据的重新迁移。

这种方式的突出优点是简单性,常用于数据库的分库分表规则,一般采用预分区的方式,提前根据数据量规划好分区数,比如划分为512或1024张表,保证可支撑未来一段时间的数据量,再根据负载情况将表迁移到其他数据库中。扩容时通常采用翻倍扩容,避免数据映射全部被打乱导致全量迁移的情况。

一致性哈希分区

​ 一致性哈希分区( Distributed Hash Table)实现思路是为系统中每个节点分配一个 token,范围一般在0~23,这些token构成一个哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点。例如:

集群中有三个节点(Node1、Node2、Node3),五个键(key1、key2、key3、key4、key5),其路由规则为:

key1 -> Node1

key2、key3 -> Node2

key4、key5 -> Node3

在这里插入图片描述

​ 当集群中增加节点时,比如当在Node2和Node3之间增加了一个节点Node4,此时再访问节点key4时,不能在Node4中命中,更一般的,介于Node2和Node4之间的key均失效,这样的失效方式太过于“集中”和“暴力”,更好的方式应该是“平滑”和“分散”地失效。

在这里插入图片描述

​ 这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。但一致性哈希分区存在几个问题:

加减节点会造成哈希环中部分数据无法命中,需要手动处理或者忽略这部分数据,因此一致性哈希常用于缓存场景。

当使用少量节点时,节点变化将大范围影响哈希环中数据映射,因此这种方式不适合少量数据节点的分布式方案。

增加节点只能对下一个相邻节点有比较好的负载分担效果,例如上图中增加了节点Node4只能够对Node3分担部分负载,对集群中其他的节点基本没有起到负载分担的效果;类似地,删除节点会导致下一个相邻节点负载增加,而其他节点却不能有效分担负载压力。

​ 正因为一致性哈希分区的这些缺点,一些分布式系统采用虚拟槽对一致性哈希进行改进,比如 Dynamo系统。

虚拟一致性哈希分区

​ 为了在增删节点的时候,各节点能够保持动态的均衡,将每个真实节点虚拟出若干个虚拟节点,再将这些虚拟节点随机映射到环上。此时每个真实节点不再映射到环上,真实节点只是用来存储键值对,它负责接应各自的一组环上虚拟节点。当对键值对进行存取路由时,首先路由到虚拟节点上,再由虚拟节点找到真实的节点。

​ 如下图所示,三个节点真实节点:Node1、Node2和Node3,每个真实节点虚拟出三个虚拟节点:X#V1、X#V2和X#V3,这样每个真实节点所负责的hash空间不再是连续的一段,而是分散在环上的各处,这样就可以将局部的压力均衡到不同的节点,虚拟节点越多,分散性越好,理论上负载就越倾向均匀。

在这里插入图片描述

虚拟槽分区

​ Redis则是利用了虚拟槽分区,可以算上面虚拟一致性哈希分区的变种它使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中整数定义为槽( slot)。这个范围一般远远大于节点数,比如RedisCluster槽范围是0 ~16383。槽是集群内数据管理和迁移的基本单位。采用大范围槽的主要目的是为了方便数据拆分和集群扩展。每个节点会负责一定数量的槽。

​ 比如集群有5个节点,则每个节点平均大约负责3276个槽。由于采用高质量的哈希算法,每个槽所映射的数据通常比较均匀,将数据平均划分到5个节点进行数据分区。Redis Cluster就是采用虚拟槽分区,下面就介绍Redis 数据分区方法。

为什么槽的范围是0 ~16383?

​ 为什么槽的范围是0 ~16383,也就是说槽的个数在16384个?redis的作者在github上有个回答:https://github.com/redis/redis/issues/2576

The reason is:

  1. Normal heartbeat packets carry the full configuration of a node, that can be replaced in an idempotent way with the old in order to update an old config. This means they contain the slots configuration for a node, in raw form, that uses 2k of space with16k slots, but would use a prohibitive 8k of space using 65k slots.
  2. At the same time it is unlikely that Redis Cluster would scale to more than 1000 mater nodes because of other design tradeoffs.

So 16k was in the right range to ensure enough slots per master with a max of 1000 maters, but a small enough number to propagate the slot configuration as a raw bitmap easily. Note that in small clusters the bitmap would be hard to compress because when N is small the bitmap would have slots/N bits set that is a large percentage of bits set.

这个意思是:
Redis集群中,在握手成功后,两个节点之间会定期发送ping/pong消息,交换数据信息,集群中节点数量越多,消息体内容越大,比如说10个节点的状态信息约1kb,同时redis集群内节点,每秒都在发ping消息。例如,一个总节点数为200的Redis集群,默认情况下,这时ping/pong消息占用带宽达到25M。

​ 那么如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大,非常浪费带宽。

其次redis的集群主节点数量基本不可能超过1000个。集群节点越多,心跳包的消息体内携带的数据越多。如果节点超过1000个,也会导致网络拥堵。因此redis作者,不建议redis cluster节点数量超过1000个。

​ 那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了,可以以确保每个 master 有足够的插槽(平均16个槽位),没有必要拓展到65536个。

​ 再者Redis主节点的配置信息中,它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中,会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),也就是节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低,也会浪费资源。

所以Redis作者决定取16384个槽(1<<14),作为一个比较好的设计权衡。

Redis数据分区

Redis Cluser采用虚拟槽分区,所有的键根据哈希函数映射到0 ~16383整数槽内,计算公式:slot=CRC16(key) & 16383。每一个节点负责维护―部分槽以及槽所映射的键值数据。

在这里插入图片描述
在这里插入图片描述

Redis 虚拟槽分区的特点:

​ 解耦数据和节点之间的关系(数据只和槽关联, 槽只和节点关联),简化了节点扩容和收缩难度。

​ 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据。支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景。

数据分区是分布式存储的核心,理解和灵活运用数据分区规则对于掌握Redis Cluster非常有帮助。

集群功能限制

​ Redis集群相对单机在功能上存在一些限制,需要开发人员提前了解,在使用时做好规避。限制如下:

  1. key批量操作支持有限。如mset、mget,目前只支持具有相同slot值的key执行批量操作。对于映射为不同slot值的key由于执行mget、mget等操作可能存在于多个节点上因此不被支持。

  2. key事务操作支持有限。同理只支持多key在同一节点上的事务操作,当多个key分布在不同的节点上时无法使用事务功能。

  3. key作为数据分区的最小粒度,因此不能将一个大的键值对象如hash、list等映射到不同的节点。

  4. 不支持多数据库空间。单机下的Redis可以支持16个数据库,集群模式下只能使用一个数据库空间,即 db 0。

  5. 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。

搭建集群

介绍完Redis集群分区规则之后,下面我们开始搭建Redis集群。搭建集群有几种方式:

1)依照Redis 协议手工搭建,使用cluster meet、cluster addslots、cluster replicate命令。

2)5.0之前使用由ruby语言编写的redis-trib.rb,在使用前需要安装ruby语言环境。

3)5.0及其之后redis摒弃了redis-trib.rb,将搭建集群的功能合并到了redis-cli。

我们简单点,采用第三种方式搭建。集群中至少应该有奇数个节点,所以至少有三个节点,官方推荐三主三从的配置方式,我们就来搭建一个三主三从的集群。

节点配置

我们现在规定,主节点的端口为6900、6901、6902,从节点的端口为6930、6931、6932。

首先需要配置节点的conf文件,这个比较统一,所有的节点的配置文件都是类似的,我们以端口为6900的节点举例:

port 6900

daemonize yes

pidfile /var/run/redis_6900.pid

logfile "/root/redis-6.2.4/log/6900.log"

dbfilename dump-6900.rdb

dir "/root/redis-6.2.4/data/"

cluster-enabled yes

cluster-config-file nodes-6900.conf

cluster-node-timeout 15000

appendonly yes

appendfilename "appendonly-6900.aof"

注意创建log和data文件夹

# 对应位置创建
mkdir log
mkdir data

技巧:

可以通过脚本统一替换

./cp-redis-config.sh 6901 6902

[root@localhost redis-6.2.6]# cat cp-redis-config.sh 
echo 当前程序名称 $0
echo 参数总数 $#
echo 是否正常退出 1:0 $?
echo 字符串参数列表 $*
echo 数组参数列表 $@
echo pid $$
echo pre pid $?
echo $!
echo 源端端口 $1
echo 目标端口 $2
echo 主从 m:s $3
# 判断变量是否存在
if [ ! -n "$1" ]; then
	echo 源端端口不存在
	exit
fi

if [ ! -n "$2" ]; then
	echo 目标端口不存在
        exit
fi

mode=$3
echo mode = $mode
if [ ! -n "$mode" ]; then
	echo '$3默认设置m'
	mode=m
fi
echo mode = $mode

# 源端文件是否存在
echo 源文件 cluster_m_"$1".conf
if [ ! -f "cluster_m_"$1".conf" ]; then
	echo 源文件不存在
	exit
fi

if [ ! -f "cluster_"$mode"_"$2".conf" ]; then
	echo 创建文件: "cluster_"$mode"_"$2".conf"
	cp "cluster_m_"$1".conf" "cluster_"$mode"_"$2".conf"

else
	echo 清空文件
	rm "cluster_"$mode"_"$2".conf"
	cp "cluster_m_"$1".conf" "cluster_"$mode"_"$2".conf"
fi

echo 目标文件 cluster_"$mode"_"$2".conf

echo 替换变量
sed -i "s/$1/$2/g" "cluster_"$mode"_"$2".conf"

在上述配置中:

cluster-enabled yes # 是否启动集群模式(集群需要修改为yes)

cluster-node-timeout 15000 指定集群节点超时时间(打开注释即可)

cluster-config-file nodes-6900.conf 指定集群节点的配置文件(打开注释即可),这个文件不需要手工编辑,它由Redis节点创建和更新.每个Redis群集节点都需要不同的群集配置文件.确保在同一系统中运行的实例没有重叠群集配置文件名

appendonly yes 指定redis集群持久化方式(默认rdb,建议使用aof方式,此处是否修改不影响集群的搭建)

启动节点

分别执行

./redis-server ../conf/cluster_m_6900.conf
./redis-server ../conf/cluster_m_6901.conf
./redis-server ../conf/cluster_m_6902.conf
./redis-server ../conf/cluster_s_6930.conf
./redis-server ../conf/cluster_s_6931.conf
./redis-server ../conf/cluster_s_6932.conf
集群创建
创建集群主从节点
./redis-cli --cluster create 127.0.0.1:6900 127.0.0.1:6901 127.0.0.1:6902 127.0.0.1:6930 127.0.0.1:6931 127.0.0.1:6932 --cluster-replicas 1

说明:–cluster-replicas 参数为数字,1表示每个主节点需要1个从节点。

​ 为了方便在同一台机器配置, 正常生产需要部署在多台机器上, 例如三台机器, 主从错开(1m-3s, 2m-1s, 3m-2s: 其中1m->1s,其他类似)

通过该方式创建的带有从节点的机器不能够自己手动指定主节点,不符合我们的要求。所以如果需要指定的话,需要自己手动指定,先创建好主节点后,再添加从节点。

指定主从节点
创建集群主节点

命令类似:

# 默认是主节点
./redis-cli --cluster create localhost:6900 localhost:6901 localhost:6902

注意:

1、请记录下每个M后形如“7353cda9e84f6d85c0b6e41bb03d9c4bd2545c07”的字符串,在后面添加从节点时有用;

2、如果服务器存在着防火墙,那么在进行安全设置的时候,除了redis服务器本身的端口,比如6900 要加入允许列表之外,Redis服务在集群中还有一个叫集群总线端口,其端口为客户端连接端口加上10000,即 6900 + 10000 = 16900 。所以开放每个集群节点的客户端端口和集群总线端口才能成功创建集群!

添加集群从节点

命令类似:

redis-cli --cluster add-node localhost:6930 localhost:6900 --cluster-slave --cluster-master-id 224c042651ff55e9bbf67c8ff9ce6edc4cdae63d

说明:上述命令把6930节点加入到6900节点的集群中,并且当做node_id为 117457eab5071954faab5e81c3170600d5192270 的从节点。如果不指定 --cluster-master-id 会随机分配到任意一个主节点。

有三个从节点,自然就要执行三次类似的命令。

./redis-cli --cluster add-nodelocalhost:6930 localhost:6900 --cluster-slave --cluster-master-id 224c042651ff55e9bbf67c8ff9ce6edc4cdae63d

./redis-cli --cluster add-node localhost:6931 localhost:6901 --cluster-slave --cluster-master-id fa2199a4049ef681a0f4c683bd13631dbbd52551

./redis-cli --cluster add-node localhost:6932 localhost:6902 --cluster-slave --cluster-master-id 6497602a2780465ecc48e7b7744ad8e560702a76
集群管理
检查集群
./redis-cli --cluster check localhost:6900 --cluster-search-multiple-owners

说明:任意连接一个集群节点,进行集群状态检查

集群信息查看
./redis-cli --cluster info localhost:6900

说明:检查key、slots、从节点个数的分配情况

报错

[ERR] Not all 16384 slots are covered by nodes.

关闭集群,重新启动

修复集群
redis-cli --cluster fix localhost:6900 --cluster-search-multiple-owners

说明:修复集群和槽的重复分配问题

设置集群的超时时间
redis-cli --cluster set-timeout localhost:6900 10000

说明:连接到集群的任意一节点来设置集群的超时时间参数cluster-node-timeout

集群配置
# 主节点设置密码
redis-cli --cluster call localhost:6900 config set requirepass cc
# 从节点复制时候使用的密码
redis-cli --cluster call localhost:6900 config set masterauth cc -a cc
# Redis Config rewrite 命令对启动 Redis 服务器时所指定的 redis.conf 配置文件进行改写。
redis-cli --cluster call localhost:6900 config rewrite -a cc

说明:连接到集群的任意一节点来对整个集群的所有节点进行设置。

redis-cli –cluster 参数参考

redis-cli --cluster help

Cluster Manager Commands:

create host1:port1 … hostN:portN #创建集群

​ --cluster-replicas #从节点个数

check host:port #检查集群

​ --cluster-search-multiple-owners #检查是否有槽同时被分配给了多个节点

info host:port #查看集群状态

fix host:port #修复集群

​ --cluster-search-multiple-owners #修复槽的重复分配问题

reshard host:port #指定集群的任意一节点进行迁移slot,重新分slots

​ --cluster-from #需要从哪些源节点上迁移slot,可从多个源节点完成迁移,以逗号隔开,传递的是节点的node id,还可以直接传递–from all,这样源节点就是集群的所有节点,不传递该参数的话,则会在迁移过程中提示用户输入

​ --cluster-to #slot需要迁移的目的节点的node id,目的节点只能填写一个,不传递该参数的话,则会在迁移过程中提示用户输入

​ --cluster-slots #需要迁移的slot数量,不传递该参数的话,则会在迁移过程中提示用户输入。

​ --cluster-yes #指定迁移时的确认输入

​ --cluster-timeout #设置migrate命令的超时时间

​ --cluster-pipeline #定义cluster getkeysinslot命令一次取出的key数量,不传的话使用默认值为10

​ --cluster-replace #是否直接replace到目标节点

rebalance host:port #指定集群的任意一节点进行平衡集群节点slot数量

​ --cluster-weight <node1=w1…nodeN=wN> #指定集群节点的权重

​ --cluster-use-empty-masters #设置可以让没有分配slot的主节点参与,默认不允许

​ --cluster-timeout #设置migrate命令的超时时间

​ --cluster-simulate #模拟rebalance操作,不会真正执行迁移操作

​ --cluster-pipeline #定义cluster getkeysinslot命令一次取出的key数量,默认值为10

​ --cluster-threshold #迁移的slot阈值超过threshold,执行rebalance操作

​ --cluster-replace #是否直接replace到目标节点

add-node new_host:new_port existing_host:existing_port #添加节点,把新节点加入到指定的集群,默认添加主节点

​ --cluster-slave #新节点作为从节点,默认随机一个主节点

​ --cluster-master-id #给新节点指定主节点

del-node host:port node_id #删除给定的一个节点,成功后关闭该节点服务

call host:port command arg arg … arg #在集群的所有节点执行相关命令

set-timeout host:port milliseconds #设置cluster-node-timeout

import host:port #将外部redis数据导入集群

​ --cluster-from #将指定实例的数据导入到集群

​ --cluster-copy #migrate时指定copy

​ --cluster-replace #migrate时指定replace

关闭集群
./redis-cli -p 6900 shutdown
./redis-cli -p 6901 shutdown
./redis-cli -p 6902 shutdown
./redis-cli -p 6930 shutdown
./redis-cli -p 6931 shutdown
./redis-cli -p 6932 shutdown

集群伸缩

​ Redis集群提供了灵活的节点扩容和收缩方案。在不影响集群对外服务的情况下,可以为集群添加节点进行扩容也可以下线部分节点进行缩容。

​ Redis集群可以实现对节点的灵活上下线控制。其中原理可抽象为槽和对应数据在不同节点之间灵活移动。首先来看我们之前搭建的集群槽和数据与节点的对应关系。

	**三个主节点分别维护自己负责的槽和对应的数据**,如果希望加入1个节点实现集群扩容时,需要通过相关命令把一部分槽和数据迁移给新节点。
集群扩容
节点配置和启动节点

​ 我们加入两个节点,主节点的端口为6903,从节点的端口为6933。配置与前面的6900类似,不再赘述。

启动这两个节点。

使用之前编写的脚本cp配置

./cp-redis-config.sh 6900 6903 m
./cp-redis-config.sh 6900 6933 s

启动新节点

./redis-server ../conf/cluster_m_6903.conf
./redis-server ../conf/cluster_s_6933.conf
加入集群

执行命令

./redis-cli --cluster info localhost:6900
[root@localhost src]# ./redis-cli --cluster info localhost:6900 -a cc
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
localhost:6900 (224c0426...) -> 0 keys | 5461 slots | 1 slaves.
127.0.0.1:6901 (fa2199a4...) -> 0 keys | 5462 slots | 1 slaves.
127.0.0.1:6902 (6497602a...) -> 0 keys | 5461 slots | 1 slaves.

执行命令

##-a cc 认证必须在cluster nodes前面
[root@localhost src]# ./redis-cli -h localhost -p 6900 -a cc  cluster nodes
./redis-cli -h localWarning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
4ded6ea1b37e88f2a9d18f28417191ead33435b4 127.0.0.1:6930@16930 slave 224c042651ff55e9bbf67c8ff9ce6edc4cdae63d 0 1641190590800 1 connected
fa2199a4049ef681a0f4c683bd13631dbbd52551 127.0.0.1:6901@16901 master - 0 1641190591000 2 connected 5461-10922
224c042651ff55e9bbf67c8ff9ce6edc4cdae63d 127.0.0.1:6900@16900 myself,master - 0 1641190590000 1 connected 0-5460
6497602a2780465ecc48e7b7744ad8e560702a76 127.0.0.1:6902@16902 master - 0 1641190592816 3 connected 10923-16383
9f306d850e9c708c109748c84a04f2d8b905a5fd 127.0.0.1:6931@16931 slave fa2199a4049ef681a0f4c683bd13631dbbd52551 0 1641190591000 2 connected
c0a78a8c1f9d0554bea95148b1b9fab3590b38ea 127.0.0.1:6932@16932 slave 6497602a2780465ecc48e7b7744ad8e560702a76 0 1641190591808 3 connected

​ 可以看到,6903和6933还属于孤立节点,需要将这两个实例节点加入到集群中。

将主节点6903加入集群

执行命令

./redis-cli --cluster add-node localhost:6903 127.0.0.1:6900 -a cc

./redis-cli --cluster add-node localhost:6903 localhost:6900
>>> Send CLUSTER MEET to node localhost:6903 to make it join the cluster.
Node localhost:6903 replied with error:
ERR Invalid node address specified: localhost:6900
改成 127.0.0.1
[root@localhost src]# ./redis-cli --cluster add-node localhost:6903 127.0.0.1:6900 -a cc
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
>>> Adding node localhost:6903 to cluster 127.0.0.1:6900
>>> Performing Cluster Check (using node 127.0.0.1:6900)
M: 224c042651ff55e9bbf67c8ff9ce6edc4cdae63d 127.0.0.1:6900
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: 4ded6ea1b37e88f2a9d18f28417191ead33435b4 127.0.0.1:6930
   slots: (0 slots) slave
   replicates 224c042651ff55e9bbf67c8ff9ce6edc4cdae63d
M: fa2199a4049ef681a0f4c683bd13631dbbd52551 127.0.0.1:6901
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
M: 6497602a2780465ecc48e7b7744ad8e560702a76 127.0.0.1:6902
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
S: 9f306d850e9c708c109748c84a04f2d8b905a5fd 127.0.0.1:6931
   slots: (0 slots) slave
   replicates fa2199a4049ef681a0f4c683bd13631dbbd52551
S: c0a78a8c1f9d0554bea95148b1b9fab3590b38ea 127.0.0.1:6932
   slots: (0 slots) slave
   replicates 6497602a2780465ecc48e7b7744ad8e560702a76
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Send CLUSTER MEET to node localhost:6903 to make it join the cluster.
[OK] New node added correctly.

执行命令

./redis-cli --cluster info localhost:6900

[root@localhost src]# ./redis-cli --cluster info localhost:6900 -a cc
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
localhost:6900 (224c0426...) -> 0 keys | 5461 slots | 1 slaves.
127.0.0.1:6901 (fa2199a4...) -> 0 keys | 5462 slots | 1 slaves.
127.0.0.1:6903 (706395d2...) -> 0 keys | 0 slots | 0 slaves. ##没有数据槽, 没有从节点
127.0.0.1:6902 (6497602a...) -> 0 keys | 5461 slots | 1 slaves.
[OK] 0 keys in 4 masters.
0.00 keys per slot on average.

执行命令

./redis-cli -p 6900 cluster nodes

[root@localhost src]# ./redis-cli -p 6900 -a cc cluster nodes
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
4ded6ea1b37e88f2a9d18f28417191ead33435b4 127.0.0.1:6930@16930 slave 224c042651ff55e9bbf67c8ff9ce6edc4cdae63d 0 1641191241325 1 connected
fa2199a4049ef681a0f4c683bd13631dbbd52551 127.0.0.1:6901@16901 master - 0 1641191245357 2 connected 5461-10922
706395d234c72167b2f1285f88825b947fee147f 127.0.0.1:6903@16903 master - 0 1641191242000 0 connected
224c042651ff55e9bbf67c8ff9ce6edc4cdae63d 127.0.0.1:6900@16900 myself,master - 0 1641191243000 1 connected 0-5460
6497602a2780465ecc48e7b7744ad8e560702a76 127.0.0.1:6902@16902 master - 0 1641191244350 3 connected 10923-16383
9f306d850e9c708c109748c84a04f2d8b905a5fd 127.0.0.1:6931@16931 slave fa2199a4049ef681a0f4c683bd13631dbbd52551 0 1641191244000 2 connected
c0a78a8c1f9d0554bea95148b1b9fab3590b38ea 127.0.0.1:6932@16932 slave 6497602a2780465ecc48e7b7744ad8e560702a76 0 1641191243340 3 connected
将从节点6933加入集群

执行命令, 同时将刚刚加入的节点6903作为从节点6933的主节点

./redis-cli --cluster add-node localhost:6933 127.0.0.1:6900 --cluster-slave --cluster-master-id 706395d234c72167b2f1285f88825b947fee147f -a cc

[root@localhost src]# ./redis-cli --cluster add-node localhost:6933 127.0.0.1:6900 --cluster-slave --cluster-master-id 706395d234c72167b2f1285f88825b947fee147f -a cc
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
>>> Adding node localhost:6933 to cluster 127.0.0.1:6900
>>> Performing Cluster Check (using node 127.0.0.1:6900)
M: 224c042651ff55e9bbf67c8ff9ce6edc4cdae63d 127.0.0.1:6900
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: 4ded6ea1b37e88f2a9d18f28417191ead33435b4 127.0.0.1:6930
   slots: (0 slots) slave
   replicates 224c042651ff55e9bbf67c8ff9ce6edc4cdae63d
M: fa2199a4049ef681a0f4c683bd13631dbbd52551 127.0.0.1:6901
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
M: 706395d234c72167b2f1285f88825b947fee147f 127.0.0.1:6903
   slots: (0 slots) master
M: 6497602a2780465ecc48e7b7744ad8e560702a76 127.0.0.1:6902
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
S: 9f306d850e9c708c109748c84a04f2d8b905a5fd 127.0.0.1:6931
   slots: (0 slots) slave
   replicates fa2199a4049ef681a0f4c683bd13631dbbd52551
S: c0a78a8c1f9d0554bea95148b1b9fab3590b38ea 127.0.0.1:6932
   slots: (0 slots) slave
   replicates 6497602a2780465ecc48e7b7744ad8e560702a76
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Send CLUSTER MEET to node localhost:6933 to make it join the cluster.
Waiting for the cluster to join

>>> Configure node as replica of 127.0.0.1:6903.
[OK] New node added correctly.

报错:

Node localhost:6933 replied with error:
ERR Invalid node address specified: localhost:6900

localhost替换成127.0.0.1

查看节点信息

[root@localhost src]# ./redis-cli -p 6900 -a cc cluster nodes
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
4ded6ea1b37e88f2a9d18f28417191ead33435b4 127.0.0.1:6930@16930 slave 224c042651ff55e9bbf67c8ff9ce6edc4cdae63d 0 1641191655000 1 connected
fa2199a4049ef681a0f4c683bd13631dbbd52551 127.0.0.1:6901@16901 master - 0 1641191656053 2 connected 5461-10922
706395d234c72167b2f1285f88825b947fee147f 127.0.0.1:6903@16903 master - 0 1641191656000 0 connected
ea31b19744a25c35f3cbfd7359378f196ecbb449 127.0.0.1:6933@16933 slave 706395d234c72167b2f1285f88825b947fee147f 0 1641191656961 0 connected
224c042651ff55e9bbf67c8ff9ce6edc4cdae63d 127.0.0.1:6900@16900 myself,master - 0 1641191653000 1 connected 0-5460
6497602a2780465ecc48e7b7744ad8e560702a76 127.0.0.1:6902@16902 master - 0 1641191657970 3 connected 10923-16383
9f306d850e9c708c109748c84a04f2d8b905a5fd 127.0.0.1:6931@16931 slave fa2199a4049ef681a0f4c683bd13631dbbd52551 0 1641191657000 2 connected
c0a78a8c1f9d0554bea95148b1b9fab3590b38ea 127.0.0.1:6932@16932 slave 6497602a2780465ecc48e7b7744ad8e560702a76 0 1641191658979 3 connected

查看集群信息

[root@localhost src]# ./redis-cli --cluster info localhost:6900 -a cc
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
localhost:6900 (224c0426...) -> 0 keys | 5461 slots | 1 slaves.
127.0.0.1:6901 (fa2199a4...) -> 0 keys | 5462 slots | 1 slaves.
127.0.0.1:6903 (706395d2...) -> 0 keys | 0 slots | 1 slaves. ##没有数据槽, 有从节点
127.0.0.1:6902 (6497602a...) -> 0 keys | 5461 slots | 1 slaves.
[OK] 0 keys in 4 masters.
0.00 keys per slot on average.
迁移槽和数据

​ 上面的图中可以看到,6903和6933已正确添加到集群中,接下来就开始分配槽位。我们将6900、6901、6902三个节点中的槽位分别迁出一些槽位给6903,假设分配后的每个节点槽位平均,那么应该分出(16384/4)=4096个槽位。

执行命令

./redis-cli --cluster reshard localhost:6900

[root@localhost src]# ./redis-cli --cluster reshard localhost:6900 -a cc
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
>>> Performing Cluster Check (using node localhost:6900)
M: 224c042651ff55e9bbf67c8ff9ce6edc4cdae63d localhost:6900
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: 4ded6ea1b37e88f2a9d18f28417191ead33435b4 127.0.0.1:6930
   slots: (0 slots) slave
   replicates 224c042651ff55e9bbf67c8ff9ce6edc4cdae63d
M: fa2199a4049ef681a0f4c683bd13631dbbd52551 127.0.0.1:6901
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
M: 706395d234c72167b2f1285f88825b947fee147f 127.0.0.1:6903
   slots: (0 slots) master
   1 additional replica(s)
S: ea31b19744a25c35f3cbfd7359378f196ecbb449 127.0.0.1:6933
   slots: (0 slots) slave
   replicates 706395d234c72167b2f1285f88825b947fee147f
M: 6497602a2780465ecc48e7b7744ad8e560702a76 127.0.0.1:6902
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
S: 9f306d850e9c708c109748c84a04f2d8b905a5fd 127.0.0.1:6931
   slots: (0 slots) slave
   replicates fa2199a4049ef681a0f4c683bd13631dbbd52551
S: c0a78a8c1f9d0554bea95148b1b9fab3590b38ea 127.0.0.1:6932
   slots: (0 slots) slave
   replicates 6497602a2780465ecc48e7b7744ad8e560702a76
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

Redis会提问要迁移的槽位数和接受槽位的节点id,我们这里输入4096和706395d234c72167b2f1285f88825b947fee147f。

How many slots do you want to move (from 1 to 16384)? 4096
What is the receiving node ID? 706395d234c72167b2f1285f88825b947fee147f

接下来,Redis会提问从哪些源节点进行迁移,我们输入“all”

Please enter all the source node IDs.
  Type 'all' to use all the nodes as source nodes for the hash slots.
  Type 'done' once you entered all the source nodes IDs.
Source node #1: 

Redis会显示一个分配计划:

...
	Moving slot 12286 from 6497602a2780465ecc48e7b7744ad8e560702a76
    Moving slot 12287 from 6497602a2780465ecc48e7b7744ad8e560702a76
Do you want to proceed with the proposed reshard plan (yes/no)? yes

填入“yes”。

Redis会开始进行迁移, 稍等一会,等待Redis迁移完成。

迁移完成后,执行命令

./redis-cli -p 6900 cluster nodes ## 注意认证信息, 在端口之后

节点信息

[root@localhost src]# ./redis-cli -p 6900 -a cc cluster nodes 
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
4ded6ea1b37e88f2a9d18f28417191ead33435b4 127.0.0.1:6930@16930 slave 224c042651ff55e9bbf67c8ff9ce6edc4cdae63d 0 1641192161407 1 connected
fa2199a4049ef681a0f4c683bd13631dbbd52551 127.0.0.1:6901@16901 master - 0 1641192161407 2 connected 6827-10922
706395d234c72167b2f1285f88825b947fee147f 127.0.0.1:6903@16903 master - 0 1641192160399 7 connected 0-1364 5461-6826 10923-12287 ## 三个区间的槽位
ea31b19744a25c35f3cbfd7359378f196ecbb449 127.0.0.1:6933@16933 slave 706395d234c72167b2f1285f88825b947fee147f 0 1641192161003 7 connected
224c042651ff55e9bbf67c8ff9ce6edc4cdae63d 127.0.0.1:6900@16900 myself,master - 0 1641192160000 1 connected 1365-5460
6497602a2780465ecc48e7b7744ad8e560702a76 127.0.0.1:6902@16902 master - 0 1641192158000 3 connected 12288-16383
9f306d850e9c708c109748c84a04f2d8b905a5fd 127.0.0.1:6931@16931 slave fa2199a4049ef681a0f4c683bd13631dbbd52551 0 1641192160000 2 connected
c0a78a8c1f9d0554bea95148b1b9fab3590b38ea 127.0.0.1:6932@16932 slave 6497602a2780465ecc48e7b7744ad8e560702a76 0 1641192159390 3 connected

集群信息

[root@localhost src]# ./redis-cli --cluster info 127.0.0.1:6900 -a cc
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6900 (224c0426...) -> 0 keys | 4096 slots | 1 slaves.
127.0.0.1:6901 (fa2199a4...) -> 0 keys | 4096 slots | 1 slaves.
127.0.0.1:6903 (706395d2...) -> 0 keys | 4096 slots | 1 slaves.
127.0.0.1:6902 (6497602a...) -> 0 keys | 4096 slots | 1 slaves.
[OK] 0 keys in 4 masters.
0.00 keys per slot on average.

可以看到槽位确实被迁移到了节点6903之上。这样就实现了集群的扩容。

集群缩容
迁移槽和数据

​ 命令语法:redis-cli --cluster reshard --cluster-from 要迁出节点ID --cluster-to 接收槽节点ID --cluster-slots 迁出槽数量 已存在节点ip 端口

例如:

迁出1365个槽位到6900节点

./redis-cli --cluster reshard --cluster-from 706395d234c72167b2f1285f88825b947fee147f --cluster-to 224c042651ff55e9bbf67c8ff9ce6edc4cdae63d --cluster-slots 1365 localhost:6900 -a cc

迁出1365个槽位到6901节点

./redis-cli --cluster reshard --cluster-from 706395d234c72167b2f1285f88825b947fee147f  --cluster-to fa2199a4049ef681a0f4c683bd13631dbbd52551 --cluster-slots 1365 localhost:6900 -a cc

迁出1366个槽位到6902节点

./redis-cli --cluster reshard --cluster-from 706395d234c72167b2f1285f88825b947fee147f --cluster-to 6497602a2780465ecc48e7b7744ad8e560702a76 --cluster-slots 1366 localhost:6900 -a cc

稍等片刻,等全部槽迁移完成后,执行命令

./redis-cli -p 6900 -a cc cluster nodes

[root@localhost src]# ./redis-cli -p 6900 -a cc cluster nodes
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
4ded6ea1b37e88f2a9d18f28417191ead33435b4 127.0.0.1:6930@16930 slave 224c042651ff55e9bbf67c8ff9ce6edc4cdae63d 0 1641193578799 8 connected
fa2199a4049ef681a0f4c683bd13631dbbd52551 127.0.0.1:6901@16901 master - 0 1641193579807 9 connected 5461-6825 6827-10922
706395d234c72167b2f1285f88825b947fee147f 127.0.0.1:6903@16903 master - 0 1641193580009 7 connected
ea31b19744a25c35f3cbfd7359378f196ecbb449 127.0.0.1:6933@16933 slave 6497602a2780465ecc48e7b7744ad8e560702a76 0 1641193579000 10 connected
224c042651ff55e9bbf67c8ff9ce6edc4cdae63d 127.0.0.1:6900@16900 myself,master - 0 1641193577000 8 connected 0-5460
6497602a2780465ecc48e7b7744ad8e560702a76 127.0.0.1:6902@16902 master - 0 1641193579000 10 connected 6826 10923-16383
9f306d850e9c708c109748c84a04f2d8b905a5fd 127.0.0.1:6931@16931 slave fa2199a4049ef681a0f4c683bd13631dbbd52551 0 1641193576000 9 connected
c0a78a8c1f9d0554bea95148b1b9fab3590b38ea 127.0.0.1:6932@16932 slave 6497602a2780465ecc48e7b7744ad8e560702a76 0 1641193579000 10 connected

./redis-cli --cluster info localhost:6900 -a cc

[root@localhost src]# ./redis-cli --cluster info localhost:6900 -a cc
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
localhost:6900 (224c0426...) -> 0 keys | 5461 slots | 1 slaves.
127.0.0.1:6901 (fa2199a4...) -> 0 keys | 5461 slots | 1 slaves.
127.0.0.1:6903 (706395d2...) -> 0 keys | 0 slots | 0 slaves.
127.0.0.1:6902 (6497602a...) -> 0 keys | 5462 slots | 2 slaves.
[OK] 0 keys in 4 masters.
0.00 keys per slot on average.

可以看到6903上不再存在着槽了。

下线节点

执行命令格式redis-cli --cluster del-node 已存在节点:端口 要删除的节点ID

例如:

./redis-cli --cluster del-node localhost:6900 706395d234c72167b2f1285f88825b947fee147f

[root@localhost src]# ./redis-cli --cluster del-node localhost:6900 706395d234c72167b2f1285f88825b947fee147f -a cc
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
>>> Removing node 706395d234c72167b2f1285f88825b947fee147f from cluster localhost:6900
>>> Sending CLUSTER FORGET messages to the cluster...
>>> Sending CLUSTER RESET SOFT to the deleted node.

./redis-cli --cluster del-node localhost:6900 ea31b19744a25c35f3cbfd7359378f196ecbb449

[root@localhost src]# ./redis-cli --cluster del-node localhost:6900 ea31b19744a25c35f3cbfd7359378f196ecbb449 -
a cc
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
>>> Removing node ea31b19744a25c35f3cbfd7359378f196ecbb449 from cluster localhost:6900
>>> Sending CLUSTER FORGET messages to the cluster...
>>> Sending CLUSTER RESET SOFT to the deleted node.

可以看到这两个节点确实脱离集群了,这样就完成了集群的缩容

[root@localhost src]# ./redis-cli --cluster info localhost:6900 -a cc
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
localhost:6900 (224c0426...) -> 0 keys | 5461 slots | 1 slaves.
127.0.0.1:6901 (fa2199a4...) -> 0 keys | 5461 slots | 1 slaves.
127.0.0.1:6902 (6497602a...) -> 0 keys | 5462 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.

再关闭节点即可。

./redis-cli -p 6903 -a cc shutdown
./redis-cli -p 6933 -a cc shutdown

查看进程

[root@localhost src]# ps -ef | grep 69
root         169       2  0 05:38 ?        00:00:00 [irq/36-pciehp]
root         869       2  0 05:38 ?        00:00:00 [rpciod]
root        2469    2247  0 05:43 tty2     00:00:02 /usr/libexec/gsd-smartcard
root       17482       1  0 20:24 ?        00:00:09 ./redis-server *:6900 [cluster]
root       17484       1  0 20:24 ?        00:00:09 ./redis-server *:6901 [cluster]
root       17494       1  0 20:24 ?        00:00:09 ./redis-server *:6902 [cluster]
root       17500       1  0 20:24 ?        00:00:07 ./redis-server *:6930 [cluster]
root       17506       1  0 20:24 ?        00:00:07 ./redis-server *:6931 [cluster]
root       17512       1  0 20:24 ?        00:00:07 ./redis-server *:6932 [cluster]
root       20281    2970  0 22:54 pts/1    00:00:00 ./redis-cli --cluster info localhsot 6900 -a cc
root       20637    2970  0 23:13 pts/1    00:00:00 grep --color=auto 69

已关闭

迁移相关
在线迁移slot

​ 在线把集群的一些slot从集群原来slot节点迁移到新的节点。其实在前面扩容集群的时候我们已经看到了相关的用法

​ 直接连接到集群的任意一节点

redis-cli --cluster reshard XXXXXXXXXXX:XXXX

按提示操作即可。

平衡(rebalance)slot

1)平衡集群中各个节点的slot数量

redis-cli --cluster rebalance XXXXXXXXXXX:XXXX

2)还可以根据集群中各个节点设置的权重来平衡slot数量

redis-cli --cluster rebalance --cluster-weight 117457eab5071954faab5e81c3170600d5192270=5 815da8448f5d5a304df0353ca10d8f9b77016b28=4 56005b9413cbf225783906307a2631109e753f8f=3 --cluster-simulate 192.168.163.132:6379

请求路由

​ 目前我们已经搭建好Redis集群并且理解了通信和伸缩细节,但还没有使用客户端去操作集群。Redis集群对客户端通信协议做了比较大的修改,为了追求性能最大化,并没有采用代理的方式而是采用客户端直连节点的方式。因此对于希望从单机切换到集群环境的应用需要修改客户端代码。

请求重定向

​ 在集群模式下,Redis接收任何键相关命令时首先计算键对应的槽,再根据槽找出所对应的节点,如果节点是自身,则处理键命令;否则回复MOVED重定向错误,通知客户端请求正确的节点。这个过程称为MOVED重定向。

例如,在之前搭建的集群上执行如下命令:

127.0.0.1:6900> set hell cluster
OK

​ 执行set命令成功,因为键hello对应槽正好位于6900节点负责的槽范围内,可以借助cluster keyslot { key}命令返回key所对应的槽,如下所示:

127.0.0.1:6900> cluster keyslot hell
(integer) 4071
127.0.0.1:6900> set k1 vq
(error) MOVED 12706 127.0.0.1:6902

​ 由于键对应槽是12706,不属于6900节点,则回复 **MOVED (slot}{ip} {port]**格式重定向信息,重定向信息包含了键所对应的槽以及负责该槽的节点地址,根据这些信息客户端就可以向正确的节点发起请求。

​ 需要我们在6902节点上成功执行之前的命令:

127.0.0.1:6902> set k1 vj
OK

使用redis-cli命令时,可以加入-c参数支持自动重定向,简化手动发起重定向操作,如下所示:

[root@localhost src]# redis-cli  -p 6900 -c -a cc
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6900> set k1 ui
-> Redirected to slot [12706] located at 127.0.0.1:6902
OK

​ redis-cli自动帮我们连接到正确的节点执行命令,这个过程是在redis-cli内部维护实质上是client端接到MOVED信息之后再次发起请求,并不在Redis节点中完成请求转发。

同节点对于不属于它的键命令只回复重定向响应,并不负责转发。正因为集群模式下把解析发起重定向的过程放到客户端完成,所以集群客户端协议相对于单机有了很大的变化。

键命令执行步骤主要分两步:计算槽,查找槽所对应的节点。

计算槽

​ Redis首先需要计算键所对应的槽。**根据键的有效部分使用CRC16函数计算出散列值,再取对16383的余数,**使每个键都可以映射到0 ~16383槽范围内。

槽节点查找

Redis计算得到键对应的槽后,需要查找槽所对应的节点。集群内通过消息交换每个节点都会知道所有节点的槽信息。

​ 根据MOVED重定向机制,客户端可以随机连接集群内任一Redis获取键所在节点,这种客户端又叫 Dummy(傀儡)客户端它优点是代码实现简单,对客户端协议影响较小,只需要根据重定向信息再次发送请求即可。但是它的弊端很明显,每次执行键命令前都要到Redis上进行重定向才能找到要执行命令的节点,额外增加了IO开销,这不是Redis集群高效的使用方式。

​ 正因为如此通常集群客户端都采用另一种实现:Smart(智能)客户端。

call命令

​ call命令可以用来在集群的全部节点执行相同的命令。call命令也是需要通过集群的一个节点地址,连上整个集群,然后在集群的每个节点执行该命令。

./redis-cli --cluster call localhost:6900 get name

关闭重启集群

​ 关闭集群就按普通的关闭redis节点逐一关闭即可。

保留原有集群的重启

​ 不需要再执行(形如redis-cli -a ds123 --cluster create 10.8.109.24:6379 10.8.109.36:6379 10.8.109.49:6379 10.8.109.49:6380 10.8.109.24:6380 10.8.109.36:6380 --cluster-replicas 1这样的)集群生成指令。

第一步:先关闭各个Redis节点。

第二步:再启动各个Redis节点即可。(注意:启动redis服务时请在上次成功启动服务的目录启动,即已经生成rdb,conf的目录,否则该节点加入不了集群)

删除原有集群的重启

需要再执行(形如redis-cli -a ds123 --cluster create 10.8.109.24:6379 10.8.109.36:6379 10.8.109.49:6379 10.8.109.49:6380 10.8.109.24:6380 10.8.109.36:6380 --cluster-replicas 1这样的)集群生成指令。

第一步:先关闭各个Redis节点。

第二步:删除各个Redis安装目录下的节点配置文件nodes.conf、数据文件dump.rdb等。

第三步:再启动各个Redis节点。

第四步:执行集群搭建指令。

Smart客户端

smart客户端原理

大多数开发语言的Redis客户端都采用Smart 客户端支持集群协议。Smart客户端通过在内部维护 slot →node的映射关系,本地就可实现键到节点的查找,从而保证IO效率的最大化,而MOVED重定向负责协助Smart客户端更新slot →node映射。我们以Java的Jedis为例,说明Smart客户端操作集群的流程。

1)首先在JedisCluster初始化时会选择一个运行节点,初始化槽和节点映射关系,使用cluster slots命令完成。

2 ) JedisCluster解析cluster slots结果缓存在本地,并为每个节点创建唯一的JedisPool连接池。映射关系在JedisClusterInfoCache类中。

3 ) JedisCluster执行键命令的过程有些复杂,键命令执行流程:

a) 计算slot并根据slots缓存获取目标节点连接,发送命令。

b)如果出现连接错误,使用随机连接重新执行键命令,每次命令重试对redi-rections参数减1。

c)捕获到MOVED重定向错误,使用cluster slots命令更新slots缓存(renewslotCache方法)。

d)重复执行1)~3)步,直到命令执行成功,或者当redirections<=0 时抛出JedisClusterMaxRedirectionsException异常。

ASK 重定向

1.客户端ASK 重定向流程

​ Redis集群支持在线迁移槽(slot)和数据来完成水平伸缩,当slot对应的数据从源节点到目标节点迁移过程中,客户端需要做到智能识别,保证键命令可正常执行。例如当一个slot数据从源节点迁移到目标节点时,期间可能出现一部分数据在源节点,而另一部分在目标节点。

当出现上述情况时,客户端键命令执行流程将发生变化:

  1. 客户端根据本地slots缓存发送命令到源节点,如果存在键对象则直接执行并返回结果给客户端。
  1. 如果键对象不存在,则可能存在于目标节点,这时源节点会回复ASK重定向异常。格式如下:(error) ASK (slot} {targetIP}:{targetPort}。
  1. 客户端从ASK重定向异常提取出目标节点信息,发送asking命令到目标节点打开客户端连接标识,再执行键命令。如果存在则执行,不存在则返回不存在信息。

​ ASK与MOVED虽然都是对客户端的重定向控制,但是有着本质区别。ASK重定向说明集群正在进行slot数据迁移,客户端无法知道什么时候迁移完成,因此只能是临时性的重定向,客户端不会更新slots缓存。但是MOVED重定向说明键对应的槽已经明确指定到新的节点,因此需要更新slots缓存。

集群下的Jedis客户端

依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.3</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>3.6.3</version>
    </dependency>
</dependencies>

properties配置

# Redis集群服务器地址
redis.nodes=aliyun:6900,aliyun:6901,aliyun:6902,aliyun:6930,aliyun:6931,aliyun:6932
#在集群中执行命令时要遵循的最大重定向数目
redis.cluster.max-redirects=5
# Redis服务器连接密码(默认为空)
redis.password=null
redis.timeout=30000
# 连接池最大连接数(使用负值表示没有限制)
redis.maxTotal=30
# 连接池中的最大空闲连接
redis.maxIdle=10
redis.numTestsPerEvictionRun=1024
redis.timeBetweenEvictionRunsMillis=30000
redis.minEvictableIdleTimeMillis=1800000
redis.softMinEvictableIdleTimeMillis=10000
# 连接池最大阻塞等待时间(使用负值表示没有限制)
redis.maxWaitMillis=1500
redis.testOnBorrow=true
redis.testWhileIdle=true
redis.blockWhenExhausted=false
redis.JmxEnabled=true

java bean配置

@Configuration
@PropertySource("classpath:application.properties")
public class RedisClusterConfig {
    @Value("${redis.nodes}")
    private String hosts;

    @Value("${redis.cluster.max-redirects}")
    private int maxRedirects;

    @Value("${redis.timeout}")
    private int timeout;

    @Value("${redis.maxIdle}")
    private int maxIdle;

    @Value("${redis.maxWaitMillis}")
    private int maxWaitMillis;

    @Value("${redis.blockWhenExhausted}")
    private Boolean blockWhenExhausted;

    @Value("${redis.JmxEnabled}")
    private Boolean JmxEnabled;

    @Bean
    public JedisPoolConfig  jedisPoolConfigFactory() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
        // 连接耗尽时是否阻塞, false报异常,true阻塞直到超时, 默认true
        jedisPoolConfig.setBlockWhenExhausted(blockWhenExhausted);
        // 是否启用pool的jmx管理功能, 默认true
        jedisPoolConfig.setJmxEnabled(JmxEnabled);

        jedisPoolConfig.setTestOnBorrow(true);
        jedisPoolConfig.setTestOnReturn(true);
        return jedisPoolConfig;
    }

    @Bean
    public JedisCluster getJedisCluster(JedisPoolConfig jedisPoolConfig) {
        Set<HostAndPort> nodes = new HashSet<>();
        String[] hostsArray = hosts.split(",");
        for (String ipPort : hostsArray) {
            String[] ipPortPair = ipPort.split(":");
            nodes.add(new HostAndPort(ipPortPair[0].trim(), Integer.parseInt(ipPortPair[1].trim())));
        }
        return new JedisCluster(nodes,timeout,1000,1,jedisPoolConfig);
    }

}

字符串操作

/**
 * 操作字符串类型
 */
@Component
public class RedisString {

    public final static String RS_STR_NS = "rs:";

    @Autowired
    private JedisCluster jedisCluster;

    /**
     * 向Redis中存值,永久有效
     */
    public String set(String key, String value) {
        try {
            return jedisCluster.set(RS_STR_NS +key, value);
        } catch (Exception e) {
            throw new RuntimeException("向Redis中存值失败!");
        }
    }

    /**
     * 根据传入Key获取指定Value
     */
    public String get(String key) {
        try {
            return jedisCluster.get(RS_STR_NS +key);
        } catch (Exception e) {
            throw new RuntimeException("获取Redis值失败!");
        }
    }

}

启动项目

@SpringBootApplication
public class RedisClusterApplication {

    public static void main(String[] args) {
        SpringApplication.run(RedisClusterApplication.class, args);
    }
}

​ 同时集群下的Jedis客户端只能支持有限的有限的批量操作,必须要求所有key的slot值相等。这时可以考虑使用hash tags。

Hash tags

​ 集群支持hash tags功能,即可以把一类key定位到同一个slot,tag的标识目前不支持配置,只能使用{},redis处理hash tag的逻辑也很简单,redis只计算从第一次出现{,到第一次出现}的substring的hash值,substring为空,则仍然计算整个key的值。

​ 比如这两个键 {user1000}.following 和 {user1000}.followers 会被哈希到同一个哈希槽里,因为只有 user1000 这个子串会被用来计算哈希值。

对于 foo{}{bar} 这个键,整个键都会被用来计算哈希值,因为第一个出现的 { 和它右边第一个出现的 } 之间没有任何字符。

对于 foo{bar}{zap} 这个键,用来计算哈希值的是 bar 这个子串。

​ 我们在使用hashtag特性时,一定要注意,不能把key的离散性变得非常差。

​ 比如,没有利用hashtag特性之前,key是这样的:mall:sale:freq:ctrl:860000000000001,很明显这种key由于与用户相关,所以离散性非常好。

​ 而使用hashtag以后,key是这样的:mall:sale:freq:ctrl:{860000000000001},这种key还是与用户相关,所以离散性依然非常好。

我们千万不要这样来使用hashtag特性,例如将key设置为:mall:{sale:freq:ctrl}:860000000000001。

​ 这样的话,无论有多少个用户多少个key,其{}中的内容完全一样都是sale:freq:ctrl,也就是说,所有的key都会落在同一个slot上,导致整个Redis集群出现严重的倾斜问题。

集群原理

节点通信
通信流程

​ 在分布式存储中需要提供维护节点元数据信息的机制,所谓元数据是指:节点负责哪些数据,是否出现故障等状态信息。常见的元数据维护方式分为:集中式和P2P方式Redis集群采用P2P的Gossip(流言)协议Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播。

通信过程说明:

  1. 集群中的每个节点都会单独开辟一个TCP通道,用于节点之间彼此通信,通信端口号在基础端口上加10000。

  2. 每个节点在固定周期内通过特定规则选择几个节点发送ping消息。

  3. 接收到ping消息的节点用pong消息作为响应。

​ 集群中每个节点通过一定规则挑选要通信的节点,每个节点可能知道全部节点,也可能仅知道部分节点,只要这些节点彼此可以正常通信,最终它们会达到一致的状态。当节点出故障、新节点加入、主从角色变化、槽信息变更等事件发生时,通过不断的ping/pong消息通信,经过一段时间后所有的节点都会知道整个集群全部节点的最新状态,从而达到集群状态同步的目的。

Gossip 消息

Gossip协议的主要职责就是信息交换。信息交换的载体就是节点彼此发送的Gossip消息,了解这些消息有助于我们理解集群如何完成信息交换。

常用的Gossip消息可分为:ping消息、pong消息、meet消息、fail消息等,

​ meet消息:**用于通知新节点加入。消息发送者通知接收者加入到当前集群,**meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换。

​ ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。ping消息发送封装了自身节点和部分其他节点的状态数据。

​ pong消息:当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内部封装了自身状态数据。节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新。

​ fail消息:**当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,**其他节点接收到fail消息之后把对应节点更新为下线状态。

消息交换的成本

虽然Gossip协议的信息交换机制具有天然的分布式特性,但它是有成本的。由于内部需要频繁地进行节点信息交换,而ping/pong消息会携带当前节点和部分其他节点的状态数据,势必会加重带宽和计算的负担。Redis集群内节点通信采用固定频率(定时任务每秒执行10次)。

​ 因此节点每次选择需要通信的节点列表变得非常重要。通信节点选择过多虽然可以做到信息及时交换但成本过高。节点选择过少会降低集群内所有节点彼此信息交换频率,从而影响故障判定、新节点发现等需求的速度。因此Redis集群的Gossip协议需要兼顾信息交换实时性和成本开销。

​ 消息交换的成本主要体现在单位时间选择发送消息的节点数量和每个消息携带的数据量。

1.选择发送消息的节点数量

​ 集群内每个节点维护定时任务默认间隔1秒,每秒执行10次,定时任务里每秒随机选取5个节点,找出最久没有通信的节点发送ping消息,用于保证 Gossip信息交换的随机性。同时每100毫秒都会扫描本地节点列表,如果发现节点最近一次接受pong消息的时间大于cluster_node_timeout/2,则立刻发送ping消息,防止该节点信息太长时间未更新。

​ 根据以上规则得出每个节点每秒需要发送ping消息的数量= 1 +10 * num(node.pong_received >cluster_node_timeout/2),因此cluster_node_timeout参数对消息发送的节点数量影响非常大。当我们的带宽资源紧张时,可以适当调大这个参数,如从默认15秒改为30秒来降低带宽占用率。过度调大cluster_node_timeout 会影响消息交换的频率从而影响故障转移、槽信息更新、新节点发现的速度。因此需要根据业务容忍度和资源消耗进行平衡。同时整个集群消息总交换量也跟节点数成正比。

⒉消息数据量

​ 每个ping消息的数据量体现在消息头和消息体中,其中消息头主要占用空间的字段是myslots [CLUSTER_SLOTS/8],占用2KB,这块空间占用相对固定。消息体会携带一定数量的其他节点信息用于信息交换。

​ 根消息体携带数据量跟集群的节点数息息相关,更大的集群每次消息通信的成本也就更高,因此对于Redis集群来说并不是大而全的集群更好。

故障转移

Redis集群自身实现了高可用。高可用首先需要解决集群部分失败的场景:当集群内少量节点出现故障时通过自动故障转移保证集群可以正常对外提供服务。

故障发现

​ 当集群内某个节点出现问题时,需要通过一种健壮的方式保证识别出节点是否发生了故障。Redis集群内节点通过ping/pong消息实现节点通信,消息不但可以传播节点槽信息,还可以传播其他状态如:主从状态、节点故障等。因此故障发现也是通过消息传播机制实现的,主要环节包括:主观下线(pfail)和客观下线(fail)。

主观下线:

​ 指某个节点认为另一个节点不可用,即下线状态,这个状态并不是最终的故障判定,只能代表一个节点的意见,可能存在误判情况。

客观下线:

​ 指标记一个节点真正的下线,集群内多个节点都认为该节点不可用,从而达成共识的结果。如果是持有槽的主节点故障,需要为该节点进行故障转移。

主观下线

​ 集群中每个节点都会定期向其他节点发送ping消息,接收节点回复pong消息作为响应。如果在cluster-node-timeout时间内通信一直失败,则发送节点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态。

流程说明:

1)节点a发送ping消息给节点b,如果通信正常将接收到pong消息,节点 a更新最近一次与节点b的通信时间。

2)如果节点 a与节点b通信出现问题则断开连接,下次会进行重连。如果一直通信失败,则节点a记录的与节点b最后通信时间将无法更新。

3)节点a内的定时任务检测到与节点b最后通信时间超高cluster-node-timeout时,更新本地对节点b的状态为主观下线(pfail)。

​ 主观下线简单来讲就是,当cluster-note-timeout时间内某节点无法与另一个节点顺利完成ping消息通信时,则将该节点标记为主观下线状态。每个节点内的clusterstate结构都需要保存其他节点信息,用于从自身视角判断其他节点的状态。

​ Redis集群对于节点最终是否故障判断非常严谨,只有一个节点认为主观下线并不能准确判断是否故障。

​ 比如节点6379与6385通信中断,导致6379判断6385为主观下线状态,但是6380与6385节点之间通信正常,这种情况不能判定节点6385发生故障。因此对于一个健壮的故障发现机制,需要集群内大多数节点都判断6385故障时,才能认为6385确实发生故障,然后为6385节点进行故障转移。而这种多个节点协作完成故障发现的过程叫做客观下线。

客观下线

​ 当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播。

​ ping/pong消息的消息体会携带集群1/10的其他节点状态数据,当接受节点发现消息体中含有主观下线的节点状态时,会在本地找到故障节点的ClusterNode结构,保存到下线报告链表中。

​ 通过Gossip消息传播,集群内节点不断收集到故障节点的下线报告。当半数以上持有槽的主节点都认为某个节点是主观下线时。触发客观下线流程。这里有两个问题:

  1. 为什么必须是负责槽的主节点参与故障发现决策?因为集群模式下只有处理槽的主节点才负责读写请求和集群槽等关键信息维护,而从节点只进行主节点数据和状态信息的复制。
  1. 为什么半数以上处理槽的主节点?必须半数以上是为了应对网络分区等原因造成的集群分割情况,被分割的小集群因为无法完成从主观下线到客观下线这一关键过程,从而防止小集群完成故障转移之后继续对外提供服务。

尝试客观下线

​ 集群中的节点每次接收到其他节点的pfail状态,都会尝试触发客观下线,

流程说明:

  1. 首先统计有效的下线报告数量,如果小于集群内持有槽的主节点总数的一半则退出。
  1. 当下线报告大于槽主节点数量一半时,标记对应故障节点为客观下线状态。
  1. 向集群广播一条fail消息,通知所有的节点将故障节点标记为客观下线,fail消息的消息体只包含故障节点的ID。

广播fail消息是客观下线的最后一步,它承担着非常重要的职责:

​ 通知集群内所有的节点标记故障节点为客观下线状态并立刻生效。

​ 通知故障节点的从节点触发故障转移流程。

故障恢复

​ 故障节点变为客观下线后,**如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它,**从而保证集群的高可用。下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节点进入客观下线时,将会触发故障恢复流程。

资格检查

​ 每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障的主节点。如果从节点与主节点断线时间超过cluster-node-time * cluster-slave-validity-factor,则当前从节点不具备故障转移资格。参数cluster-slave-validity-factor用于从节点的有效因子,默认为10。

准备选举时间

​ 当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该时间后才能执行后续流程。

​ 这里之所以采用延迟触发机制,主要是通过对多个从节点使用不同的延迟选举时间来支持优先级问题。复制偏移量越大说明从节点延迟越低,那么它应该具有更高的优先级来替换故障主节点。

​ 所有的从节点中复制偏移量最大的将提前触发故障选举流程,以保证复制延迟低的从节点优先发起选举。

发起选举

​ 当从节点定时任务检测到达故障选举时间(failover_auth_time)到达后,发起选举流程如下:

(1)更新配置纪元

​ 配置纪元是一个只增不减的整数,每个主节点自身维护一个配置纪元(clusterNode .configEpoch)标示当前主节点的版本,所有主节点的配置纪元都不相等,从节点会复制主节点的配置纪元。整个集群又维护一个全局的配置纪元(clusterstate.currentEpoch),用于记录集群内所有主节点配置纪元的最大版本。执行cluster info命令可以查看配置纪元信息:

配置纪元的主要作用:

​ 标示集群内每个主节点的不同版本和当前集群最大的版本。

​ 每次集群发生重要事件时,这里的重要事件指出现新的主节点(新加入的或者由从节点转换而来),从节点竞争选举。都会递增集群全局的配置纪元并赋值给相关主节点,用于记录这一关键事件。

​ 主节点具有更大的配置纪元代表了更新的集群状态,因此当节点间进行ping/pong消息交换时,如出现slots等关键信息不一致时,以配置纪元更大的一方为准,防止过时的消息状态污染集群。

配置纪元的应用场景有:

​ 新节点加入。槽节点映射冲突检测。从节点投票选举冲突检测。

选举投票

只有持有槽的主节点才会处理故障选举消息(FAILOVER_AUTH_REQUEST),因为每个持有槽的节点在一个配置纪元内都有唯一的一张选票,当接到第一个请求投票的从节点消息时回复FAILOVER_AUTH_ACK消息作为投票,之后相同配置纪元内其他从节点的选举消息将忽略。

​ 投票过程其实是一个领导者选举的过程,如集群内有N个持有槽的主节点代表有N张选票。由于在每个配置纪元内持有槽的主节点只能投票给一个从节点,因此只能有一个从节点获得 N/2+1的选票,保证能够找出唯一的从节点

Redis集群没有直接使用从节点进行领导者选举,主要因为从节点数必须大于等于3个才能保证凑够N/2+1个节点,将导致从节点资源浪费。使用集群内所有持有槽的主节点进行领导者选举,即使只有一个从节点也可以完成选举过程。

​ 当从节点收集到N/2+1个持有槽的主节点投票时,从节点可以执行替换主节点操作,例如集群内有5个持有槽的主节点,主节点b故障后还有4个,当其中一个从节点收集到3张投票时代表获得了足够的选票可以进行替换主节点操作,。

​ 投票作废:每个配置纪元代表了一次选举周期,如果在开始投票之后的cluster-node-timeout*2时间内从节点没有获取足够数量的投票,则本次选举作废。从节点对配置纪元自增并发起下一轮投票,直到选举成功为止。

替换主节点

当从节点收集到足够的选票之后,触发替换主节点操作:

1)当前从节点取消复制变为主节点。

2)执行clusterDelslot 操作撤销故障主节点负责的槽,并执行clusterAddSlot把这些槽委派给自己。

3)向集群广播自己的pong消息,通知集群内所有的节点当前从节点变为主节点并接管了故障主节点的槽信息。

故障转移时间

在介绍完故障发现和恢复的流程后,这时我们可以估算出故障转移时间:

1)主观下线(pfail)识别时间=cluster-node-timeout。

2)主观下线状态消息传播时间<=cluster-node-timeout/2。消息通信机制对超过cluster-node-timeout/2未通信节点会发起ping消息,消息体在选择包含哪些节点时会优先选取下线状态节点,所以通常这段时间内能够收集到半数以上主节点的pfail 报告从而完成故障发现。

3)从节点转移时间<=1000毫秒。由于存在延迟发起选举机制,偏移量最大的从节点会最多延迟1秒发起选举。通常第一次选举就会成功,所以从节点执行转移时间在1秒以内。

根据以上分析可以预估出故障转移时间,如下:

failover-time(毫秒)≤cluster-node-timeout + cluster-node-timeout/2 + 1000

因此,故障转移时间跟cluster-node-timeout参数息息相关,默认15秒。配置时可以根据业务容忍度做出适当调整,但不是越小越好。

集群不可用判定

​ 为了保证集群完整性,默认情况下当集群16384个槽任何一个没有指派到节点时整个集群不可用。执行任何键命令返回( error)CLUSTERDOWN Hash slot not served错误。这是对集群完整性的一种保护措施,保证所有的槽都指派给在线的节点。但是当持有槽的主节点下线时,从故障发现到自动完成转移期间整个集群是不可用状态,对于大多数业务无法容忍这种情况,因此可以将参数cluster-require-full-coverage配置为no,当主节点故障时只影响它负责槽的相关命令执行,不会影响其他主节点的可用性。

但是从集群的故障转移的原理来说,集群会出现不可用,当:

1、当访问一个 Master 和 Slave 节点都挂了的时候,cluster-require-full-coverage=yes,会报槽无法获取。

2、集群主库半数宕机(根据 failover 原理,fail 掉一个主需要一半以上主都投票通过才可以)。

​ 另外,当集群 Master 节点个数小于 3 个的时候,或者集群可用节点个数为偶数的时候,基于 fail 的这种选举机制的自动主从切换过程可能会不能正常工作,一个是标记 fail 的过程,一个是选举新的 master 的过程,都有可能异常。

集群读写分离
  1. 只读连接

​ 集群模式下从节点不接受任何读写请求,发送过来的键命令会重定向到负责槽的主节点上(其中包括它的主节点)。当需要使用从节点分担主节点读压力时,可以使用readonly命令打开客户端连接只读状态。之前的复制配置slave-read-only在集群模式下无效。当开启只读状态时,从节点接收读命令处理流程变为:如果对应的槽属于自己正在复制的主节点则直接执行读命令,否则返回重定向信息。

​ readonly命令是连接级别生效,因此每次新建连接时都需要执行readonly开启只读状态。执行readwrite命令可以关闭连接只读状态。

  1. 读写分离

​ 集群模式下的读写分离,同样会遇到:复制延迟,读取过期数据,从节点故障等问题。针对从节点故障问题,客户端需要维护可用节点列表,集群提供了cluster slaves {nodeld}命令,返回nodeId对应主节点下所有从节点信息,命令如下:

cluster slaves 210bb9bb737488b91de2c47355020c89af284f63

127.0.0.1:6902> cluster slaves d8f0b8f6aa785576a4e2b4ca2234fced6520eb2a
1) "ed7264f34bdb8972ba0cf52806cf974821a0e35f 127.0.0.1:6930@16930 slave d8f0b8f6aa785576a4e2b4ca2234fced6520eb2a 0 1641204188065 3 connected"

解析以上从节点列表信息,排除fail状态节点,这样客户端对从节点的故障判定可以委托给集群处理,简化维护可用从节点列表难度。

同时集群模式下读写分离涉及对客户端修改如下:

1)维护每个主节点可用从节点列表。

2)针对读命令维护请求节点路由。

3)从节点新建连接开启readonly状态。

集群模式下读写分离成本比较高,可以直接扩展主节点数量提高集群性能,一般不建议集群模式下做读写分离。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

岁月玲珑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值