【Redis-10】Redis集群的实现原理(Redis Cluster)

news2024/9/28 17:35:56

 Redis集群是Redis提供的分布式数据库方案,通过分片来进行数据共享,实现复制和故障转移的功能。

1. Redis集群节点

 一个Redis集群由多个节点组成,多个节点通过命令连接,由独立状态转为集群状态,命令是cluster meet <ip> <port>,其中在A节点上执行此命令,指定B节点的ip地址和端口号,两个节点通过握手的方式,A节点就加入到B节点所在的集群中。
 每一个Redis服务器默认是运行在单机模式下的,如果想开启集群模式,需要在配置文件中开启配置cluster-enabled设置为yes。节点在开启集群模式后,会继续使用单机模式中的服务器组件,比如会继续执行serverCron函数、正常保存键值对数据,进行持久化动作等等。除此之外,单个节点还会继续使用Redis服务器的redisServer结构来保存服务器的状态,使用redisClient来保存客户端的状态。至于跟集群相关的属性,则使用了其他三种属性结构,下面就看来下这些结构。

1.1 clusterNode结构

 clusterNode结构用来保存节点当前的状态信息,每个节点都会有一个clusterNode结构来保存自身相关状态,包含但不限于节点的创建时间、节点名称、配置纪元、ip和端口号等信息,贴源码来看下:

typedef struct clusterNode {
	// 节点创建时间
    mstime_t ctime; /* Node object creation time. */
    // 节点名称
    char name[CLUSTER_NAMELEN]; /* Node name, hex string, sha1-size */
    // 节点目前所处的状态信息
    int flags;      /* CLUSTER_NODE_... */
    // 节点当前的配置纪元
    uint64_t configEpoch; /* Last configEpoch observed for this node */
    // 节点的ip地址
    char ip[NET_IP_STR_LEN];  /* Latest known IP address of this node */
    // 节点的端口号
    int port;                   /* Latest known clients port of this node */
    // 与当前节点的连接节点相关的信息(如套接字描述符、输入和输出缓冲区)
    clusterLink *link;          /* TCP/IP link with this node */
    ...
} clusterNode;

1.2 clusterLink结构

 这个结构表示连接节点有关的信息,保存了连接节点的有关信息,重要的有套接字描述符、输入缓冲区和输出缓冲区等,贴代码看下:

typedef struct clusterLink {
	// 连接的创建时间
    mstime_t ctime;             /* Link creation time */
    // 套接字描述符
    int fd;                     /* TCP socket file descriptor */
    // 输出缓冲区,保存着等待发送给其他节点的消息
    sds sndbuf;                 /* Packet send buffer */
    // 输入缓冲区,保存着从其他节点接收到的消息
    sds rcvbuf;                 /* Packet reception buffer */
    // 与这个链接相关联的节点,如果没有就是空
    struct clusterNode *node;   /* Node related to this link if any, or NULL */
} clusterLink;

1.3 clusterState结构

 这个结构记录了在当前节点的视角下,集群所处的状态,如集群是上线还是下线的状态、有多少个节点,配置纪元等信息。先来贴代码看一下:

typedef struct clusterState {
	// 指向代码自身的clusterNode节点
    clusterNode *myself;  /* This node */
    // 集群当前的配置纪元
    uint64_t currentEpoch;
    // 集群当前状态(上线还是下线)
    int state;            /* CLUSTER_OK, CLUSTER_FAIL, ... */
    // 集群中节点的数量
    int size;             /* Num of master nodes with at least one slot */
    // 集群中的节点名单(是包含了myself)
    dict *nodes;          /* Hash table of name -> clusterNode structures */
    ...
} clusterState;

 所以看一下clusterState和clusterNode属性结构的图示,就如下图:

在这里插入图片描述

