集群

      Redis集群使Redis提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移操作;

节点

      一个Redis集群通常由多个节点组成,起初,各节点相互独立,均处于一个仅包含自身的集群当中,必须将各个节点连接起来组建成一个真正可工作的集群。

      命令:CLUSTER  MEET  <ip>  <port>,向一个节点发送该命令,可让该节点与指定ip地址以及端口号的节点握手连接,从而将目标node节点添加至自身节点所在的集群当中;

启动节点:

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

节点会继续使用所有在单机模式中使用的服务器组件,以及仍然会使用redisServer结构来保存服务器的状态,使用redisClient结构来保存客户端的状态;

节点使用cluster.h/clusterNode结构、cluster.h/clusterLink结构、cluster.h/clusterState结构来分别保存在节点模式下才会使用的数据;

集群数据结构:

clusterNode结构保存了一个节点的当前状态(如节点的创建时间、节点的名字、ip地址端口号等);

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

cluster上用mget redis redis cluster meet_数据库

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

cluster上用mget redis redis cluster meet_java_02

每个节点都保存着一个clusterState结构,该结构记录了在当前节点的视角下,集群目前所处的状态:;

cluster上用mget redis redis cluster meet_redis_03

 

CLUSTER  MEET 命令实现:

CLUSTER  MEET  <ip>  <port>,通过客户端向结点A发送该命令,可将接收命令的节点B添加至A当前所在的集群里面;

收到命令的A节点将与B节点进行握手通信:

  1. 节点A为节点B创建一个clusterNode结构,并将该结构添加至自身的clusterState.nodes字典内;
  2. 节点A根据命令所指定的ip地址以及端口号向节点B发送一条MEET信息;
  3. 当节点B顺利收到MEET消息,则节点B会为节点A创建一个clusterNode结构,同样将其添加至节点B的clusterState.nodes字典中;
  4. 节点B向节点A返回一条PONG消息;
  5. 若A顺利收到PONG消息,则知B已经成功接收到自身发送的MEET消息;
  6. A向B再返回一条PING消息;
  7. 若B顺利收到PING消息,则知A收到自己发送的PONG消息,握手完成;

之后,节点A会将节点B的信息通过Gossip协议传播给集群中的其他节点,使其他节点也与B进行通信握手 ---à最终,节点B会被集群中的所有节点所认知;

槽指派

     Redis集群通过分片的方式来保存数据库中的键值对:集群中的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽;

  1. 上线状态:数据库中的16384个槽都有节点在处理;
  2. 离线状态:数据库中只要存在一个槽没有得到处理;

      命令:CLUSTER  ADDSLOTS  [ slot1  slot2  slot3  slot4  ....]

      通过向某个节点发送该命令,可以将一个或多个槽指派给接收命令的节点负责;

记录节点的槽指派信息

      clusterNode结构的slots属性、numslot属性记录了节点负责处理哪些槽;

      slots属性是一个二进制位数组,该数组长16384 / 8 = 2048个字节,一共包含16384个二进制位;

      Redis以0为起始索引,16383为终止索引,对slots数组中的16384个二进制位进行编号,并根据索引i上的为二进制位的值来判断节点是否负责处理槽i:

  • 若slots数组在索引i上的二进制位的值为1,则表示节点负责处理槽i;
  • 若slots数组在索引i上的二进制位的值为0,则表示节点不处理槽i;

因为取出设置slots数组中的任意一个二进制位的值的复杂度仅为O(1),所以对于一个给定节点的slots数组来说,程序检查节点是否负责处理某个槽,或者将某个槽指派给节点负责,这两个动作的时间复杂度都为O(1)

numslot属性记录了节点负责处理的槽的数量,即slots数组中值为1的二进制位数量;

传播节点的槽指派信息

      一个节点不仅会将自己负责处理的槽记录在clusterNode结构的slots属性和numslot属性之外,还会将自己的slots数组通过消息发送给集群中的其他节点à以此来告知其他节点自己目前负责处理哪些槽;

节点A通过消息从节点B那里得知节点B的slots数组时,节点A会在自己的clusterState.nodes字典中查找到节点B对应的clusterNode结构,并对结构中的slots数组进行保存或者更新;

集群中每个节点都会知道数据库的16384个槽分别指派给了集群中的哪些节点;

