• 技术文章 >数据库 >Redis

    带你吃透Redis中的主从复制、Sentinel、集群

    青灯夜游青灯夜游2021-10-15 11:15:01转载198
    本篇文章给大家介绍一下Redis分布式的相关知识,带大家吃透主从复制、Sentinel、集群,让你的Redis水平更上一层!

    一、主从复制

    1、简介

    主从复制是Redis分布式的基石,也是Redis高可用的保障。在Redis中,被复制的服务器称为主服务器(Master),对主服务器进行复制的服务器称为从服务器(Slave)。【相关推荐:Redis视频教程

    主-从.png

    主从复制的配置非常简单,有三种方式(其中IP-主服务器IP地址/PORT-主服务器Redis服务端口):

    2、主从复制的演进

    Redis的主从复制机制,并不是一开始就像6.x版本一样完善,而是一个版本一个版本迭代而来的。它大体上经过三个版本的迭代:

    随着版本的增长,Redis主从复制机制逐渐完善;但是他们的本质都是围绕同步(sync)和命令传播(command propagate)两个操作展开:

    2.1 版本2.8以前

    2.1.1 同步

    2.8以前的版本,从服务器对主服务器的同步需要从服务器向主服务器发生sync命令来完成:

    2.8版本.png

    2.1.2 命令传播

    当同步工作完成之后,主从之间需要通过命令传播来维持数据状态的一致性。 如下图,当前主从服务器之间完成同步工作之后,主服务接收客户端的DEL K6指令后删除了K6,此时从服务器仍然存在K6,主从数据状态并不一致。为了维持主从服务器状态一致,主服务器会将导致自己数据状态发生改变的命令传播到从服务器执行,当从服务器也执行了相同的命令之后,主从服务器之间的数据状态将会保持一致。

    2.8主从同步+命令传播.png

    2.1.3 缺陷

    从上面看不出2.8以前版本的主从复制有什么缺陷,这是因为我们还没有考虑网络波动的情况。了解分布式的兄弟们肯定听说过CAP理论,CAP理论是分布式存储系统的基石,在CAP理论中P(partition网络分区)必然存在,Redis主从复制也不例外。当主从服务器之间出现网络故障,导致一段时间内从服务器与主服务器之间无法通信,当从服务器重新连接上主服务器时,如果主服务器在这段时间内数据状态发生了改变,那么主从服务器之间将出现数据状态不一致。 在Redis 2.8以前的主从复制版本中,解决这种数据状态不一致的方式是通过重新发送sync命令来实现。虽然sync能保证主从服务器数据状态一致,但是很明显sync是一个非常消耗资源的操作。

    sync命令执行,主从服务器需要占用的资源:

    从上面三点可以看出,sync命令不仅会导致主服务器的响应能力下降,也会导致从服务器在此期间拒绝对外提供服务。

    2.2 版本2.8-4.0

    2.2.1 改进点

    针对2.8以前的版本,Redis在2.8之后对从服务器重连后的数据状态同步进行了改进。改进的方向是减少全量同步(full resynchronizaztion)的发生,尽可能使用增量同步(partial resynchronization)。在2.8版本之后使用psync命令代替了sync命令来执行同步操作,psync命令同时具备全量同步和增量同步的功能:

    2.2.2 psync如何实现

    Redis为了实现从服务器断线重连后的增量同步,增加了三个辅助参数:

    2.2.2.1 复制偏移量

    在主服务器和从服务器内都会维护一个复制偏移量

    正常同步的情况如下:

    偏移量.png

    通过对比主从服务器之间的复制偏移量是否相等,能够得知主从服务器之间的数据状态是否保持一致。 假设此时A/B正常传播,C从服务器断线,那么将出现如下情况:

    偏移量+断线.png

    很明显有了复制偏移量之后,从服务器C断线重连后,主服务器只需要发送从服务器缺少的100字节数据即可。但是主服务器又是如何知道从服务器缺少的是那些数据呢?

    2.2.2.2 复制积压缓冲区

    复制积压缓冲区是一个固定长度的队列,默认为1MB大小。当主服务器数据状态发生改变,主服务器将数据同步给从服务器的同时会另存一份到复制积压缓冲区中。

    复制积压缓冲区.png

    复制积压缓冲区为了能和偏移量进行匹配,它不仅存储了数据内容,还记录了每个字节对应的偏移量:

    复制积压缓冲区+字节值+偏移量.png

    当从服务器断线重连后,从服务器通过psync命令将自己的复制偏移量(offset)发送给主服务器,主服务器便可通过这个偏移量来判断进行增量传播还是全量同步。

    Redis的复制积压缓冲区的大小默认为1MB,如果需要自定义应该如何设置呢? 很明显,我们希望能尽可能的使用增量同步,但是又不希望缓冲区占用过多的内存空间。那么我们可以通过预估Redis从服务断线后重连的时间T,Redis主服务器每秒接收的写命令的内存大小M,来设置复制积压缓冲区的大小S。

    S = 2 * M * T

    注意这里扩大2倍是为了留有一定的余地,保证绝大部分的断线重连都能采用增量同步。

    2.2.2.3 服务器运行 ID

    看到这里是不是再想上面已经可以实现断线重连的增量同步了,还要运行ID干嘛?其实还有一种情况没考虑,就是当主服务器宕机后,某台从服务器被选举成为新的主服务器,这种情况我们就通过比较运行ID来区分。

    2.2.3 完整的psync

    完整的psync过程非常的复杂,在2.8-4.0的主从复制版本中已经做到了非常完善。psync命令发送的参数如下:

    psync

    当从服务器没有复制过任何主服务器(并不是主从第一次复制,因为主服务器可能会变化,而是从服务器第一次全量同步),从服务器将会发送:

    psync ? -1

    psync.png

    一起完整的psync流程如下图:

    一次完整的psync.png

    2.3 版本4.0

    Redis 2.8-4.0版本仍然有一些改进的空间,当主服务器切换时,是否也能进行增量同步呢?因此Redis 4.0版本针对这个问题做了优化处理,psync升级为psync2.0。 psync2.0 抛弃了服务器运行ID,采用了replid和replid2来代替,其中replid存储的是当前主服务器的运行ID,replid2保存的是上一个主服务器运行ID。

    通过replid和replid2我们可以解决主服务器切换时,增量同步的问题:

    二、Sentinel

    1、简介

    主从复制奠定了Redis分布式的基础,但是普通的主从复制并不能达到高可用的状态。在普通的主从复制模式下,如果主服务器宕机,就只能通过运维人员手动切换主服务器,很显然这种方案并不可取。 针对上述情况,Redis官方推出了可抵抗节点故障的高可用方案——Redis Sentinel(哨兵)。Redis Sentinel(哨兵):由一个或多个Sentinel实例组成的Sentinel系统,它可以监视任意多个主从服务器,当监视的主服务器宕机时,自动下线主服务器,并且择优选取从服务器升级为新的主服务器。

    如下示例:当旧Master下线时长超过用户设定的下线时长上限,Sentinel系统就会对旧Master执行故障转移操作,故障转移操作包含三个步骤:

    sentinel监视主服务器下线.png

    本文基于如下资源清单进行开展:

    IP地址节点角色端口
    192.168.211.104Redis Master/ Sentinel6379/26379
    192.168.211.105Redis Slave/ Sentinel6379/26379
    192.168.211.106Redis Slave/ Sentinel6379/26379

    2、Sentinel初始化与网络连接

    Sentinel并没有什么特别神奇的地方,它就是一个更加简单的Redis服务器,在Sentinel启动的时候它会加载不同的命令表和配置文件,因此从本质上来讲Sentinel就是一个拥有较少命令和部分特殊功能的Redis服务。当一个Sentinel启动时它需要经历如下步骤:

    2.1 初始化Sentinel服务器

    Sentinel本质上就是一个Redis服务器,因此启动Sentinel需要启动一个Redis服务器,但是Sentinel并不需要读取RDB/AOF文件来还原数据状态。

    2.2 替换普通Redis代码为Sentinel的专用代码

    Sentinel用于较少的Redis命令,大部分命令在Sentinel客户端都不支持,并且Sentinel拥有一些特殊的功能,这些需要Sentinel在启动时将Redis服务器使用的代码替换为Sentinel的专用代码。在此期间Sentinel会载入与普通Redis服务器不同的命令表。 Sentinel不支持SET、DBSIZE等命令;保留支持PING、PSUBSCRIBE、SUBSCRIBE、UNSUBSCRIBE、INFO等指令;这些指令在Sentinel工作中提供了保障。

    2.3 初始化Sentinel状态

    装载Sentinel的特有代码之后,Sentinel会初始化sentinelState结构,该结构用于存储Sentinel相关的状态信息,其中最重要的就是masters字典。

    struct sentinelState {
       
        //当前纪元,故障转移使用
     uint64_t current_epoch; 
      
        // Sentinel监视的主服务器信息 
        // key -> 主服务器名称 
        // value -> 指向sentinelRedisInstance指针
        dict *masters; 
        // ...
    } sentinel;

    2.4 初始化Sentinel监视的主服务器列表

    Sentinel监视的主服务器列表保存在sentinelState的masters字典中,当sentinelState创建之后,开始对Sentinel监视的主服务器列表进行初始化。

    主服务器的名字由我们sentinel.conf配置文件指定,如下主服务器名字为redis-master(我这里是一主二从的配置):

    daemonize yes
    port 26379
    protected-mode no
    dir "/usr/local/soft/redis-6.2.4/sentinel-tmp"
    sentinel monitor redis-master 192.168.211.104 6379 2
    sentinel down-after-milliseconds redis-master 30000
    sentinel failover-timeout redis-master 180000
    sentinel parallel-syncs redis-master 1

    sentinelRedisInstance实例保存了Redis服务器的信息(主服务器、从服务器、Sentinel信息都保存在这个实例中)。

    typedef struct sentinelRedisInstance {
     
        // 标识值,标识当前实例的类型和状态。如SRI_MASTER、SRI_SLVAE、SRI_SENTINEL
        int flags;
        
        // 实例名称 主服务器为用户配置实例名称、从服务器和Sentinel为ip:port
        char *name;
        
        // 服务器运行ID
        char *runid;
        
        //配置纪元,故障转移使用
     uint64_t config_epoch; 
        
        // 实例地址
        sentinelAddr *addr;
        
        // 实例判断为主观下线的时长 sentinel down-after-milliseconds redis-master 30000
        mstime_t down_after_period; 
        
        // 实例判断为客观下线所需支持的投票数 sentinel monitor redis-master 192.168.211.104 6379 2
        int quorum;
        
        // 执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量 sentinel parallel-syncs redis-master 1
        int parallel-syncs;
        
        // 刷新故障迁移状态的最大时限 sentinel failover-timeout redis-master 180000
     mstime_t failover_timeout;
        
        // ...
    } sentinelRedisInstance;

    根据上面的一主二从配置将会得到如下实例结构:

    初始实例结构.png

    2.5 创建连接主服务器的网络连接

    当实例结构初始化完成之后,Sentinel将会开始创建连接Master的网络连接,这一步Sentinel将成为Master的客户端。 Sentinel和Master之间会创建一个命令连接和一个订阅连接:

    命令连接和订阅连接.png

    Sentinel在创建命令连接完成之后,每隔10秒钟向Master发送一次INFO指令,通过Master的回复信息可以获得两方面的知识:

    主从信息.png

    2.6 创建连接从服务器的网络连接

    根据主服务获取从服务器信息,Sentinel可以创建到Slave的网络连接,Sentinel和Slave之间也会创建命令连接和订阅连接。

    Slave命令连接和订阅连接.png

    当Sentinel和Slave之间创建网络连接之后,Sentinel成为了Slave的客户端,Sentinel也会每隔10秒钟通过INFO指令请求Slave获取服务器信息。 到这一步Sentinel获取到了Master和Slave的相关服务器数据。这其中比较重要的信息如下:

    此时实例结构信息如下所示:

    从服务器信息.png

    2.7 创建Sentinel之间的网络连接

    此时是不是还有疑问,Sentinel之间是怎么互相发现对方并且相互通信的,这个就和上面Sentinel与自己监视的主从之间订阅_sentinel_:hello频道有关了。 Sentinel会与自己监视的所有Master和Slave之间订阅_sentinel_:hello频道,并且Sentinel每隔2秒钟向_sentinel_:hello频道发送一条消息,消息内容如下:

    PUBLISH sentinel:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_ip>,<m_port>,<m_runid>,<m_epoch>"

    其中s代码Sentinel,m代表Master;ip表示IP地址,port表示端口、runid表示运行id、epoch表示配置纪元。

    多个Sentinel在配置文件中会配置相同的主服务器ip和端口信息,因此多个Sentinel均会订阅_sentinel_:hello频道,通过频道接收到的信息就可获取到其他Sentinel的ip和port,其中有如下两点需要注意:

    Sentinel之间不会创建订阅连接,它们只会创建命令连接:

    sentinel之间的命令连接.png

    此时实例结构信息如下所示:

    sentinel服务器信息.png

    3、Sentinel工作

    Sentinel最主要的工作就是监视Redis服务器,当Master实例超出预设的时限后切换新的Master实例。这其中有很多细节工作,大致分为检测Master是否主观下线、检测Master是否客观下线、选举领头Sentinel、故障转移四个步骤。

    3.1 检测Master是否主观下线

    Sentinel每隔1秒钟,向sentinelRedisInstance实例中的所有Master、Slave、Sentinel发送PING命令,通过其他服务器的回复来判断其是否仍然在线。

    sentinel down-after-milliseconds redis-master 30000

    在Sentinel的配置文件中,当Sentinel PING的实例在连续down-after-milliseconds配置的时间内返回无效命令,则当前Sentinel认为其主观下线。Sentinel的配置文件中配置的down-after-milliseconds将会对其sentinelRedisInstance实例中的所有Master、Slave、Sentinel都适应。

    无效指令指的是+PONG、-LOADING、-MASTERDOWN之外的其他指令,包括无响应

    如果当前Sentinel检测到Master处于主观下线状态,那么它将会修改其sentinelRedisInstance的flags为SRI_S_DOWN

    主观下线状态修改.png

    3.2 检测Master是否客观下线

    当前Sentinel认为其下线只能处于主观下线状态,要想判断当前Master是否客观下线,还需要询问其他Sentinel,并且所有认为Master主观下线或者客观下线的总和需要达到quorum配置的值,当前Sentinel才会将Master标志为客观下线。

    客观下线状态修改.png

    当前Sentinel向sentinelRedisInstance实例中的其他Sentinel发送如下命令:

    SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>

    current_epoch和runid均用于Sentinel的选举,Master下线之后,需要选举一个领头Sentinel来选举一个新的Master,current_epoch和runid在其中发挥着重要作用,这个后续讲解。

    接收到命令的Sentinel,会根据命令中的参数检查主服务器是否下线,检查完成后会返回如下三个参数:

    询问其他sentinel.png

    3.3 选举领头Sentinel

    down_state返回1,证明接收is-master-down-by-addr命令的Sentinel认为该Master也主观下线了,如果down_state返回1的数量(包括本身)大于等于quorum(配置文件中配置的值),那么Master正式被当前Sentinel标记为客观下线。 此时,Sentinel会再次发送如下指令:

    SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>

    此时的runid将不再是0,而是Sentinel自己的运行id(runid)的值,表示当前Sentinel希望接收到is-master-down-by-addr命令的其他Sentinel将其设置为领头Sentinel。这个设置是先到先得的,Sentinel先接收到谁的设置请求,就将谁设置为领头Sentinel。 发送命令的Sentinel会根据其他Sentinel回复的结果来判断自己是否被该Sentinel设置为领头Sentinel,如果Sentinel被其他Sentinel设置为领头Sentinel的数量超过半数Sentinel(这个数量在sentinelRedisInstance的sentinel字典中可以获取),那么Sentinel会认为自己已经成为领头Sentinel,并开始后续故障转移工作(由于需要半数,且每个Sentinel只会设置一个领头Sentinel,那么只会出现一个领头Sentinel,如果没有一个达到领头Sentinel的要求,Sentinel将会重新选举直到领头Sentinel产生为止)。

    3.4 故障转移

    故障转移将会交给领头sentinel全权负责,领头sentinel需要做如下事情:

    这其中最难的一步是如果选择最佳的新Master,领头Sentinel会做如下清洗和排序工作:

    新的Master产生后,领头sentinel会向已下线主服务器的其他从服务器(不包括新Master)发送SLAVEOF ip port命令,使其成为新master的slave。

    到这里Sentinel的的工作流程就算是结束了,如果新master下线,则循环流程即可!

    三、集群

    1、简介

    Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)进行数据共享,Redis集群主要实现了以下目标:

    关于Redis集群的学习,如果没有任何经验的弟兄们建议先看下这三篇文章(中文系列): Redis集群教程

    REDIS cluster-tutorial -- Redis中文资料站 -- Redis中国用户组(CRUG)

    Redis集群规范

    REDIS cluster-spec -- Redis中文资料站 -- Redis中国用户组(CRUG)

    Redis3主3从伪集群部署

    CentOS 7单机安装Redis Cluster(3主3从伪集群),仅需简单五步_李子捌的博客-CSDN博客

    下文内容依赖下图三主三从结构开展:

    8624ecbdf7f691d78ef48bc5afdb6d18.png

    资源清单:

    节点IP槽(slot)范围
    Master[0]192.168.211.107:6319Slots 0 - 5460
    Master[1]192.168.211.107:6329Slots 5461 - 10922
    Master[2]192.168.211.107:6339Slots 10923 - 16383
    Slave[0]192.168.211.107:6369
    Slave[1]192.168.211.107:6349
    Slave[2]192.168.211.107:6359

    Redis集群.png

    Redis集群.png

    2、集群内部

    Redis 集群没有使用一致性hash, 而是引入了 哈希槽的概念。Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,这种结构很容易添加或者删除节点。集群的每个节点负责一部分hash槽,比如上面资源清单的集群有3个节点,其槽分配如下所示:

    深入学习Redis集群之前,需要了解集群中Redis实例的内部结构。当某个Redis服务节点通过cluster_enabled配置为yes开启集群模式之后,Redis服务节点不仅会继续使用单机模式下的服务器组件,还会增加custerState、clusterNode、custerLink等结构用于存储集群模式下的特殊数据。

    如下三个数据承载对象一定要认真看,尤其是结构中的注释,看完之后集群大体上怎么工作的,心里就有数了,嘿嘿嘿;

    2.1 clsuterNode

    clsuterNode用于存储节点信息,比如节点的名字、IP地址、端口信息和配置纪元等等,以下代码列出部分非常重要的属性:

    typedef struct clsuterNode {
    
        // 创建时间
        mstime_t ctime;
        
        // 节点名字,由40位随机16进制的字符组成(与sentinel中讲的服务器运行id相同)
        char name[REDIS_CLUSTER_NAMELEN];
        
        // 节点标识,可以标识节点的角色和状态
        // 角色 -> 主节点或从节点 例如:REDIS_NODE_MASTER(主节点) REDIS_NODE_SLAVE(从节点)
        // 状态 -> 在线或下线 例如:REDIS_NODE_PFAIL(疑似下线) REDIS_NODE_FAIL(下线) 
        int flags;
        
        // 节点配置纪元,用于故障转移,与sentinel中用法类似
        // clusterState中的代表集群的配置纪元
        unit64_t configEpoch;
        
        // 节点IP地址
        char ip[REDIS_IP_STR_LEN];
        
        // 节点端口
        int port;
        
        // 连接节点的信息
        clusterLink *link;
        
        // 一个2048字节的二进制位数组
        // 位数组索引值可能为0或1
        // 数组索引i位置值为0,代表节点不负责处理槽i
        // 数组索引i位置值为1,代表节点负责处理槽i
        unsigned char slots[16384/8];
        
        // 记录当前节点处理槽的数量总和
        int numslots;
        
        // 如果当前节点是从节点
        // 指向当前从节点的主节点
        struct clusterNode *slaveof;
        
        // 如果当前节点是主节点
        // 正在复制当前主节点的从节点数量
        int numslaves;
        
        // 数组——记录正在复制当前主节点的所有从节点
        struct clusterNode **slaves;
        
    } clsuterNode;

    上述代码中可能不太好理解的是slots[16384/8],其实可以简单的理解为一个16384大小的数组,数组索引下标处如果为1表示当前槽属于当前clusterNode处理,如果为0表示不属于当前clusterNode处理。clusterNode能够通过slots来识别,当前节点处理负责处理哪些槽。 初始clsuterNode或者未分配槽的集群中的clsuterNode的slots如下所示:

    初始slots[16384_8].png

    假设集群如上面我给出的资源清单,此时代表Master[0]的clusterNode的slots如下所示:

    Master[0]的clusterNode的slots.png

    clusterLink是clsuterNode中的一个属性,用于存储连接节点所需的相关信息,比如套接字描述符、输入输出缓冲区等待,以下代码列出部分非常重要的属性:

    typedef struct clusterState {
    
        // 连接创建时间
        mstime_t ctime;
       
        // TCP 套接字描述符
        int fd;
        
        // 输出缓冲区,需要发送给其他节点的消息缓存在这里
        sds sndbuf;
        
        // 输入缓冲区,接收打其他节点的消息缓存在这里
        sds rcvbuf;
        
        // 与当前clsuterNode节点代表的节点建立连接的其他节点保存在这里
        struct clusterNode *node;
    } clusterState;

    2.3 custerState

    每个节点都会有一个custerState结构,这个结构中存储了当前集群的全部数据,比如集群状态、集群中的所有节点信息(主节点、从节点)等等,以下代码列出部分非常重要的属性:

    typedef struct clusterState {
    
        // 当前节点指针,指向一个clusterNode
        clusterNode *myself;
        
        // 集群当前配置纪元,用于故障转移,与sentinel中用法类似
        unit64_t currentEpoch;
        
        // 集群状态 在线/下线
        int state;
        
        // 集群中处理着槽的节点数量总和
        int size;
        
        // 集群节点字典,所有clusterNode包括自己
        dict *node;
        
        // 集群中所有槽的指派信息
        clsuterNode *slots[16384];
        
        // 用于槽的重新分配——记录当前节点正在从其他节点导入的槽
        clusterNode *importing_slots_from[16384];
        
        // 用于槽的重新分配——记录当前节点正在迁移至其他节点的槽
        clusterNode *migrating_slots_to[16384];
        
        // ...
        
    } clusterState;

    在custerState有三个结构需要认真了解的,第一个是slots数组,clusterState中的slots数组与clsuterNode中的slots数组是不一样的,在clusterNode中slots数组记录的是当前clusterNode所负责的槽,而clusterState中的slots数组记录的是整个集群的每个槽由哪个clsuterNode负责,因此集群正常工作的时候clusterState的slots数组每个索引指向负责该槽的clusterNode,集群槽未分配之前指向null。

    如图展示资源清单中的集群clusterState中的slots数组与clsuterNode中的slots数组:

    clusterState中Slots数组.png

    Redis集群中使用两个slots数组的原因是出于性能的考虑:

    第二个需要认真了解的结构是node字典,该结构虽然简单,但是node字典中存储了所有的clusterNode,这也是Redis集群中的单个节点获取其他主节点、从节点信息的主要位置,因此我们也需要注意一下。 第三个需要认真了解的结构是importing_slots_from[16384]数组和migrating_slots_to[16384],这两个数组在集群重新分片时需要使用,需要重点了解,后面再说吧,这里说的话顺序不太对。

    3、集群工作

    3.1 槽(slot)如何指派?

    Redis集群一共16384个槽,如上资源清单我们在三主三从的集群中,每个主节点负责自己相应的槽,而在上面的三主三从部署的过程中并未看到我指定槽给对应的主节点,这是因为Redis集群自己内部给我们划分了槽,但是如果我们想自己指派槽该如何整呢? 我们可以向节点发送如下命令,将一个或多个槽指派给当前节点负责:

    CLUSTER ADDSLOTS

    比如我们想把0和1槽指派给Master[0],我们只需要想Master[0]节点发送如下命令即可:

    CLUSTER ADDSLOTS 0 1

    当节点被指派了槽后,会将clusterNode的slots数组更新,节点会将自己负责处理的槽也就是slots数组通过消息发送给集群中的其他节点,其他节点在接收当消息后会更新对应clusterNode的slots数组以及clusterState的solts数组。

    3.2 ADDSLOTS 在Redis集群内部是如何实现的呢?

    这个其实也比较简单,当我们向Redis集群中的某个节点发送CLUSTER ADDSLOTS命令时,当前节点首先会通过clusterState中的slots数组来确认指派给当前节点的槽是否没有指派给其他节点,如果已经指派了,那么会直接抛出异常,返回错误给指派的客户端。如果指派给当前节点的所有槽都未指派给其他节点,那么当前节点会将这些槽指派给自己。 指派主要有三个步骤:

    3.3 集群这么多节点,客户端怎么知道请求哪个节点?

    在了解这个问题之前先要知道一个点,Redis集群是怎么计算当前这个键属于哪个槽的呢?根据官网的介绍,Redis其实并未使用一致性hash算法,而是将每个请求的key通过CRC16校验后对16384取模来决定放置到哪个槽中。

    HASH_SLOT = CRC16(key) mod 16384

    此时,当客户端连接向某个节点发送请求时,当前接收到命令的节点首先会通过算法计算出当前key所属的槽i,计算完后当前节点会判断clusterState的槽i是否由自己负责,如果恰好由自己负责那么当前节点就会之间响应客户端的请求,如果不由当前节点负责,则会经历如下步骤:

    3.4 如果我想将已经分配给A节点的槽重新分配给B节点,怎么整?

    这个问题其实涵括了很多问题,比如移除Redis集群中的某些节点,增加节点等都可以概括为把哈希槽从一个节点移动到另外一个节点。并且Redis集群非常牛逼的一点也在这里,它支持在线(不停机)的分配,也就是官方说集群在线重配置(live reconfiguration )。

    在将实现之前先来看下CLUSTER的指令,指令会了操作就会了:

    CLUSTER 用于槽分配的指令主要有如上这些,ADDSLOTS 和DELSLOTS主要用于槽的快速指派和快速删除,通常我们在集群刚刚建立的时候进行快速分配的时候才使用。CLUSTER SETSLOT slot NODE node也用于直接给指定的节点指派槽。如果集群已经建立我们通常使用最后两个来重分配,其代表的含义如下所示:

    上面这两句话是不是感觉不太看的懂,这是官方的描述,不太懂的话我来给你通俗的描述,整个流程大致如下步骤:

    3.5 如果客户端访问的key所属的槽正在迁移怎么办?

    优秀的你总会想到这种并发情况,牛皮呀!大佬们!

    u=79087421,2199932123&fm=26&fmt=auto.webp

    这个问题官方也考虑了,还记得我们在聊clusterState结构的时候么?importing_slots_from和migrating_slots_to就是用来处理这个问题的。

    typedef struct clusterState {
    
        // ...
        
        // 用于槽的重新分配——记录当前节点正在从其他节点导入的槽
        clusterNode *importing_slots_from[16384];
        
        // 用于槽的重新分配——记录当前节点正在迁移至其他节点的槽
        clusterNode *migrating_slots_to[16384];
        
        // ...
        
    } clusterState;

    有了上述两个相互数组,就能判断当前槽是否在迁移了,而且从哪里迁移来,要迁移到哪里去?搞笑不就是这么简单……

    此时,回到问题中,如果客户端请求的key刚好属于正在迁移的槽。那么接收到命令的节点首先会尝试在自己的数据库中查找键key,如果这个槽还没迁移完成,且当前key刚好也还没迁移完成,那就直接响应客户端的请求就行。如果该key已经不在了,此时节点会去查询migrating_slots_to数组对应的索引槽,如果索引处的值不为null,而是指向了某个clusterNode结构,那说明这个key已经被迁移到这个clusterNode了。这个时候节点不会继续在处理指令,而是返回ASKING命令,这个命令也会携带导入槽clusterNode对应的ip和port。客户端在接收到ASKING命令之后就需要将请求转向正确的节点了,不过这里有一点需要注意的地方**(因此我放个表情包在这里,方便读者注意)。**

    u=1042078930,3579951952&fm=26&fmt=auto.webp

    前面说了,当节点发现当前槽不属于自己处理时会返回MOVED指令,那么在迁移中的槽时怎么处理的呢?这个Redis集群是这个玩的。 节点发现槽正在迁移则向客户端返回ASKING命令,客户端会接收到ASKING命令,其中包含了槽迁入的clusterNode的节点ip和port。那么客户端首先会向迁入的clusterNode发送一条ASKING命令,这个命令必须要发目的是告诉当前节点,你要破例处理这次请求,因为这个槽已经迁移到你这里了,你不能直接拒绝我(因此如果Redis未接收到ASKING命令,会直接查询节点的clusterState,而正在迁移中的槽还没有更新到clusterState中,那么只能直接返回MOVED,这样不就会一直循环很多次……),接收到ASKING命令的节点会强制执行一次这个请求(只执行一次,下次再来需要重新提前发送ASKING命令)。

    4、集群故障

    Redis集群故障比较简单,这个和sentinel中主节点宕机或者在指定最长时间内未响应,重新在从节点中选举新的主节点的方式其实差不多。当然前提是Redis集群中的每个主节点,我们提前设置了从节点,要不就嘿嘿嘿……没戏。其大致步骤如下:

    这里我写得非常模糊,如果需要细致挖掘的一定要看这篇文章:

    REDIS cluster-spec -- Redis中文资料站 -- Redis中国用户组(CRUG)

    http://redis.cn/topics/cluster-spec.html

    或者可以看下黄健宏老师的《Redis设计与实现》这本书写得挺好,我也参考了很多内容。

    更多编程相关知识,请访问:编程视频!!

    以上就是带你吃透Redis中的主从复制、Sentinel、集群的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:csdn,如有侵犯,请联系admin@php.cn删除
    上一篇:深入解析Redis中的Info指令 下一篇:2021年Redis高频面试题分享(附答案分析)
    线上培训班

    相关文章推荐

    • 深入了解Redis中主从架构数据一致性同步原理• Redis中如何实现无畏宕机快速恢复和持久化• 手把手带你使用Redis+Bitmap实现亿级海量数据统计• 一文了解redis中RDB和AOP持久化• 一起来聊聊redis中的主从复制• 浅谈Redis中布隆过滤器的安装和配置方法

    全部评论我要评论

  • 取消发布评论发送
  • 1/1

    PHP中文网