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

一、节点

一个Redis集群通常由多个节点组成,连接各个节点的工作可以使用CLUSTER MEET命令来完成,该命令的格式如下:

CLUSTER MEET <ip> <port>

1)、启动节点

Redis服务器在启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式

cluster容错 redis redis cluster nodes_数组

2)、集群数据结构

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

cluster容错 redis redis cluster nodes_数据库_02

3)、CLUSTER MEET命令的实现

1)节点A会为节点B创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面

2)之后,节点A将根据CLUSTER MEET命令给定的IP地址和端口号,向节点B发送一条MEET消息

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消息,握手完成

cluster容错 redis redis cluster nodes_Redis集群_03

二、槽指派

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

当数据库中的16384个槽都有节点在处理时,集群处于上线状态;相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态

1)、记录节点的槽指派信息

struct clusterNode{
    //...
    
    unsigned char slots[16384/8];
    
    int numslots;
    
    //...
};

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

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

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

numslots属性则记录节点负责处理的槽的数量,也即是slots数组中值为1的二进制位的数量

2)、传播节点的槽指派信息

一个节点会将自己的slots数组通过消息发送给集群中的其他节点,以此来告知其他节点自己目前负责处理哪些槽,集群中的每个及诶点都会知道数据库中的16384个槽分别被指派给了集群中的哪些节点

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

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

slots数组包含16384个项,每个数组项都是一个指向clusterNode结构的指针:

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

cluster容错 redis redis cluster nodes_客户端_04

通过将所有槽的指派信息保存在clusterState.slots数组里面,程序要检查槽i是否已经被指派,又或者取得负责处理槽i的节点,只需要访问clusterState.slots[i]的值即可,这个操作的复杂度仅为O(1)

4)、CLUSTER ADDSLOTS命令的实现

CLUSTER ADDSLOTS命令接受一个或多个槽作为参数,并将所有输入的槽指派给接收该命令的节点负责:

CLUSTER ADDSLOTS <slot> [slot …]

举个例子,下图中展示了一个节点的clusterState结构,clusterState.slots数组中的所有指针都指向NULL,并且clusterState.slots数组中的所有二进制位的值都是0,这说明当前节点没有被指派任何槽,并且集群中的所有槽都是未指派的

cluster容错 redis redis cluster nodes_数组_05

当客户端对该节点执行命令:

CLUSTER ADDSLOTS 1 2

将槽1和槽2指派给节点之后,节点的clusterState将被更新成下图所示的样子:

  • clusterState.slots数组在索引1和索引2上的指针指向了代表当前节点的clusterNode结构
  • 并且clusterState.slots数组在索引1和索引2上的位置被设置成了1

cluster容错 redis redis cluster nodes_客户端_06

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

三、在集群中执行命令

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

  • 如果键所在的槽正好就指派给了当前节点,那么节点直接执行这个命令
  • 如果键所在的槽并没有指派给当前节点,那么节点会向客户端返回一个MOVED错误,指引客户端转向至正确的节点,并再次发送之前想要执行的命令

cluster容错 redis redis cluster nodes_客户端_07

1)、计算键属于哪个槽

节点使用以下算法来计算给定键key属于哪个槽:

def slot_number(key):
	return CRC16(key) & 16383

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

使用CLUSTER KEYSLOT<key>命令可以查看一个给定键属于哪个槽

2)、判断槽是否由当前节点负责处理

当节点计算出键所属的槽i之后,节点就会检查自己在clusterState.slots数组中的项i,判断键所在的槽是否由自己负责:

1)如果clusterState.slots[i]等于clusterState.myself,那么说明槽i由当前节点负责,节点可以执行客户端发送的命令

2)如果clusterState.slots[i]不等于clusterState.myself,那么说明槽i并非由当前节点负责,节点会根据clusterState.slots[i]指向的clusterNode结构所记录的节点IP和端口号,向客户端返回MOVED错误,指引客户端转向至正在处理槽i的节点

3)、MOVED错误

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

MOVED错误的格式为:

MOVED <slot> <ip>:<port>

其中slot为键所在的槽,而ip和port则是负责处理槽slot的节点的IP地址和端口号

4)、节点数据库的实现

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

四、重新分片

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

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

重新分片的实现原理:

Redis集群的重新分片操作是由Redis集群的管理软件redis-trib负责执行的,Redis提供了进行重新分片所需的所有命令,而redis-trib则通过向源节点和目标节点发送命令来进行重新分片操作,redis-trib对集群的单个槽slot进行重新分片的步骤如下:

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向集群中的任意一个结点发送CLUSTER SETSLOT <slot> NODE <target_id>命令,将槽slot指派给目标节点,这一指派信息会通过消息发送至整个集群,最终集群中的所有节点都会知道槽slot已经指派给了目标节点

cluster容错 redis redis cluster nodes_Redis集群_08

五、ASK错误

在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面

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

  • 源节点会先在自己的数据库里面查找指定的键,如果找到的话,就直接执行客户端发送的命令
  • 相反地,如果源节点没能在自己的数据库里面找到指定的键,那么这个键有可能已经被迁移到了目标节点,源节点将向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令

cluster容错 redis redis cluster nodes_数组_09

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

  • MOVED错误代表槽的负责权已经从一个节点转移到了另一个节点:在客户端收到关于槽i的MOVED错误之后,客户端每次遇到关于槽i的命令请求时,都可以直接将命令请求发送至MOVED错误所指向的节点,因为该节点就是目前负责槽i的节点
  • ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施:在客户端收到关于槽i的ASK错误之后,客户端只会在接下来的一次命令请求中将关于槽i的命令请求发送至ASK错误所指示的节点,但这种转向不会对客户端今后发送关于槽i的命令请求产生任何影响,客户端依然会将关于槽i的命令请求发送至目前负责处理槽i的节点,除非ASK错误再次出现

六、复制与故障转移

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

1)、设置从节点

向一个节点发送命令:

CLUSTER REPLICATE <node_id>

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

2)、故障检测

集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此来检测对方是否在线,如果接收PING消息的节点没有在规定的时间内,向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线

集群中的各个节点会通过互相发送消息的方式来交换集群中各个节点的状态信息,例如某个节点是处于在线状态、疑似下线状态还是已下线状态

如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线,那么这个主节点x将被标记为已下线,将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线

3)、故障转移

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

1)复制下线主节点的所有从节点里面,会有一个从节点被选出

2)被选中的从节点会执行SLAVEOF no one命令,成为新的主节点

3)新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己

4)新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽

5)新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成