Redis 集群的实现

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

一、节点

一个Redis集群通常由多个节点组成,连接各个节点可以使用如下命令,来让该节点与该ip和端口号指定的节点进行握手,当握手成功时,该ip和端口号指定的节点就会加入发送命令的节点的集群中

127.0.0.1:6379> CLUSTER MEET 127.0.0.1 7000		# CLUSTER MEET <ip> <port>

1. 启动节点

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

cluster-enabled yes			# yes 表示开启集群模式  no 表示开启单机模式

节点(运行在集群模式下的Redis服务器)会继续使用所有在单机模式中使用的服务器组件。例如继续使用RDB和AOF持久化,使用时间时间处理器处理serverCron函数等等

那些只有在集群模式下才会用到的数据,节点将它们保存到了 cluster.h/clusterNode结构、cluster.h/clusterLink结构,以及cluster.h/clusterState结构里面,下一章会进行讲解

2. 集群数据结构

clusterNode结构保存了一个节点的当前状态,比如节点的创建时间、节点的名字、节点当前的配置纪元(集群也有主从节点,也要进行故障转移)、节点的IP地址和端口号等等

当使用集群模式开启了服务器时,就代表这个服务器是一个节点,节点会用clusterNode结构来记录自己的状态,并为集群中的所有节点(主节点和从节点)都会去创建一个相应的clusterNode结构,用该结构去记录其他节点的状态

struct clusterNode(

	//创建节点时间
    mstime_t ctime;

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

    //节点标识
    //用来区分节点是主还是从节点
    //以及区分节点的状态(上线还是下线)
    int flags;

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

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

    //节点的端口号
    int port;

    //保存连接节点所需的有关信息(TCP建链连接)
    clusterLink *link;
    //...
)

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

typedef struct clusterLink(

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

    //TCP套接字描述符(记录节点连接当前节点使用的套接字)
    int fd;
  
    //输出缓冲区(保存着等待发送给其他节点的消息)
    sds sndbuf
 
    //输入缓冲区(保存着从其他节点接收到的信息)
    sds rcvbuf;
 
    //与这个连接相关联的节点,如果没有的话就为NULL(也就是引用clusterLink的节点)
    //相当于形成了一个互通
    struct clusterNode *node;
)clusterLink

每一个节点都保存着一个ClusterState结构,该结构记录了在当前节点的视角下,集群目前所处的状态,例如集群是下线的还是上线,集群包含多少个节点,集群当前的配置纪元等

typedef struct clusterState(
	
	//指向当前节点的指针
    clusterNode *myself;
    
    //集群当前的配置纪元,用来实现故障转移的
    uinit64_t currentEpoch;
    
    //集群当前的状态,下线还是上线
    int state;
    
    //集群中处理槽的节点的数量
    int size;
    
    //集群节点名单(包括Myself)
    //使用字典保存,键为节点的名字,值为节点对应的clusterNode结构
    dict *nodes;
    //....
)clusterState;

3. CLUSTER MEET命令的实现

从客户端向节点A发送节点B的ip和端口号之后,节点A会先跟B进行握手,以此来确认彼此的存在,并未将来的进一步通信打好基础,过程如下

  • 节点A先为节点B创建一个ClusterNode结构,并将该结构添加到自己的ClusterState.nodes字典里面
  • 节点A根据命令里面给出的IP地址和PORT端口号,建立TCP连接,向节点B发送一条Meet信息
  • 节点B顺利接收到节点A发送的Meet信息的话,节点B就会为节点A创建一个ClusterNode结构,并将该结构添加到自己的ClusterState.nodes字典里面
  • 节点B添加完成之后,节点B会返回一条PONG信息给节点A
  • 节点接收到节点B返回的PONG命令,说明节点B接收到了节点A的MEET消息,然后向节点B返回一条PING消息
  • 节点B如果接收到了节点A的PING消息,说明三次握手完成

在这里插入图片描述

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

二、槽指派

Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分成16384个槽。当数据库中的16384个槽都有节点在处理时,集群处于上线状态。相反,数据库中有任何一个槽没有得到处理,那么集群处于下线状态。

可以通过向节点发送命令,来将一些槽指派给该节点负责

127.0.0.1:7000> CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000

1. 记录节点的槽指派信息

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

struct clusterNode {
	. . .
	
	// 一个二进制数组
	// 包含2048个字节,也就是16384个二进制位	
	unsigned char slots[16384 / 8];
	
	// 	记录节点负责处理的槽的数量,也就是数组中二进制位为1的数量
	int numslots;
}
  • Redis以0位起始索引,16383为终止索引,对每一个二进制位进行了编号
    • 如果该索引位置的值为1,则表示该节点负责该处理槽
    • 如果是0,则不负责
  • 例如,该图,表示该节点负责0~7槽位
    在这里插入图片描述