1.4 cluster meet命令的实现

 通过一个例子,看下这个命令的执行步骤。比如我们现在向节点A发送命令CLUSTER MEET 127.0.0.1 6380。其中6380是B节点:

  1. 首先A节点会为B节点创建一个clusterNode结构,并加入到自己的clusterState.nodes字典中;
  2. 节点A向节点B(127.0.0.1:6380)发送一条meet消息;
  3. 节点B收到消息,也会在自己这里创建一个clusterNode结构,并加入到自己的clusterState.nodes字典中;
  4. 节点B向节点A返回一条PONG消息;
  5. 节点A接收到PONG消息时,确认节点B接收成功,并向节点B返回一条PING消息;
  6. 节点B接收到节点A返回的PING消息,双方通信成功,握手动作完成。
  7. 节点B会将节点A的信息传播给集群中的其他节点(通过Gossip协议),并依次与A节点握手,最终A节点被集群中的所有节点所认识。

在这里插入图片描述

2. 槽指派

 Redis集群通过分片的方式来保存数据,集群中整个数据库状态是被拆分为16384个槽(slot),集群中每个节点都可以处理0~16384个槽位中的数据。需要注意的是,数据库中16384个槽必须都有对应的节点处理时,集群才处于上线的状态,否则集群不可用。我们可以通过CLUSTER INFO这个命令来查看所有槽的信息。
 我们现在通过cluster meet创建一个由三个结点组成的集群,分别是:

  • 127.0.0.1:7001
  • 127.0.0.1:7002
  • 127.0.0.1:7003

 通过cluster nodes命令可以看到三个结点组成的集群情况,如下:

127.0.0.1:7003> cluster nodes
fe75b9fa54c2d657fd9d02ff0bb4b1849 127.0.0.1:7003@17003 myself,master - 0 1671199866000 0 connected
df5936d9fdc9079e1e79b5cc65850be45 127.0.0.1:7001@17001 master - 0 1671199868687 2 connected
c91349f021123rbdd6849c33a9be75331 127.0.0.1:7002@17002 master - 0 1671199867654 1 connected

 我们再通过cluster info命令看一下这个集群的状态,如下,我们可以得出结论,由于槽没有进行任何的指派,所以集群当前是出于下线状态的(cluster_state:fail)。

127.0.0.1:7003> cluster info
cluster_state:fail
cluster_slots_assigned:0
cluster_slots_ok:0
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:3
cluster_size:0
cluster_current_epoch:2
cluster_my_epoch:0
cluster_stats_messages_ping_sent:146
cluster_stats_messages_pong_sent:138
cluster_stats_messages_meet_sent:1
cluster_stats_messages_sent:285
cluster_stats_messages_ping_received:138
cluster_stats_messages_pong_received:147
cluster_stats_messages_received:285

 下面我们通过cluster addslots命令把所有的槽分配给集群中的节点,把0~5000的槽指派给7001节点,把5001~10000的槽指派给7002节点,把10001~16383的槽指派给7003节点。执行的命令是:

$ redis-cli -p 7001 cluster addslots {0..5000} 
$ redis-cli -p 7002 cluster addslots {5001..10000} 
$ redis-cli -p 7003 cluster addslots {10001..16383} 

 现在在通过cluster nodescluster info来看下节点中槽的分配情况,可以看到按照我们的指派,所有槽都有了对应节点处理,此时集群就处于上线的状态。如下:
在这里插入图片描述

2.1 节点槽指派信息的记录

 在节点中,每个clusterNode结构有两个属性用于记录当前节点正在负责处理的槽,代码:

typedef struct clusterNode {
    // ...
    unsigned char slots[CLUSTER_SLOTS/8]; /* slots handled by this node */
    int numslots;   /* Number of slots handled by this node */
    // ...
} clusterNode;

 这里slots是一个二进制的位数组,长度是16384(占用2048个字节),每个元素都是0或者1的取值,1代表这个位的槽归属当前节点处理,0表示不负责处理。numslots表示负责的槽的数量,其实就是上面这个数组的长度。下面表示一个实例:
在这里插入图片描述

2.2 槽指派信息的传播

 一个节点除了记录自己的clusterNode负责的槽之外,还会将自己负责的槽通过消息发送给集群中所有的节点,通过此种方式,集群中所有的节点就知道了其余节点负责的槽信息,避免冲突。具体是这样的:
 比如节点A负责0~5000的槽,节点B通过消息得知后,会把自己clusterState.nodes字典中表示A节点的clusterNode找出来,并保存或更新这个节点的slots信息。同理,其他节点也会发送或者接受槽信息,最终达到互相传播的目的。