记录集群所有槽的指派信息

      clusterState结构中的slots数组记录了集群中所有16384个槽的指派信息,该数组中包含了16384个项,每个数组项都是一个指向clusterNode结构的指针:

  • 若slots[i]指针指向NULL,则表示槽i尚未指派给任何节点;
  • 若slots[i]指向一个clusterNode结构,则表示槽i已经指派给了clusterNode结构所代表的节点;

通过clusterState结构中的slots数组,可以将“查看槽i是否已经被指派,以及查看槽i被指派给了哪个节点负责”问题的时间复杂度由O(n)降低到O(1);(原因:若不存在clusterState.slots数组,则解决以上问题就需要遍历每个节点的clusterNode.slots数组,时间复杂度高)

二者区别:clusterState.slots数组记录了集群中所有槽的指派信息,clusterNode.slots数组仅仅记录了clusterNode结构所代表的节点负责处理哪些槽;(两者互补)

CLUSTER  ADDSLOTS命令的实现

      CLUSTER  ADDSLOTS命令接受一个或多个槽的序号作为参数,功能是将所有指定的序号的槽指派给接收该命令的节点负责;

      具体过程:

  1. 遍历所有输入的槽,检查它们是否都尚未指派给任何节点;输入的这些槽中只要有一个已经被指派给其他节点,则向客户端返回错误并终止命令;
  2. 若所有输入槽都尚未指派给任何节点,则再次遍历所有槽将所有槽指派给当前节点,首先将当前节点的clusterState.slots数组中对应序号的元素项的指针指向代表该节点的clusterNode结构;再将代表该节点的clusterNode结构的slots数组中对应序号的元素项的值置为1;

CLUSTER  ADDSLOTS命令执行结束之后,节点会通过发送消息告知集群中的其他节点,自己目前正在负责处理哪些槽;

在集群中执行命令

     当数据库中16384个槽都被指派后,集群就进入了上线状态,此时客户端就可以向集群中的节点发送数据命令了;

      当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了自己:

  • 若数据库键指派给了当前节点,那么当前节点执行该命令;
  • 若检查数据库键所属的槽指派给了其他节点,则当前节点会向客户端返回一个MOVED错误,指引客户端转向至正确的节点,并再次发送要执行的命令;

计算数据库键属于哪个槽

      节点使用以下算法判断键属于哪个槽:

            def  slot_number (key):

                  return CRC16 (key) & 16383

      CRC16(key)用于计算键key的CRC-16的校验和,& 16383则用于计算出一个介于0~16383之间的整数作为键key的槽号;

判断槽号是否由当前节点负责处理

      计算出键所属的槽i之后,节点会检查自己的clusterState.slots数组中的项i,查看其记录的地址值(指针值)是否与clusterState.myself值相等,若相等,则当前节点执行客户端发送的命令;反之,根据clusterState.slots[i]指向的clusterNode结构所记录节点的ip地址以及端口号,向客户端返回MOVED错误,指引客户端转向至正确的节点;

MOVED 错误

       当节点发现键所属的槽并非由自身负责处理时,节点就会向客户端返回一个MOVED错误,指引客户端转向至正确的节点;

      MOVED错误的格式为:MOVED  <slot>  <ip>:<port>

      当客户端接收到错误信息时,会根据MOVED错误信息中提供的IP地址以及端口号转向至目标节点,再重新发送之前的命令;

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

      Notes:集群模式下的客户端在接收到MOVED错误时,并不会打印错误信息,而是直接打印转向信息;而单机模式下的MOVED错误信息就会被打印出;

节点数据库的实现

      节点和单机服务器在数据库方面的区别:节点只能使用0号数据库,单机Redis服务器则没有这一限制;

      节点使用clusterState结构中的slots_to_keys跳跃表来保存槽和键之间的关系,slots_to_keys跳跃表每个节点的分值都是一个槽号,每个节点的成员都是一个数据库键;

      通过在slots_to_keys跳跃表中记录各个数据库键所属的槽,节点可以很方便地对属于某个或某些槽的所有数据库键进行批量操作,如CLUSTER  GETKEYSINSLOT  <slot>  <count> 命令可以返回最多count个属于槽slot的数据库建,该命令即通过遍历slots_to_keys跳跃表来实现的;

