Redis学习(九)分布式与集群

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

目录

节点

集群命令的实现

重新分片

复制和故障转移

节点消息传递

总结


节点

       一个 Redis 集群通常由多个节点(node)组成, 在刚开始的时候, 每个节点都是相互独立的, 它们都处于一个只包含自己的集群当中, 要组建一个真正可工作的集群, 我们必须将各个独立的节点连接起来, 构成一个包含多个节点的集群。

连接各个节点的工作可以使用 CLUSTER MEET 命令来完成, 该命令的格式如下:

CLUSTER MEET <ip> <port>

向一个节点 node 发送 CLUSTER MEET 命令, 可以让 node 节点与 ip 和 port 所指定的节点进行握手(handshake), 当握手成功时, node节点就会将 ip 和 port 所指定的节点添加到 node 节点当前所在的集群中。

1、启动节点

一个节点就是一个运行在集群模式下的 Redis 服务器, Redis 服务器在启动时会根据 cluster-enabled 配置选项的是否为 yes 来决定是否开启服务器的集群模式。

digraph {      label = "\n å¾ IMAGE_NODE_OR_SERVER    æå¡å¨å¤æ­æ¯å¦å¼å¯é群模å¼çè¿ç¨";      //      node [shape = box];      start [label = "å¯å¨æå¡å¨", width = 3.3];      cluster_enabled_or_not [label = "cluster-enabled \né项çå¼ä¸º yes ï¼", shape = diamond];      start_node [label = "å¼å¯æå¡å¨çé群模å¼\næ为ä¸ä¸ªèç¹"];      start_server [label = "å¼å¯æå¡å¨çåæºï¼stand aloneï¼æ¨¡å¼\næ为ä¸ä¸ªæ®é Redis æå¡å¨"];      //      start -> cluster_enabled_or_not;      cluster_enabled_or_not -> start_node [label = "æ¯"];      cluster_enabled_or_not -> start_server [label = "å¦"];  }

节点(运行在集群模式下的 Redis 服务器)会继续使用所有在单机模式中使用的服务器组件, 比如说:

  • 节点会继续使用文件事件处理器来处理命令请求和返回命令回复。
  • 节点会继续使用时间事件处理器来执行 serverCron 函数, 而 serverCron 函数又会调用集群模式特有的 clusterCron 函数: clusterCron 函数负责执行在集群模式下需要执行的常规操作, 比如向集群中的其他节点发送 Gossip 消息, 检查节点是否断线; 又或者检查是否需要对下线节点进行自动故障转移, 等等。
  • 节点会继续使用数据库来保存键值对数据,键值对依然会是各种不同类型的对象。
  • 节点会继续使用 RDB 持久化模块和 AOF 持久化模块来执行持久化工作。
  • 节点会继续使用发布与订阅模块来执行 PUBLISH 、 SUBSCRIBE 等命令。
  • 节点会继续使用复制模块来进行节点的复制工作。
  • 节点会继续使用 Lua 脚本环境来执行客户端输入的 Lua 脚本。

除此之外, 节点会继续使用 redisServer 结构来保存服务器的状态, 使用 redisClient 结构来保存客户端的状态, 至于那些只有在集群模式下才会用到的数据, 节点将它们保存到了 clusterNode 结构,clusterLink 结构,以及 clusterState 结构里面。

2、集群数据结构

clusterNode 结构保存了一个节点的当前状态,如节点的创建时间节点的名字,节点当前的配置纪元节点的 IP端口号等。

每个节点都会使用一个 clusterNode 结构来记录自己的状态,并为集群中的所有其他节点(包括主节点和从节点)都创建一个相应的 clusterNode 结构, 以此来记录其他节点的状态:

struct clusterNode {

    // 创建节点的时间
    mstime_t ctime;

    // 节点的名字,由 40 个十六进制字符组成
    // 例如 68eef66df23420a5862208ef5b1a7005b806f2ff
    char name[REDIS_CLUSTER_NAMELEN];

    // 节点标识
    // 使用各种不同的标识值记录节点的角色(比如主节点或者从节点),
    // 以及节点目前所处的状态(比如在线或者下线)。
    int flags;

    // 节点当前的配置纪元,用于实现故障转移
    uint64_t configEpoch;

    // 节点的 IP 地址
    char ip[REDIS_IP_STR_LEN];

    // 节点的端口号
    int port;

    // 保存连接节点所需的有关信息
    clusterLink *link;

    // ...

};

其中,clusterNode 结构的 link 属性是一个 clusterLink 结构,该结构保存了连接节点所需的有关信息,比如套接字描述符, 输入缓冲区和输出缓冲区:

typedef struct clusterLink {

    // 连接的创建时间
    mstime_t ctime;

    // TCP 套接字描述符
    int fd;

    // 输出缓冲区,保存着等待发送给其他节点的消息(message)。
    sds sndbuf;

    // 输入缓冲区,保存着从其他节点接收到的消息。
    sds rcvbuf;

    // 与这个连接相关联的节点,如果没有的话就为 NULL
    struct clusterNode *node;

} clusterLink;

redisClient 结构和 clusterLink 结构的相同和不同之处

redisClient 结构和 clusterLink 结构都有自己的套接字描述符和输入、输出缓冲区, 这两个结构的区别在于,redisClient 结构中的套接字和缓冲区是用于连接客户端的,而 clusterLink 结构中的套接字和缓冲区则是用于连接节点。

每个节点都保存着一个 clusterState 结构, 这个结构记录了在当前节点的视角下,集群目前所处的状态 —— 比如集群是在线还是下线,集群包含多少个节点,集群当前的配置纪元, 诸如此类:

typedef struct clusterState {

    // 指向当前节点的指针
    clusterNode *myself;

    // 集群当前的配置纪元,用于实现故障转移
    uint64_t currentEpoch;

    // 集群当前的状态:是在线还是下线
    int state;

    // 集群中至少处理着一个槽的节点的数量
    int size;

    // 集群节点名单(包括 myself 节点)
    // 字典的键为节点的名字,字典的值为节点对应的 clusterNode 结构
    dict *nodes;

    // ...

} clusterState;

以下面的7000 、7001 、7002三个节点为例,展示了节点 7000 创建的 clusterState 结构,这个结构从节点 7000 的视角角度(myself指针指向自己的clusterNode结构)记录了集群以及集群包含的三个节点的当前状态。

digraph {      label = "\n å¾ IMAGE_CLUSTER_STATE_OF_7000    èç¹ 7000 å建ç clusterState ç»æ";      rankdir = LR;      //      node [shape = record];      clusterState [label = " <head> clusterState | <myself> myself | currentEpoch \n 0 | state \n REDIS_CLUSTER_FAIL | size \n 0 | <nodes> nodes | ... "];      nodes [label = " <head> nodes | <0> \"5154...2939\" | <1> \"68ee...f2ff\" | <2> \"9dfb...5c26\" "];      node7000 [label = " <head> clusterNode | name \n \"5154...2939\" | flags \n REDIS_NODE_MASTER | configEpoch \n 0 | ip \n \"127.0.0.1\" | port \n 7000 | ... "];     node7001 [label = " <head> clusterNode | name \n \"68ee...f2ff\"| flags \n REDIS_NODE_MASTER | configEpoch \n 0 | ip \n \"127.0.0.1\" | port \n 7001 | ... "];     node7002 [label = " <head> clusterNode | name \n \"9dfb...5c26\"| flags \n REDIS_NODE_MASTER | configEpoch \n 0 | ip \n \"127.0.0.1\" | port \n 7002 | ... "];      //link7000 [label = " <head> clusterLink | ctime | fd | sndbuf | rcvbuf "];      clusterState:myself ->  node7000:head;      clusterState:nodes -> nodes:head;      nodes:0 -> node7000:head;     nodes:1 -> node7001:head;     nodes:2 -> node7002:head;  }

3、CLUSTER MEET 命令的实现 

通过向节点 A 发送 CLUSTER MEET 命令,客户端可以让接收命令的节点 A 将另一个节点B添加到节点 A当前所在的集群里面,收到命令的节点 A 将与节点 B 进行握手(handshake), 以此来确认彼此的存在, 并为将来的进一步通信打好基础:

  1. 节点 A 会为节点 B 创建一个 clusterNode 结构, 并将该结构添加到自己的 clusterState.nodes 字典里面。
  2. 之后, 节点 A 将根据 CLUSTER MEET 命令给定的 IP 地址和端口号, 向节点 B 发送一条 MEET 消息(message)。
  3. 如果一切顺利, 节点 B 将接收到节点 A 发送的 MEET 消息, 节点 B 会为节点 A 创建一个 clusterNode 结构, 并将该结构添加到自己的 clusterState.nodes 字典里面。
  4. 之后, 节点 B 将向节点 A 返回一条 PONG 消息。
  5. 如果一切顺利, 节点 A 将接收到节点 B 返回的 PONG 消息, 通过这条 PONG 消息节点 A 可以知道节点 B 已经成功地接收到了自己发送的 MEET 消息。
  6. 之后, 节点 A 将向节点 B 返回一条 PING 消息。
  7. 如果一切顺利, 节点 B 将接收到节点 A 返回的 PING 消息, 通过这条 PING 消息节点 B 可以知道节点 A 已经成功地接收到了自己返回的 PONG 消息, 握手完成。

                                digraph {      label = "\n å¾ IMAGE_HANDSHAKE    èç¹çæ¡æè¿ç¨";      rankdir = LR;      splines = ortho;      //      node [shape = box, height = 2];      client [label = "客\næ·\n端"];      A [label = "è\nç¹\nA"];      B [label = "è\nç¹\nB"];      //      client -> A [label = "åéå½ä»¤ \n CLUSTER MEET \n <B_ip> <B_port>"];      A -> B [label = "åé MEET æ¶æ¯"];      A -> B [dir = back, label = "\nè¿å PONG æ¶æ¯"];      A -> B [label = "\nè¿å PING æ¶æ¯"];  }

之后, 节点 A 会将节点 B 的信息通过 Gossip 协议传播给集群中的其他节点, 让其他节点也与节点 B 进行握手,最终,经过一段时间之后, 节点 B 会被集群中的所有节点认识。这里的握手更TCP三次握手基本类似。

redis集群通过分片的方式,来保存数据库中的键值对。集群的整个数据库被分为16384个槽(slot),数据库中的每个键,都属于16384个槽中的一个,集群每个节点可以处理0~16384个槽。

当集群中,每一个槽都有节点在处理时,则这个集群是上线(ok)的状态;任意一个槽没有节点处理,则该集群下线(fail)。

采用命令cluster info,可以查看当前集群的状态,ok是上线,fail是下线。

通过向节点发送cluster addslots <slot>/[slot1 slot2 ….]命令,可以将槽指派给节点。槽是用数字从0~16383进行编号的。

在哪个节点输入cluster addslots,则对该节点指派槽。

1、记录节点指派信息

节点指派的槽的信息,记录在clusterNode结构体中:

struct clusterNode{
         //….其他信息
         unsigned char slots[16384/8];//二进制数组
         int numslots;//节点处理的槽数量
};

slots是一个二进制位数组,长度是16384/8 = 2048个字节byte = 2kb,共包含16384个二进制位。每一个下标代表8个槽,用二进制位表示。如果节点负责某个槽,则数组下标对应的二进制位的相应位置的值是1,否则是0

例如,下图中的数组,表示节点负责的槽是1、3、5、8、9、10这几个。

使用二进制的方式,目的是便于获取、修改节点负责的槽,因为时间复杂度都是O(1)。

2、传播节点槽指派信息

节点被分配了槽,不仅会记录在节点自身的clusterNode结构体,还会将信息传播给集群的其他节点。

节点a收到节点b的槽分配信息,会从自身的clusterState.nodes字典查找记录节点b信息clusterNode的结构中,并对结构中相应的属性slots与numslots记录槽的位置与槽的数量进行更新。(也就是A节点更新对于B节点的认知)

因为集群中的每个节点都会将自己的slots数组发送给其他节点,其他节点则会将接收到的slots数组保存其节点字典的源相应节点的clusterNode结构里从而刷新对于此节点的认知状态,所以集群中的每个节点都会知道数据库中的16384(2^14)个槽被分配给了集群的哪些节点。

3、记录集群所有槽指派信息

在clusterState结构体中slots数组记录了集群中所有16384个槽的指派信息:

typedef stuct clusterState{
//….其他信息
clusterNode *slots[16384];
}clusterState;

这个结构体中,数组每个下标表示一个槽,下标的值都是指针,指向负责该槽的节点clusterNode。如果某个数组下标是null,表示目前没有节点负责该槽。

                                    

clusterState.slots数组记录了集群中所有槽的指派信息,而clusterNode.slots数组只记录了clusterNode结构代表的节点的槽指派信息。

Redis的设计非常巧妙,clusterState.slots可以快速找到每个槽所属的负责节点,而节点内部clusterNode.slots数组可以快速查找、改变某个节点负责的槽,且获取某个节点负责的全部槽的速度比从clusterState的slots中快得多。

4、槽指派的实现

cluster addslots 命令接受了一个或多个槽作为参数,并将所有输入的槽指派给接受该命令的节点负责,槽指派之前,会先检查槽是否已经有节点负责,如果一个或以上的槽已经有节点负责,则停止指派,并且报错。

如果所有槽都没有节点负责,则修改clusterState的slots数组,将每个槽下标的值指向该节点的clusterNode结构;并修改该clusterNode的slots数组,将槽对应的二进制位置设置成1。

例如,执行命令clusteraddslots 1 2,节点变化如下:

完成上述命令写入后,节点会发消息通知集群的其他节点。

集群命令的实现

1、节点对命令的判断

当对集群的16384个槽都完成指派后,集群就上线,可以对集群进行操作。当客户端向节点发送数据库键有关的命令,接收命令的节点,会计算命令属于哪个槽,并检查槽是否指派给自己。

  • 如果槽是该节点负责,则执行命令;
  • 如果不是,则向客户端返回一个MOVED错误,指引客户端对正确的节点执行命令,客户端根据返回结果,会自动连接上相应的节点,再次执行命令。

2、计算键属于哪个槽

假设键名为key,计算方法如下:

crc16(key) & 16383

其中,crc16是一种算法,将key用crc16算法获取的结果,在与16383进行与操作(即对16384取余),获取一个介于0~16383的整数。可以采用cluster keyslot  key,查看键属于哪个槽。

3、判断槽是否由当前节点处理

根据上述算法计算出键所述的槽后,节点会与clusterState.slots数组相应下标的指针比对,判断键所在的槽是否是自己负责:

  • 如果指针指向自身则表示该槽由自身负责;
  • 如果指针不是指向自身,而是指向某个节点的clusterNode结构,则从该结构获取ip和端口号,并将ip、端口号、moved错误一并返回给客户端,指引客户端转向正在处理该槽的节点。

4、moved错误介绍

move命令为:moved <slot> <ip>:<port>

当客户端收到moved命令,就会解析里面的ip和端口号,并重新连上相应的节点,再执行命令。

一个客户端通常会与集群中的多个节点创建套接字连接,所以所谓的节点转向实际上就是换一个套接字连接来发送命令。

不过,由于moved错误的时候,处于集群状态下的redis-cli客户端会自动重定向,显示的也是redirect,因此客户端上看不到报错信息。

但是,如果是单机模式下的redis-cli客户端,则会直接报错,因为其并不知道moved命令的含义,也不会自动连接上新的节点。

5、节点数据的实现

集群节点有个限制,只能用0号数据库。键值对的保存方式和单机一样。节点的clusterState结构,还会使用跳跃表slots_to_keys保存槽和键的关系。

typedef struct clusterState{
         //….其他内容
         zskiplist *slots_to_keys;
}clusterState;

节点保存槽和键的关系,用的是zskiplist,其分值(score)是槽的编号,每个阶节点成员(member)是数据库的键。如下:

               

重新分片

redis集群的重新分片功能,可以将任意数量已经指派给某个节点的槽,修改为指派给另一个节点,且相关槽对应的数据库的键值对数据也迁移到另一个节点。

重新分片工作可以在线进行,集群不需要下线,并且源节点和目标节点都可以正常处理客户端的其他命令。

1、重新分片的实现

redis重新分片,是由redis集群管理软件redis-trib负责执行的,redis提供了进行重新分配所需的所有命令。而redis-trib软件通过向源节点和目标节点发送命令,来完成重新分片的工作。

对单个槽进行重新分片,步骤如下:

  1. redis-trib对目标节点发送命令cluster setslot <slot> importing <source_id>命令,让目标节点准备好从源节点导入编号是slot的槽的键值对。
  2. redis-trib对源节点发送命令cluster setslot <slot> migrating <target_id>命令,让源节点准备好将编号是slot的槽的键值对导入到目标节点。
  3. redis-trib向源节点发送命令cluster getkeysinslot <slot> <count>命令,获取最多count个属于槽slot的键值对的键。
  4. 对于第3步的每个键,redis-trib都向源节点发送命令migrate<target_ip> <target_port> <key_name> 0 <timeout>命令,将被选中的键迁移到目标节点。
  5. 重复3、4步骤,直到所有属于编号slot的槽都完成迁移。
  6. redis-trib将命令clustersetsslot <slot> node <target_id>发送给集群中的任一节点,然后命令会在集群中广播,所有节点都会知道槽slot已经归目标节点负责。

如果是多个槽重新分片,则每个槽都会经历上述的步骤进行重新分片。

2、ASK错误

     在进行重新分片的期间,源节点向目标节点迁移一个槽的过程中,可能会有属于被迁移槽的一部分键值对保存在源节点黎明,另一部分已被迁移完成的键值对保存在目标节点里。

当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时,源节点会在自己的数据库里面查找指定的键,如果找到就直接执行客户端发送的命令;否则,这个键有可能已经被迁移到目标节点了,源节点将向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令。

需要注意的是,ASK跟MOVED错误一样,对于客户端来说同样是隐藏的,单机模式的redis-cli客户端可以看见。

2、cluster setslot importing 命令的实现

clusterState结构的 importing_slots_from 数组记录了当前节点正在从其他节点导入的槽:

typedef struct clusterState{
         //….其他内容
         clusterNode *importing_slots_from[16384];
}clusterState;

如果importing_slots_from[i] 的值不为NULL,而是指向一个 clusterNode结构,那么表示当前节点正在从clusterNode节点导入槽 i

3、cluster setslot migrating 命令的实现

clusterState结构的 importing_slots_to 数组记录了当前节点正在迁移至其他节点的槽:

typedef struct clusterState{
         //….其他内容
         clusterNode *importing_slots_to[16384];
}clusterState;

如果importing_slots_to[i] 的值不为NULL,而是指向一个 clusterNode结构,那么表示当前节点正在将槽 i 迁移至clusterNode节点。

4、ASK错误与MOVED错误的区别

ASK错误和MOVED错误都会导致客户端转向,但其区别在于:

  • MOVED错误代表槽的负责权已经从一个节点转向另一个节点:客户端收到关于槽 i 的MOVED错误后,客户端每次遇到关于槽 i 命令请求时,都可以直接将命令请求发送至MOVED错误所指向的节点,因为该节点就是目前负责槽i的节点。
  • ASK错误只是两个节点进行迁移时的一个临时措施:客户端收到关于槽 i 的ASK错误后,客户端只会在接下来的一次命令请求中将关于槽 i 的命令请求发送至ASK错误指引的节点,但这种转向不会对客户端以后发送关于槽i的命令产生任何影响,所以ASK是一次性的。

复制和故障转移

redis集群的节点,分为主节点从节点主节点负责处理槽,从节点复制主节点的数据并且在复制的主节点下线时,替代主节点继续处理命令请求。集群中可以有多个主节点。

如上图,7000~7003都是主节点,7004和7005作为从节点复制7000节点,当7000下线则7004和7005有一个会被选为主节点,替代7000。例如7004被选为主节点,则7004替代7000管理7000原先负责的槽,而7005则复制7004。当7000重新上线,也会成为7004的从节点。

1、设置从节点

命令如下:cluster  replicate  <node_id>,则将当前节点设置为node_id的从节点,并对其进行复制。

设置过程如下:

1)接收到该命令的节点,会先在自身结构的clusterState结构体的nodes数组中,找到对应的节点的clusterNode,使用slaveof属性来记录主节点。

structclusterNode{
         //….其他内容
         struct clusterNode *slaveof;
};

2)修改自身的flags属性,关闭原本的标识redis_node_master,打开redis_node_slave标识,表示变成从属节点。

3)调用复制的代码,根据上述slaveof保存的主节点的信息中的ip和端口号,去获取数据。