2. 传播节点的槽指派信息

一个节点除了会将自己负责处理的槽记录在clusterNode结构的slots属性和nunmslots属性之外,它还会将自己的slots数组通过消息发送给集群中的其他节点,来告知其他节点自己负责哪个槽位

当节点A通过消息从节点B那里接收到节点B的slots数组时,节点A会在自己的clusterState.nodes字典中查找节点B对应的clusterNode结构,并对结构中的slots数组进行保存或者更新。所以集群中的每个节点都会知道数据库中的16384个槽分别被指派给了集群中的哪些节点。

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

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

typedef struct clusterState {
	. . . 
	
	// 指向了每个槽对应属于哪个节点
	clusterNode *slots[16384];
}

为什么clusterNode结构中有slots数组了还需要在clusterState 中定义该数组呢?

  • 因为如果想查找具体某一个槽位是否被指派或者指派给哪一个节点进行管理,如果通过clusterNode.slots来查找,则需要遍历买一个节点来看该槽位的二进制位是否为 1 ,而在clusterState 中,每一个槽位都有一个指针指向了负责该槽位的节点,查找的效率达到了O(1)

那为什么还需要clusterNode结构中的slots数组呢,clusterState 结构中的数组不是已经保存了所有槽指派的信息了吗?

  • 因为当某个节点要将该槽指派信息传递给其他节点时,或者是要知道该节点负责哪些槽位时,如果只有clusterState 结构的slots数组,则需要遍历整个数组,记录该节点负责哪些槽位,而clusterNode结构中的slots数组,记录的就直接是该节点负责的槽位

clusterState.slots 记录了集群中所有槽的指派信息。
clusterNode.slots 记录了该节点的槽的指派信息

三、在集群中执行命令

在对数据库16384个槽都进行了指派之后,集群就进入了上线状态,客户端就可以想集群中的节点发送命令

当客户端发送命令了之后,接收到命令的节点就会计算出该键属于哪个槽,并检查这个槽是否是归于自己负责

  • 如果是:那么这个节点直接执行该命令
  • 如果不是:那么节点就会像客户端返回一个MOVED的错误,指引客户端转向(redirect)至正确的节点,并且再次发送之前想要执行的命令
    在这里插入图片描述

1. 计算键属于哪个槽

节点通过以下算法来计算给定键属于哪个槽

def slot_number(key)
	return CRC16(key) & 16383
  • 通过CRC-16计算key的校验和,得到一串数字
  • 16383的二进制对应11111111111111,全是1,则代表取与操作相当于取摸运算(类比Java HashMap 计算桶下标)从而得到槽位索引

2. 判断槽是否由当前节点负责

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

  • 如果是由自己负责,(clusterState.slots[i] == clusterState.myself),则几点自己执行命令
  • 如果不是由自己负责,则从数组中找出该槽位由谁负责,根据该引用指向的节点的IP和端口号,先向客户端返回MOVED错误,然后指引客户端重定向到该节点处理该命令

四、重新分片

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

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

实现原理

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

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

  • redis-trib对目标节点发送CLUSTER SETSLOTIMPORTING <source_id>命令,让目标节点准备好从源节点导入(import)属于槽slot的键值对。
  • redis-trib对源节点发送CLUSTER SETSLOT MIGRATING<target_id>命令,让源节点准备好将属于槽 slot的键值对迁移(migrate)至目标节点。
  • redis-trib向源节点发送CLUSTER GETKEYSINSLOT命令,获得最多 count 个属于槽 slot 的键值对的键名(key name)。
  • 对于步骤3获得的每个键名,redis-trib都向源节点发送一个MIGRATE <target_ip> <target_port> <key_name> 0 < timeout>命令,将被选中的键原子地从源节点迁移至目标节点。
  • 重复执行上面两个操作,直到源节点保存的所有属于槽slot的键值对都被迁移至目标节点为止
  • redis-trib向集群中的任意一个节点发送CLUSTER SETSLOT NODE 命令,将槽slot指派给目标节点,这一指派信息会通过消息发送至整个集群,最终集群中的所有节点都会知道槽 slot 已经指派给了目标节点。

在这里插入图片描述

ASK错误

可能出现这么一种场景,如果对源节点的一个槽进行操作,该槽一部分键值对已经转移到了目标节点,一部分还留在源节点,此时,源节点先会在自己的数据库中查找该键值对,如果查到,则自己执行命令,如果没有,则说明已经被转移到目标节点,此时会返回一个ASK错误,并且重定向到目标节点,执行该命令

五、复制与故障转移

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

1. 设置从节点

向一个节点发送命令

127.0.0.1:6379> CLUSTER REPLICATE <node_id>

让该节点成为node_id指定节点的从节点,并开始进行复制,就相当于salveof一个Redis服务器