重新分片

      Redis集群的重新分片可以将任意数量已经指派给某个节点的槽改为指派给另一个节点,并且相关槽所属的键值对也会从源节点被移动到目标节点;

      重新分片操作可以在线进行,在重新分片过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求;

重新分片实现原理

      Redis集群的重新分片操作是由Redis集群管理软件redis-trib负责执行,redis-trib通过向源节点和目标节点发送命令来进行重新分片操作;

      redis-trib对集群的单个槽slot进行重新分片步骤如下:

  • redis-trib对目标节点发送CLUSTER  SETSLOT  <slot>  IMPORTING  <source_id>  命令,让目标节点准备好从源节点导入属于槽slot的键值对;
  • redis-trib对源节点发送CLUSTER  SETSLOT  <slot>  MIGRATING  <target_id>命令,让源节点准备好将属于槽slot的键值对迁移到目标节点;
  • redis-trib向源节点发送CLUSTER  GETKEYSINSLOT  <slot>  <count> 命令,获得最多count个属于槽slot的键值对的键名;
  • 对于返回的count个键的每个键都执行MIGRATING  <target_ip>  <target_port>  <key_name>  0  <timeout>命令,将被选中的键原子地从源节点迁移至目标节点;
  • 重复上两个步骤,直到保存在指定槽slot的所有键都被迁移到目标节点为止;
  • redis-trib向集群中的任意一个节点发送CLUSTER  SETSLOT  <slot>  NODE  <target_di>命令,将槽slot指派给目标节点,这一指派信息会通过消息告知集群中的所有节点;

ASK错误

      重新分片过程中可能会出现的问题:属于被迁移槽的一部分键值对保存在源节点里面,另一部分保存在目标节点里面;

      当客户端向源节点发送一个与数据库键有关的命令,恰好该命令属于正在被迁移的槽时:

  1. 源节点会先在自己的数据库内查找指定的键,若找到,则执行客户端发送的命令;
  2. 若没有找到,则该键可能属于已经被迁移到目标节点的那一部分,则源节点会向客户端返回一个ASK错误,指引客户端转向目标节点中重新发送要执行的命令;

Notes:与MOVED错误类似,在集群模式下,客户端并不会打印ASK错误信息,而是直接转向至目标节点中;

CLUSTER  SETSLOT  IMPORTING命令的实现

      clusterState结构的importing_slots_from数组记录了当前节点正在从其他节点导入的槽;数组长度为16384,数组元素为指向clusterNode结构的指针,当importing_slots_from[i]的值不为NULL,而是指向一个clusterNode结构,则表示当前节点正在从clusterNode所代表的节点导入槽i;

CLUSTER  SETSLOT  MIGRATING命令的实现

      clusterState结构的migrating_slots_to数组记录了当前节点正在迁移至其他节点的槽,长度为16384,元素为指向clusterNode结构的指针,若migrating_slots_to[i]的值不为NULL,而是指向一个clusterNode结构,则表示槽i正在由当前节点迁移至clusterNode所代表的节点;

ASK错误

      当向某节点发送有关数据库键的命令时,节点首先会在自身的数据库中查找键key,若该键key所属的槽由当前节点负责,则执行命令;

      若在当前节点中没有找到数据库键key,则会检查自己的clusterState.migrating_slots_to[i],查看该键key所属的槽i是否正在被迁移,若正在被迁移,则向客户端返回ASK错误,引导客户端到目标节点发送该命令请求;

      接收到ASK错误的客户端会根据消息提示自动转向至目标ip地址、端口号的节点,然后首先向目标节点发送ASKING命令,之后再重新发送原本想要执行的命令;

ASKING命令

      ASKING命令唯一的作用就是打开发送该命令的客户端的REDIS_ASKING标识;

      一般情况下,若客户端向节点发送一个关于槽i的命令,而槽i又没有指派给该节点,则会向客户端返回MOVED错误;

      但是,若节点的clusterState.importing_slots_from[i]显示节点正在导入槽i,且发送命令的客户端带有REDIS_ASKIGN标识,则该节点将破例执行这个关于槽i的命令一次;  客户端在转向至目标节点若不发送ASKING命令直接发送原本想要执行的命令的话,会被目标节点拒绝执行并返回MOVED错误;(因为目标节点会检查REDIS_ASKIGN标识是否存在)

      注:REDIS_ASKING标识是一次性标识,当目标节点检查并执行一次之后,便会移除该标识;