因此,cluster replicate命令等同于单机情况下的主从复制中的slaveof命令。

命令执行完毕后,该节点会在集群中发送消息给其他节点告知。

其他节点都会在各自保存主节点结构的结构体中,对应的numslaves属性记录该节点的从节点个数,以及slaves数组,该数组每个属性记录的是指向该节点从节点的clusterNode指针。

struct clusterNode{
         //….其他内容
         int numslaves;
         struct clusterNode **slaves;
};

2、故障检测

每个节点都会定期给其他节点发送ping信息,以此检测对方是否在线,如果在规定时间内ping没有得到返回pong,则认为疑似下线。如果节点认为某个节点疑似下线,就会在保存那个节点的clusterNode结构体中,flags属性加上redis_node_pfail属性。

每个节点自身的clusterNode结构体中,都有一个属性是fail_reports,该属性是一个链表,用于记录其他节点的下线报告。

     structclusterNode{
         //….其他内容
         list *fail_reports;
     };

list中的每一个元素,都是一个clusterNodeFailReport结构体,结构如下:

struct clusterNodeFailReport{
//指向下线节点的指针
   struct clusterNode *node;
//最后一次从node节点收到的下线报告时间,程序使用这个时间戳来检查下线报告是否过期
//如果与当前时间相差太久,该结构体会被删除
   mstime_t time;
}typedef clusterNodeFailReport;