2.3 记录集群中槽的分配情况

 通过2.2我们其实已经知道了,集群中所有节点负责的槽位,但是redis还通过另一种方式记录了集群中槽位的指派情况,是通过clusterState.slots这个数组来表示的。

typedef struct clusterState {
    ...
    clusterNode *slots[CLUSTER_SLOTS];
    ...
} clusterState;

 这个slots数组每一项都是一个指针,指向负责这个槽的clusterNode结构。

在这里插入图片描述
 需要区别的是,2.1中的slots数组是在clusterNode结构中,而这里的slots数组是在clusterState结构中。那么为什么要重复记录这个槽指派信息呢?因为仅仅通过2.1中的槽位数组,想知道某个槽归哪个节点负责,需要遍历所有的节点,时间复杂度是O(N),而通过clusterState.slots,时间复杂度是O(1)。

 所以通过2.1和2.3中的数组,我们了解到:clusterState.slots记录了集群中所有槽的指派信息,而clusterNode.slots记录了这个节点负责的槽指派信息。现在,我们可以通过节点找到它负责的槽,也可以通过槽找到负责它的节点,时间复杂度都是O(1)。

 其实cluster addslots命令可以指派槽也是通过这两个数组实现的,首先把参数中的槽记录到clusterState.slots数组中,然后再把对应的clusterNodes.slots节点数组中的槽从0→1,最后再通过消息广播到集群中的其他节点。

3. 命令执行

 当所有的槽位都有对应的节点处理后,集群上线处于可用状态了。当客户端向集群中某个节点发送命令时,此节点会计算键值对的key经过计算后,是否由本节点处理,计算公式:

slot = CRC16(KEY) & 16383

 也就是经过CRC16算法计算key的校验和,再与16383做位与运算。命令cluster keyslot <key>就是通过此函数计算某个key所属于哪个槽,然后再查询clusterState.slots数组找对应的clusterNode节点。
 这里计算出来的slot可能不属于当前节点处理,那么节点会向客户端返回一个MOVED错误,并引导客户端转向正确的节点,从新发送命令,类似于http的重定向。如果计算的slot属于当前节点处理,会执行命令后返回。
在这里插入图片描述
MOVED错误命令的格式为:MOVED <slot> <ip> : <port>,其中slot是计算出来的键所在的槽,Ip和port是负责处理此槽的节点。
 集群节点和单机服务器在保存键值对及过期时间的方式是一样的,唯一区别是节点只能使用0号数据库,单机服务器没有此限制。此外,节点还会使用clusterState.slots_to_key来保存键值对和槽位的对应关系,使用跳跃表来实现。跳跃表的每个节点分值时槽号,节点成员是数据库键值,鉴于跳跃表分值可以重复的属性,允许存在槽号相同,键值对不同的情况。

typedef struct clusterState {
	...
    rax *slots_to_keys;
    ...
} clusterState;

4. 重新分片

 Redis集群的重新分片可以将指派给节点A的槽重新指派给其他节点,并且槽所属的键值对也会做对应的迁移。这种操作可以在线(online)进行,重新分片的过程中,集群不需要下线,迁移双方的节点都可以继续处理命令。
 重新分片的操作由Redis集群的管理软件redis-trib负责执行,redis-trib通过向源节点和目标节点发送命令来完成重新分片的动作。重新分片是通过clusterState结构的两个属性完成的,看代码如下:

typedef struct clusterState {
    ...
    clusterNode *migrating_slots_to[CLUSTER_SLOTS];
    clusterNode *importing_slots_from[CLUSTER_SLOTS];
    ...
} clusterState;

 migrating_slots_to和importing_slots_from是两个指针数组,长度都是16384,数组中每一项都指向一个clusterNode结构。
 migrating_slots_to表示当前节点作为源节点正在向其他节点迁移的槽,数组中每一项要么指向null,要么指向一个正在迁移的目标clusterNode结构。
 importing_slots_from表示当前节点作为目标节点正在接受其他节点迁移的槽,数组中的每一项要么指向null,要么指向一个正在迁移的源clusterNode结构。