设置过程

  1. 接收到该命令的节点首先会在自己的clusterState.nodes字典中找到node_id 所对应节点的clusterNode结构,并将自己的clusterState.myself.slaveof 指针指向这个结构,以此来记录这个节点正在复制的主节点
struct clusterNode {
   // 如果这是一个从节点,那么指向主节点
  struct clusterNode *slaveof;
}
  1. 然后节点会修改自己在clusterState.myself.flags中的属性,关闭原本的REDIS_NODE_MASTER标识,打开REDIS_NODE_SLAVE标识,表示这个节点已经由原来的主节点变成了从节点。
  2. 最后,节点会调用复制代码,并根据clusterState.myself.slaveof指向的clusterNode结构所保存的IP地址和端口号,对主节点进行复制。因为节点的复制功能和单机 Redis服务器的复制功能使用了相同的代码,所以让从节点复制主节点相当于向从节点发送命令SLAVEOF< master_ip> < master_port >。

一个节点成为从节点,并开始复制某个主节点这一信息会通过消息发送给集群中的其他节点,最终集群中所有节点都会知道某个节点正在复制某个节点

2. 故障检测

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

例如:节点7000向7001发送PING消息,7001节点没有及时返回,此时7000节点就会找到nodes字典里7001对应的clusterNode结构,将其REDIS_NODE_PFAIL标识打开,表示其疑似下线

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

当一个主节点A通过消息得知主节点B认为主节点C进入了疑似下线状态时,主节点A会在自己的clusterState.nodes字典中找到主节点C所对应的clusterNode结构,并将主节点B的下线报告(failure report)添加到clusterNode结构的fail_reports链表中。

struct clusterNode {

  // 一个链表,记录了所有其他节点对该节点的下线报告
  list *fail_reports;
}

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

3. 故障转移

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

  1. 复制下线主节点的所有从节点里面,会有一个从节点被选中。
  2. 被选中的从节点会执行 SLAVE OF no one命令,成为新的主节点。
  3. 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
  4. 新的主节点向集群广播一条PONG消息,这条 PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。
  5. 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。

4. 选举新的主节点

选取主节点方法如下:

  1. 集群的配置纪元是一个自增计数器,它的初始值为0。
  2. 当集群里的某个节点开始一次故障转移操作时,集群配置纪元的值会被增一。
  3. 对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票的机会,而第一个向主节点要求投票的从节点将获得主节点的投票。
  4. 当从节点发现自己正在复制的主节点进入已下线状态时,从节点会向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。
  5. 如果一个主节点具有投票权(它正在负责处理槽),并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,表示这个主节点支持从节点成为新的主节点。
  6. 每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持。
  7. 如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于N/2+1 张支持票时,这个从节点就会当选为新的主节点。
  8. 因为在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有N个主节点进行投票,那么具有大于等于N/2+1 张支持票的从节点只会有一个,这确保了新的主节点只会有一个。
  9. 如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。

这个选举算法和哨兵Sentinel的方法基本相同,都是基于Raft算法的领头选举方法

总的来说就是哪个从节点先发现自己的主节点下线了就会给其他主节点发送消息,选举自己成为新的主节点。

六、消息

集群中的各个节点通过发送和接收消息(message)来进行通信,我们称发送消息的节点为发送者(sender),接收消息的节点为接收者(receiver).

节点发送的消息主要有5种:

  • MEET消息∶当发送者接到客户端发送的CLUSTER MEET命令时,发送者会向接收者发送MEET消息,请求接收者加入到发送者当前所处的集群里面。
  • PING 消息∶集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING 消息的节点发送PING消息,以此来检测被选中的节点是否在线。除此之外,如果节点A最后一次收到节点B发送的PONG消息的时间,距离当前时间已经超过了节点A的cluster-node-timeout选项设置时长的一半,那么节点A也会向节点B发送PING消息,这可以防止节点A因为长时间没有随机选中节点B作为PING消息的发送对象而导致对节点B的信息更新滞后。
  • PONG消息∶当接收者收到发送者发来的MEET消息或者PING消息时,为了向发送者确认这条 MEET消息或者PING消息已到达,接收者会向发送者返回一条 PONG 消息。另外,一个节点也可以通过向集群广播自己的PONG消息来让集群中的其他节点立即刷新关于这个节点的认识,例如当一次故障转移操作成功执行之后,新的主节点会向集群广播一条 PONG 消息,以此来让集群中的其他节点立即知道这个节点已经变成了主节点,并且接管了已下线节点负责的槽。
  • FAIL消息∶当一个主节点A判断另一个主节点B已经进人FAIL状态时,节点A 会向集群广播一条关于节点B的FAIL消息,所有收到这条消息的节点都会立即将节点B标记为已下线。
  • PUBLISH消息∶当节点接收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条 PUBLISH消息,所有接收到这条 PUBLISH消息的节点都会执行相同的PUBLISH命令。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值