在一个集群中,半数以上的节点认为某个节点疑似下线,则该节点被认定为已下线。并且会广播给集群的所有节点。

3、故障转移

当一个从节点发现自己正在复制的主节点已下线状态,则会开始进行故障转移。步骤如下:

  1. 复制下线主节点的所有从节点,有一个被选中。
  2. 被选中的从节点,执行slaveof no one命令,变成主节点。
  3. 新主节点撤销对原主节点的槽指派,并指派给自己。
  4. 新主节点向集群发一条pong信息,告知集群其已经变成主节点。
  5. 新主节点开始处理槽有关的工作,故障转移完成。

4、选举新主节点方式

新主节点是通过选举产生,方法如下:

  1. 集群配置纪元是一个计数器,默认是0。
  2. 集群中进行一次故障转移,增1。
  3. 每个配置纪元期间,集群中负责处理槽的主节点都有一次投票的机会,其会投票给最先要求其投票给节点的节点。
  4. 当从节点发现主节点下线,会向集群广播clustermsg_type_failover_auth_request信息,要求所有接收到该信息并且具备投票权的主节点,给该从节点投票。
  5. 如果主节点具备投票权,并且尚未投给其他节点,则会对该节点回复clustermsg_type_failover_auth_ack,告知其投票。
  6. 每个参选的主节点都会接受clustermsg_type_failover_auth_ack回复,并记录支持的数量。
  7. 当集群有n个主节点,则从节点获得的票数大于或等于1+n/2时(即过半),则成为新的主节点。
  8. 如果一个配置纪元内,没有节点票数满足要求,则进入一个新的配置纪元,重新上述步骤。