4.1 ASK错误

 如果节点收到一个键值对的命令请求,并且key对应的槽正好属于当前节点,那么节点会在自己的数据库中查找key,如果找到的话,节点就会执行命令。
 与之相反,如果没能找到key,节点会检查clusterState.migrating_slots_to[i]对应的槽是不是正在处于迁移的状态,如果是,会向客户端返回ASK的错误,引导客户端到正在导入的目标节点去查找键key。
 这里需要注意MOVED错误和ASK错误:他们都会引导客户端转向其他节点,但是MOVED表示当前key不属于当前节点的槽指派,且集群没有进行重分片的操作;而ASK错误是节点在进行重分配过程中的临时措施,节点在找不到key切节点处于迁移槽的过程中,向客户端返回ASK错误进行重定向的操作。

5. 复制与故障转移

 Redis集群中的节点分为主节点和从节点,其中主节点用来处理槽信息,从节点用来复制从节点的数据,且在主节点下线时,进行故障转移代替主节点继续处理命令请求。

5.1 主从复制

 向集群中一个节点发送命令cluster replicate <node_id>,可以让此节点成为<node_id>指定节点的从节点。接受命令的节点会将clusterState.myself.slaveof设置为目标主节点。

typedef struct clusterNode {
	...
    struct clusterNode **slaves; /* pointers to slave nodes */
    struct clusterNode *slaveof; /* pointer to the master node. Note that it
    ...
} clusterNode;

slaves 属性数组表示如果当前是主节点(clusterNode.flags=REDIS_NODE_MASTER),数组中的每一项都指向自己的从节点。slaveof 属性表示如果当前是从节点(clusterNode.flags=REDIS_NODE_SLAVE),指针指向自己复制的主节点。
在这里插入图片描述

5.2 故障检测与转移

 当主节点因某些原因下线后,从节点经过选举升级为主节点,开始接替原来主节点处理槽,具体步骤如下:

  1. 主节点下线,从节点有一个会被选中为主节点;
  2. 从节点执行slave of no one,升级为主节点;
  3. 新的主节点会撤销原主节点的槽指派,并将这些槽指派给自己;
  4. 新的主节点会向集群中所有节点广播一个PONG消息,集群中所有节点都将知道此从节点已升级为主节点。
  5. 新的主节点开始接收和处理命令请求,故障转移完成。

 这里比较关键的一点是,主节点下线后,如何在众多的从节点中选举出一个合适的节点作为新的主节点。这里的选举机制和哨兵机制选举领头sentinel原理相似,都是基于Raft算法来实现的。

 集群中每个节点都会定期向其余节点发送 PING消息,以此检测对方是否在线,如果在指定时间内,有节点没有回复PONG消息,那么发送PING消息的节点就会将此节点设置为疑似下线。由于集群中每个节点都是通过互发消息来交互集群中所有节点的状态信息,当一个主节点A感知到集群中一半以上的主节点都将某个主节点B置为疑似下线后,A节点就会将B节点置为已下线,并且向集群中广播一条节点B已下线的消息,其余所有主节点都会将B节点标记已下线。

 当从节点通过消息得知自己复制的主节点已经下线了,此时故障转移就开始了。

  1. 从节点发现自己复制的主节点下线了,会向集群中所有的主节点发送消息,要求接收到消息的主节点将选举的票投给自己。
  2. 所有收到消息的主节点,会检查在此纪元内,自己有没有投过票。如果是首次投票,就将此票投给第一个发送消息的从节点并且记录,后面所有从节点要求投票的消息,都会拒绝。
  3. 发送投票消息的从节点,只要有一个首先收集到了 n/2 + 1 张票时,就会升级为新的主节点,本轮投票结束,所有节点纪元 +1。如果本轮投票,所有从节点都没有收集到足够的票数,会继续进行下一轮直到选举出新的主节点。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/96917.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C51——电动车简易防盗系统

这是电动车简易报警器信号电路 #include "reg52.h" sbit switcher P1^1; sbit D0_ON P1^2; sbit D1_OFF P1^3; sbit vibrator P1^4; void Delay3000ms() //11.0592MHz { unsigned char i, j, k; //_nop_(); i 22; j 3; k 227; …

生物素-双硫键-叠氮化物Biotin-SS-Azide CAS1620523-64-9 简介

名称&#xff1a;Biotin-SS-azide Azide-SS-biotin Azide-C2-SS-C2-biotin 是一种可降解 (cleavable) 的 ADC linker&#xff0c;可用于合成抗体偶联药物(ADC)。叠氮化物-SS-生物素是一种可裂解的生物素化试剂&#xff0c;用于使用点击化学标记含炔烃的生物分子。叠氮基与炔烃…

Mycat(2):mysql的集群搭建

MyCat的环境演示需要使用mysql集群 &#xff0c;下面先搭建mysql的环境 1 集群搭建概述 1.1 是什么 集群&#xff08;cluster&#xff09;技术是一种较新的技术&#xff0c;通过集群技术&#xff0c;可以在付出较低成本的情况下获得在性能、可靠性、灵活性方面的相对较高的收…

记一次新装的SQLServer本地无法访问的处理过程

本机新装的SQL Server连接不上, 首先尝试使用 计算机名\实例名 Windows 身份验证 的形式登录, 例如:Dell-WorkCenter\MSSQL2017 如果使用 计算机名\实例名 的形式可以登录, 但使用127.0.0.1或者本机IP地址无法登录的话, 有可能是Named Pipes 与 TCP/IP 协议没有启用, 开…

图书馆借阅数据分析系统设计与实现

开发工具(eclipse/idea/vscode等)&#xff1a; 数据库(sqlite/mysql/sqlserver等)&#xff1a; 功能模块(请用文字描述&#xff0c;至少200字)&#xff1a; 网站前台&#xff1a;关于图书馆、帮助信息、图书资讯、图书类型、图书信息 管理员&#xff1a; 1、管理&#xff1a;关…

大数据- 初探MapReduce

一、MapReduce编程实例——词频统计实现 启动hadoop服务 1、准备数据文件 &#xff08;1&#xff09;在虚拟机上创建文本文件 创建wordcount目录&#xff0c;在里面创建words.txt文件 &#xff08;2&#xff09;上传文件到HDFS指定目录 创建/wordcount/input目录&#…

设计模式之桥接模式

bridge design pattern 桥接模式的概念、桥接模式的结构、桥接模式的优缺点、桥接模式的使用场景、桥接模式的实现示例、桥接模式的源码分析 1、桥接模式的概念 桥接模式&#xff0c;即将抽象和实现分离&#xff0c;使他们可以独立变化。它是用组合关系来代替继承关系实现的&a…

赫夫曼树 | 实战演练

&#x1f388; 作者&#xff1a;Linux猿 &#x1f388; 简介&#xff1a;CSDN博客专家&#x1f3c6;&#xff0c;华为云享专家&#x1f3c6;&#xff0c;Linux、C/C、云计算、物联网、面试、刷题、算法尽管咨询我&#xff0c;关注我&#xff0c;有问题私聊&#xff01; &…

关于动漫的HTML网页设计:期末前端web大作业——海贼王基地(6个页面)

HTML实例网页代码, 本实例适合于初学HTML的同学。该实例里面有设置了css的样式设置&#xff0c;有div的样式格局&#xff0c;这个实例比较全面&#xff0c;有助于同学的学习,本文将介绍如何通过从头开始设计个人网站并将其转换为代码的过程来实践设计。 精彩专栏推荐&#x1f4…

QT—5种标准对话框使用详解

对话框是 GUI 程序中不可或缺的组成部分。一些不适合在主窗口实现的功能组件都必须放在对话框中设置。对话框通常会是一个顶层窗口&#xff0c;出现在程序最上层&#xff0c;用于实现短期任务或者简洁的用户交互。所谓标准对话框&#xff0c;是 Qt 内置的一系列对话框&#xff…

kafka问题总结

kafka问题总结【1】Kafka 都有哪些特点&#xff1f;【2】为什么要使用 kafka&#xff0c;为什么要使用消息队列&#xff1f;【2】kafka的使用场景【3】Kafka 的设计架构【4】kafka分区的目的【5】Kafka 是如何做到消息的有序性&#xff1f;【6】Kafka 的高可靠性是怎么实现的&a…

【操作系统-总论】发展历程、体系结构、虚拟机

文章目录1 操作系统的发展历程1.1 手工操作阶段1.2 批处理阶段1.2.1 单道批处理系统&#xff08;单道程序系统&#xff09;1.2.2 多道批处理系统&#xff08;多道程序系统&#xff09;1.3 分时操作系统1.4 实时操作系统2 操作系统的体系结构3 虚拟机1 操作系统的发展历程 1.1 …

Nginx教程(3)—负载均衡

文章目录3.1 负载均衡-轮询3.2 负载均衡-加权轮询3.3 upstream指令参数3.4 使用JMeter测试集群3.5 负载均衡之IP_hash3.6 一致性hash算法3.7 Nginx控制浏览器缓存3.8 Nginx反向代理缓存Nginx教程一 Nginx教程二 3.1 负载均衡-轮询 轮询是Nginx默认使用的策略&#xff0c;轮询算…

jmeter做压测性能调优:SSL上下文切换导致SSL频繁握手【杭州多测师_王sir】【杭州多测师】...

一、问题背景在使用 JMeter 压测时&#xff0c;发现同一后端服务&#xff0c;在单机 500 并发下&#xff0c;HTTP 和 HTTPS 协议压测 RT 差距非常大。同时观测后端服务各监控指标水位都很低&#xff0c;因此怀疑性能瓶颈在 JMeter 施压客户端。二、问题分析切入点&#xff1a;垃…

【无标题】大模型时代,视觉推理任务竟然只用语言数据也能学习

原文链接&#xff1a;https://www.techbeat.net/article-info?id4394 作者&#xff1a;seven_ 要让AI模型真正具备智能感知和认知的功能&#xff0c;我们就不得不把视觉分析和自然语言理解二者结合起来进行研究。AI大模型社区的成长为我们带来了很多极具想象力和创造力的新应用…

基于FPGA通过1Gb以太网低延迟传输专业级4K AV信号解决方案

ME10 SoC是全栈AV Over IP片上IP系统(SoC)&#xff0c;通过1Gb网络传输HDMI2.0 4K 4:4:4 的视频、音频和控制数据&#xff0c;ME10 SoC采用一个小的23 x 23毫米BGA封装。ME10主要特色为互通性和优越性能。 IPMX是AV的开放标准和规范的集合&#xff0c;专为专业AV市场开发的IP。…

【并发编程七】C++进程通信——套接字(socket)_80行代码实现一个聊天软件

【并发编程七】进程通信——套接字&#xff08;socket&#xff09;_80行代码实现一个聊天软件一、简介二、相关知识介绍1、winsock1.h、winsock2.h2、如何使用ws2_32.dll3、WSAStartup() 函数4、socket5、bind5、listen6、accept7、connect三、聊天软件的代码如下1、服务端2、客…

QT系列第5节 QT中常用输入控件

QT中经常利用控件来获取用户输入数据&#xff0c;本篇将介绍常用的用户输入控件 目录 (1) QSpinBox (2) QDoubleSpinBox &#xff08;3&#xff09;QSlider &#xff08;4&#xff09; QScrollBar &#xff08;5&#xff09;QProgressBar &#xff08;6&#xff09;QDia…

@MapperScan原理探究

1. 前言 MyBatis在整合Spring的时候&#xff0c;只需要加如下注解&#xff0c;就可以将Mapper实例注册到IOC容器交给Spring管理&#xff0c;它是怎么做到的呢&#xff1f;&#xff1f;&#xff1f; MapperScan("com.xxx.mapper")提出几个问题&#xff1a; Mapper接…

Snipaste的使用

Snipaste截屏软件的使用&#xff1a; 1、开始截屏 第一种方式&#xff1a;快捷键 &#xff08;默认是F1&#xff09;也就是说按一下F1键就会进入截屏状态。 第二种方式&#xff1a;点击软件在任务栏上的图标。 2、选定截屏区域&#xff1a; 进入截屏状态后移动鼠标&#x…