ASK错误和MOVED错误的区别

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

  1. MOVED错误表示槽的负责权已经由源节点转移至目标节点;
  2. ASK错误仅是槽在两个节点的迁移过程中的一种临时措施;

复制与故障转移

     Redis集群中的节点分为主节点、从节点,其中主节点用于处理槽从节点用于复制某个主节点,并当主节点下线时,代替下线主节点继续处理命令请求;

设置节点

      命令:CLUSTER  REPLICATE  <node_id>

      让接收命令的节点成为node_id所指定节点的从节点,并开始对主节点进行复制;

  1. 接收到该命令的节点首先会在自己的clusterState.nodes字典中找到node_id锁对应节点的clusterNode结构,并将自己的clusterState.myself.slavof指针指向该结构;
  2. 接收命令的节点将自己的clusterState.myself.flags中的属性值由REDIS_NODE_MASTER修改为REDIS_NODE_SLAVE标识(标识该节点已由原来的从节点变为主节点)
  3. 最后对clusterState.myself.slaveof指向的结构所代表的主节点进行复制操作;(具体复制操作与Redis单机模式下的复制操作相同)

某个节点成为从节点,并开始复制某一主节点这一信息会通过消息发送给集群中的其他节点,以更新其他节点的认知;

集群中的所有节点都会在代表主节点的clusterNode结构的slaves属性和numslaves属性中记录正在复制这个主节点的从节点名单;(slaves属性是一个数组,数组中的每个元素都指向正在复制当前主节点的代表从节点的clusterNode结构)

故障检测

      集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此来检测对方是否在线;若接收PING消息的节点没有在规定的时间内回复PONG消息,那么发送PING消息的节点会将其标记为疑似下线,并在clusterState.nodes属性中找到目标节点,将其clusterNode结构中的flags属性加上REDIS_NODE_PFAIL标识;

      集群中的各个节点会通过互相发送消息的方式来交换集群中各个节点的状态信息;

      若一个集群里面,半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线,则该主节点x将会被标记为已下线(FAIL),将该主节点x标记为已下线的节点会向集群广播这条FAIL消息,其他所有收到该消息的节点都会将x主节点标记为已下线状态;

故障转移

      当一个从节点发现正在复制的主节点进入已下线状态时,从节点将开始对下线主节点进行故障转移,步骤如下:

  1. 复制下线主节点的所有从节点内,会有一个被选中;
  2. 被选中的从节点执行SLAVEOF  NO  ONE命令,成为新的主节点;
  3. 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己;
  4. 新的主节点向集群广播一条PONG消息,更新其他主节点对当前新主节点的认知;
  5. 新主节点开始接收并处理命令请求,故障转移完成;

选举新的主节点

消息

     集群中的各个节点通过发送和接收消息进行通信;

      节点发送消息主要分为五种MEET、PING、PONG、FAIL、PUBLISH

  • MEET消息:客户端向某个节点发送CLUSTER  MEET消息时,该节点将向其他某个节点发送MEET消息,请求目标节点加入自身所在的集群当中;
  • PING消息:集群里的每个节点默认每隔一秒就会从已知节点列表中随机选取五个节点,然后从其中找出最长时间没有发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线;  若,节点A最后一次收到节点B回复的PONG消息的时间,距离当前时间已经超过了节点A的cluster-node-timeout选项设置时长的一半,则节点A也会向节点B发送PING消息;
  • PONG消息:当接收者收到发送者发来的MEET消息或者PING消息时,便回复PONG消息;  另外,一个节点可以通过向集群广播PONG消息来让集群中的其他节点刷新对该节点的认知(如故障转移);
  • FAIL消息:当节点A判定节点B为已下线状态FAIL状态时,便会向集群广播一条FAIL消息,收到该消息的其他集群节点会立即将节点B标识为FAIL状态;
  • PUBLISH消息:当某个节点接收到PUBLISH消息时,节点会执行该命令,并向集群广播一条该命令,所有接收到该命令的节点会执行相同的操作;

消息头

MEET、PING、PONG消息的实现

FAIL消息的实现

PUBLISH消息的实现