主节点的选取方式和选取领头sentinel的方式很相似,都是raft算法的领头选举

节点消息传递

1、发送消息类型

集群中节点通过发送与接收消息进行通信。发送消息的节点称为消息发送者,接收消息节点称为接收者。消息发送类型如下:

  • meet:当客户端发送clustermeet给节点,节点会发送meet消息给接收者,请求接收者加入到发送者当前的集群中。
  • ping:每个节点每1秒,默认会随机从当前已知节点列表,挑选5个节点,并从中挑选最久未发送过ping消息的节点,对其发送ping,检测其是否在线。另外,如果某个节点最后一次回复pong的时间,距离当前时间,已经超过redis配置文件中cluster-node-timeout设定的秒数的一半,则也会对该节点发送ping,防止多次没有随机到该节点,导致对该节点的状态更新过慢。
  • pong:当节点收到meet或者ping,为了告知发送者收到消息,会回复pong。另外,完成一次故障转移后,新的主节点会给向集群广播pong。
  • fail:当节点A认为某个节点B下线,会向集群广播关于节点B的fail状态,其他节点接收到后,都会将节点B状态置为下线。
  • publish:当节点收到publish命令,会执行该命令,并向集群发送publish,其他节点收到后也会执行该命令。

2、消息头

所有消息都由消息头包裹,消息头可以认为是消息的一部分。消息头由cluster.h/clusterMsg结构记录,如下:

structclusterMsg{
    uint32_t totlen;//消息总长度,包括消息头长度和正文长度
    uint16_t type;//消息类型
    uint16_t count;//消息正文包含节点信息数量,只有在meet、ping、pong这三种涉及到gossip协议的类型使用
    uint64_t currentEpoch;//发送者的配置纪元
    uint64_t configEpoch;//该节点是主节点时,是发送者的配置纪元;是从节点时,是对应正在复制的主节点的配置纪元
    char sender[REDIS_CLUSTER_NAMELEN];//发送者名字(ID)
    unsigned char myslots[REDIS_CLUSTER_SLOTS/8];//发送者目前的槽信息
    char slaveof[REDIS_CLUSTER_NAMELEN];//主节点时记录的是40位长的都是0的字节数组,从节点时记录的是复制的主节点的名字
    uint16_t port;//发送者端口号
    uint16_t flag;//发送者标识值
    unsigned char state;//发送者所处的集群状态
    union clusterMsgData data;//消息的正文
}clusterMsg;

消息的正文是一个联合体,共有三种类型结构体,包括pingfailpublish,其中pong、meet类型都和ping一样。

这里需要注意的是,发送心跳包信息的信息头里最占空间的是发送者的槽信息myslots[REDIS_CLUSTER_SLOTS/8],16384个槽的位图bitmap共2kb大小,这也是为什么Redis设计为16384个槽的原因。

虽然槽分配是根据公式 HASH_SLOT=CRC16(key) mod 16384 按理说这里对于键的hash算法产生的hash值有16bit,该算法可以产生2^16=65536个值。换句话说,值是分布在0~65535之间。那作者在做mod运算的时候,为什么不mod65536,而选择mod16384?

真正的原因是65536个值的位图为8kb的内存,发送的心跳包过于庞大,浪费带宽。而Redis的集群主节点数量由于设计原因基本不可能超过1000个,所以16384个槽位够用了,能确保每个节点都有槽可以处理。更详细可见这篇文章

3、meet、ping、pong 消息的实现

这三个的类型一样,都是记录在联合体clusterMsgData中的结构体。因为这三种消息有相同的正文,节点是通过消息头的type判断是这三种的哪一种。

这三种消息的类型是clusterMsgDataGossip,这是一个结构体,记录节点名字、最后给该节点发送ping的时间戳、最后收到节点pong的时间戳、节点ip、端口号、标识等。

当节点接收到信息时,如果不认识里面的节点,则会与节点进行握手,如果认识则更新对应的信息。

4、fail 消息的实现

由于使用gossip协议会有延迟,fail是用来表示该节点下线,需要尽快传达,因此不用gossip协议,而是立即让集群中的全部节点知情。其正文就是下线节点的名字。

5、publish 消息的实现

客户端向集群某个节点发送publish <channel> <message>,接收命令的节点不仅会向频道channel发送message,还会向集群广播一条publish消息,其他节点也会执行该命令。

                                                            

publish用结构体clusterMsgDataPublish记录,内容是包括频道长度,消息长度,以及具体内容

其中,bulk_data的前channel_len字节,记录channel参数;剩余字节记录message参数。

例如,发送publish“news.it” “hello” 如下:

其实也可以直接向集群广播publish命令,但是由于其不符合redis设计的各节点通过消息发送和接收来传播消息的做法,因此采用对某一节点进行消息发送。

总结

  • 节点通过握手来将其他节点添加到自己所处的集群当中。
  • 集群中的 16384 个槽可以分别指派给集群中的各个节点, 每个节点都会记录哪些槽指派给了自己, 而哪些槽又被指派给了其他节点。
  • 节点在接到一个命令请求时, 会先检查这个命令请求要处理的键所在的槽是否由自己负责, 如果不是的话, 节点将向客户端返回一个 MOVED 错误, MOVED 错误携带的信息可以指引客户端转向至正在负责相关槽的节点。
  • 对 Redis 集群的重新分片工作是由客户端执行的, 重新分片的关键是将属于某个槽的所有键值对从一个节点转移至另一个节点。
  • 如果节点 A 正在迁移槽 i 至节点 B , 那么当节点 A 没能在自己的数据库中找到命令指定的数据库键时, 节点 A 会向客户端返回一个 ASK 错误, 指引客户端到节点 B 继续查找指定的数据库键。
  • MOVED 错误表示槽的负责权已经从一个节点转移到了另一个节点, 而 ASK 错误只是两个节点在迁移槽的过程中使用的一种临时措施。
  • 集群里的从节点用于复制主节点, 并在主节点下线时, 代替主节点继续处理命令请求。
  • 集群中的节点通过发送和接收消息来进行通讯, 常见的消息包括 MEET 、 PING 、 PONG 、 PUBLISH 、 FAIL 五种。

 

 

 

 

参考文章:

《Redis设计与实现》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值