Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能
1.节点
一个Redis集群通常由多个节点(node)组成,在刚开始每个节点都是独立的。我们要做的是将各个独立的节点连接起来构成一个包含多个节点的集群。
连接各个节点的工作可以使用CLUSTER MEET命令:
CLUSTER MEET <ip> <port>
向一个节点发送CLUSTER MEET命令,可以让node节点与ip和port所指定的节点进行握手(handshake),当握手成功时,node节点就会将Ip和port所指定的节点添加到node节点当前所在的集群中。
1.1 启动节点
一个节点就是一个运行在集群模式下的Redis服务器,Redis服务器在启动时会根据cluster-enabled配置选项是否为yes来决定是否要开启服务器的集群模式。节点会继续使用所有在单机模式中使用的服务器组件。除此之外,节点会继续使用redisServer结构来保存服务器的状态,使用redisClient结构来保存客户端的状态。
至于那些只有在集群模式才会用到的数据,节点将它们保存到了cluster.h/clusterNode结构、cluster.h/clusterLink结构、cluster.h/clusterState结构里面。
1.2 集群数据结构
clusterNode结构保存了一个节点的当前状态,每个节点都会使用一个clusterNode结构来记录自己的状态,并为集群中的所有其他节点都创建一个相应的clusterNode结构,以此来记录其他节点的状态:
struct clusterNode{
//创建节点的时间
mstime_t ctime;
//节点的名字,由40个十六进制字符组成
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;
//输出缓冲区,保存着等待发送给其他节点的消息
sds sndbuf;
//输入缓冲区,保存着从其他节点接收到的消息
sds rcvbuf;
//与这个连接相关联的节点,如果没有的话就为NULL
struct clusterNode *node;
} clusterLink;
redisClient结构和clusterLink结构区别:redisClient结构中的套接字和缓冲区是用于连接客户端,clusterLink结构中的套接字和缓冲区则用于连接节点。
最后,每个节点都保存着一个clusterState结构:
typedef struct clusterState{
//指向当前节点的指针
clusterNode *myself;
//集群当前的配置的纪元,用于实现故障转移
uint64_t currentEpoch;
//集群当前的状态:在线还是下线
int state;
//集群中至少处理着一个槽的节点的数量
int size;
//集群节点名单(包括myself)
//字典的键为节点的名字,字典的值为节点对应的clusterNode结构
dict *nodes;
//...
} clusterState;
1.3 CLUSTER MEET命令的实现
通过向节点A发送CLUSTER MEET命令,客户端可以让接收命令的节点A将另一个节点B添加到节点A当前所在的集群里面:
CLUSTER MEET <ip> <port>
收到命令的节点A将与节点B进行握手,因此来确认彼此的存在,并为将来的进一步通信打好基础:
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,握手完成
2.槽指派
Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽中的一个,集群中的每个节点可以处理0-16384个槽。
当数据库中的16384个槽都由节点在处理时,集群处于上线状态(ok);相反,如果数据库中任何一个槽没有得到处理,那么集群处于下线状态。
通过向节点发送CLUSTER ADDSLOTS命令,我们可以将一个或多个槽指派(assign)给节点负责:
CLUSTER ADDSLOTS <slot> [slot ...]
2.1 记录节点的槽指派信息
clusterNode结构的slots属性和numslot属性记录了节点负责处理哪些槽:
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;若为0,表示不负责处理i。
因为取出和设置slots数组中的任意一个二进制位的值得复杂度仅为O(1),所以对于一个给定节点的slots数组来说,程序检查节点是否负责处理某个槽,又或者将某个槽指派给节点负责,这两个动作的复杂度都是O(1)
2.2 传播节点的槽指派信息
一个节点除了会将自己负责处理的槽记录在clusterNode结构slots属性和numslots属性之外,它还会将自己的slots数组通过消息发送给集群中的其他节点,以此告知其他节点自己目前负责处理哪些槽。
因为集群中的每个节点都会将自己的slots数组通过消息发送给集群中的其他节点,并且每个接收到slots数组的节点都会将数组保存到相应的clusterNode结构里面,因此,集群中的每个节点都会知道数据库中的16384个槽分别被指派给了集群中的哪些节点。
2.3 记录集群所有槽的指派信息
clusterState结构中的slots数组记录了集群中所有16384个槽的指派信息:
typedef struct clusterState{
//...
clusterNode *slot[16384];
//...
} clusterState;
slots数组包含了16384个项,每个数组项都是一个指向clusterNode结构的指针:如果slots[i]指向一个clusterNode结构,表示槽i已经指派给clusterNode结构所代表的节点;如果为NULL,表示没有指派。
如果clusterNode.slots只记录槽的指派信息,那么为了知道槽i指派给了哪个节点需要遍历节点,复杂度为O(N);而通过将所有槽的指派信息保存在clusterState.slots里,直接就能取得,复杂度为O(1)。
注:虽然clusterState.slots数组记录了集群中所有槽的指派信息,但使用clusterNode结构的slots数组来记录单个节点的槽指派信息仍然是由必要的。
clusterState.slots数组记录了集群中所有槽的指派信息,而clusterNode.slots数组只记录了clusterNode结构所代表的的节点的槽指派信息。这是两个slots数组的关键区别所在。
2.4 CLUSTER ADDSLOTS 命令的实现
CLUSTER ADDSLOTS命令接受一个或多个槽作为参数,并将所有输入的槽指派给接收该命令的节点负责:
CLUSTER ADDSLOTS <slot> [slot ...]
CLUSTER ADDSLOTS命令的实现逻辑:
1)遍历所有输入槽,检查它们是否都是未指派槽
2)如果有哪怕一个槽已经被指派给某个节点,那么向客户端返回错误,并终止命令执行
3)如果所有输入槽都是未指派槽,那么再次遍历所有输入槽,将这些槽指派给当前节点
4)设置clusterState结构的slots数组,将slots[i]的指针指向代表当前节点的clusterNode结构
5)访问代表当前节点的clusterNode结构的slots数组,将数组在索引i上的二进制位设置为1
3.在集群中执行命令
在对数据库中的16384个槽都进行了指派之后,集群就会进入上线状态,这时客户端就可以向集群中的节点发送数据命令了。
当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了自己:如果是给了自己,那么执行;否则,节点会向客户端返回一个MOVED错误,指引客户端转向(redirect)至正确的节点,并再次发送之前想要执行的命令。
3.1 计算键属于哪个槽
节点使用以下算法来计算给定键key属于哪个槽:
def slot_number(key):
return CRC16(key) & 16383
其中CRC16(key)语句用于计算键key的CRC-16校验和,而&16383则用于计算出一个介于0-16383之间的整数作为键key的槽号。
CLUSTER KEYSLOT <key>命令可以查看一个给定键属于哪个槽。
3.2 判断槽是否由当前节点负责处理
当节点计算出键所属的槽i之后,节点就会检查自己在clusterState.slots数组中的项i,判断键所在的槽是否由自己负责:如果自己负责,节点执行客户端发送的命令;如果不由自己负责,节点会根据clusterState.slots[i]指向的clusterNode结构所记录的节点IP和端口号,向客户端返回MOVED错误,指引客户端转向至正在处理槽i的节点。
3.3 MOVED错误
当节点发现键所在的槽并非由自己负责的时候,节点就会向客户端返回一个MOVED错误,指引客户端转向至正在负责槽的节点。
MOVED错误的格式为:
MOVED <slot> <ip>:<port>
其中,slot为键所在的槽,ip和port为负责处理槽slot的节点的IP地址和端口号。
当客户端接收到节点返回的MOVED错误时,客户端会根据MOVED错误中提供的IP地址和端口号,转向至负责处理槽slot的节点,并向该节点重新发送之前想要执行的命令。
一个集群客户端通常会与集群中的多个节点创建套接字连接,而所谓的节点转向实际上就是换一个套接字发送命令。
如果客户端尚未与想要转向的节点创建套接字连接,那么客户端会先根据MOVED错误提供的IP地址和端口号来连接节点,然后再进行转向
集群模式的redis-cli在接收到MOVED错误时不会打印,而是直接转向;单机模式会打印
3.4 节点数据库的实现
集群节点保存键值对以及键值对对过期时间的方式与单机模式一样。
节点和单机服务器在数据库方面的一个区别是,节点只能使用0号数据库,而单机服务器则没有这个限制。
除此之外,除了将键值对保存在数据库里面之外,节点还会用clusterState结构中的slots_to_keys跳跃表来保存槽和键之间的关系:
typedef struct clusterState{
//...
zskiplist *slots_to_keys;
//...
} clusterState;
slots_to_keys跳跃表每个节点的分值(score)都是槽号,而每个节点的成员(member)都是一个数据库键:
1)每当节点往数据库中添加一个新的键值对时,节点就会将这个键以及键的槽号关联到slots_to_keys跳跃表
2)当节点删除数据库中的某个键值对时,节点就会在slots_to_keys跳跃表解除被删除键与槽号的关联
4.重新分片
Redis集群的重新分片操作可以将任意数量已经指派给源节点的槽改为指派给目标节点,并且相关槽所属的键值对也会从源节点被移动到目标节点。
重新分片操作可以在线进行,在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理请求命令。
重新分片的实现原理:
Redis集群的重新分片操作是由Redis的集群管理软件redis-trib负责执行的,Redis提供了进行重新分片所需的所有命令,而redis-trib则通过向源节点和目标节点发送命令来进行重新分片操作。
redis-trib对集群的单个槽slot进行重新分片的步骤如下:
1)redis-trib对目标节点发送CLUSTER SETSLOT <slot> IMPORTING <source_id> 命令,让目标节点准备好从源节点导入(import)属于槽slot的键值对。
2)redis-trib对源节点发送CLUSTER SETSLOT <slot> MIGRATING <target_id> 命令,让源节点准备好将属于槽slot的键值对迁移(migrate)至目标节点
3)redis-trib对源节点发送CLUSTER GETKEYSINSLOT <slot> <count> 命令,获得最多count个属于槽slot的键值对的键名(key name)
4)对于步骤3获得的每个键名,redis-trib都向源节点发送一个MIGRATING <target_id> <target_port> <key_name> 0 <timeout>命令,将被选中的键原子地从源节点迁移至目标节点
5)重复执行步骤3、4,直到源节点保存的所有属于槽slot的键值对都被迁移至目标节点为止。
6)redis-trib向集群中的任意一个节点发送CLUSTER SETSLOT <slot> NODE <target_id>命令,将槽slot指派给目标节点,这一指派信息会通过消息发送至整个集群,最终集群中的所有节点都会知道槽slot已经指派给了目标节点
5.ASK错误
在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。
当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时:
1)源节点会现在自己的数据库里面查找指定的键,如果找到的话,就直接执行客户端发送的命令
2)相反地,如果源节点没能在自己的数据库里面找到指定的键,那么这个键有可能已经被迁移到了目标节点,源节点将向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点,并在此发送之前想要执行的命令
注:集群模式的redis-cli在集群模式直接转向不打印,在单机模式会打印
5.1 CLUSTER SETSLOT IMPORTING命令的实现
clusterState结构的importing_slots_from数组记录了当前节点正在从其他节点导入的槽:
typedef struct clusterState{
//...
clusterNode *importing_slots_from[16384];
//...
} clusterState;
如果importing_slots_from[i]的值不为NULL,而是指向一个clusterNode结构,那么表示当前节点正在从clusterNode所代表的的节点导入槽i。
在对集群进行重新分片的时候,向目标节点发送命令:
CLUSTER SETSLOT <i> IMPORTING <source_id>
可以将目标节点clusterState.importing_slots_from[i]的值设置为source_id所代表节点的clusterNode结构。
5.2 CLUSTER SETSLOT MIGRATING命令的实现
clusterState结构的migrating_slots_to数组记录了当前节点正在迁移至其他节点的槽:
typedef struct clusterState{
//...
clusterNode *migrating_slots_to[16384];
//...
} clusterState;
如果migrating_slots_to[i]的值不为NULL,而是指向一个clusterNode结构,那么表示当前节点正在将槽i迁移至clusterNode所代表的节点。
在对集群进行重新分片的时候,向源节点发送命令:
CLUSTER SETSLOT <i> MIGRATING <target_id>
可以将源节点clusterState.migrating_slots_to[i]的值设置为target_id所代表的的clusterNode结构
5.3 ASK错误
如果节点收到一个关于键key的命令请求,并且键key所属的槽i正好就指派给了这个节点,那么节点会尝试在自己的数据库里查找键key,并返回。
如果节点没有在自己的数据库里找到键key,那么节点会检查自己的clusterState.migrating_slots_to[i],看key所属的槽i是否正在进行迁移,如果正在迁移,那么节点会向客户端发送一个ASK错误,引导客户端到正在导入槽i的节点中去查找键key
5.4 ASKING命令
ASKING命令唯一要做的就是打开发送该命令的客户端的REDIS_ASKING标识,然后向客户端返回OK回复。
在一般情况下,如果客户端向节点发送一个关于槽i的命令,而槽i又没有指派给这个节点的话,那么节点将向客户端返回一个MOVED错误;但是如果节点的clusterState.importing_slots_form[i]显示节点正在导入槽i,并且发送命令的客户端带有REDIS_ASKING标识,那么节点将破例执行这个关于槽i的命令一次。
当客户端接收到ASK错误并转向至正在导入槽的节点时,客户端会先想节点发送一个ASKING命令,然后才重新发送想要执行的命令。因为如果客户端不发送ASKING命令,而直接发送想要执行的命令的话,那么客户端发送的命令会被节点拒接,并返回MOVED错误。
注:客户端的REDIS_ASKING标识是一个一次性的标识,当节点执行了一个带有REDIS_ASKING标识的客户端发送的命令之后,客户端的REDIS_ASKING标识会被移除
5.5 ASK错误和MOVED错误的区别
ASK错误和MOVED错误都会导致客户端转向,区别在于:
1)MOVED错误代表槽的负责权已经从一个节点转到另一个节点:在客户端接收到关于槽i的MOVED错误之后,客户端每次遇到关于槽i的命令请求时,都可以直接将命令请求发送至MOVED错误所指向的节点,因为该节点就是目前负责槽i的节点
2)与此相反,ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施:在客户端接收到关于槽i的ASK错误之后,客户端只会在接下来的一次命令请求中将关于槽i的命令请求发送至ASK错误所指示的节点,但这种转向不会对客户端今后发送关于槽i的命令请求产生任何影响,客户端仍然会将关于槽i的命令请求发送至目前所负责处理槽i的节点,除非ASK再次出现错误。
6.故障和复制转移
Redis中的节点分为主节点(master)和从节点(slave),其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。
6.1 设置从节点
向一个节点发送命令:
CLUSTER REPLICATE <node_id>
可以让接收命令的节点成为node_id所指定节点的从节点,并开始对主节点进行复制:
1)接收到该命令的节点首先会在自己的clusterState.nodes字典中找到node_id所对应的节点clusterNode结构,并将自己的clusterState.myself.slaveof指针指向这个结构,以此来记录这个节点正在复制的主节点:
struct clusterNode{
//...
//如果这是一个从节点,那么指向主节点
struct clusterNode *slaveof;
//...
};
2)然后节点会修改自己在clusterState.myself.flags中的属性,关闭原本的REDIS_NODE_MASTER标识,打开REDIS_NODE_SLAVE标识,表示这个节点已经由原来的主节点变成了从节点。
3)最终,节点会调用复制代码,并根据clusterState.myself.slaveof指向的clusterNode结构所保存的IP地址和端口号,对主节点进行复制。因为节点的复制功能和单机Redis复制代码一样,所以让从节点复制主节点相当于向从节点发送命令SLAVEOF <master_ip> <master_port>
4)一个节点成为从节点,并开始复制某个主节点这个信息会通过消息发送给集群中的其他节点,最终集群中的所有节点都会知道某个从节点正在复制某个主节点
集群中的所有节点都会代表主节点的clusterNode结构的slaves属性和numslaves属性中记录正在复制这个主节点的从节点名单:
struct clusterNode{
//...
//正在复制这个主节点的从节点数量
int numslaves;
//一个数组,每个数组项指向一个正在复制这个主节点的从节点的clusterNode节点
struct clusterNode **slaves;
//...
};
6.2 故障检测
集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此来检测对方是否在线,如果接收PING消息的节点在规定时间内没有返回PONG消息,那么发送PING消息的节点就会将接受消息的节点标记为疑似下线(PFAIL)。
集群中的各个节点会通过互相发送消息的方式来交换集群中各个节点的状态信息,比如在线、疑似下线(PFAIL)、下线(FAIL)。
当主节点A通过消息得知主节点B认为主节点C疑似下线,主节点A会在自己的clusterState.nodes字典里找到主节点C的clusterNode结构,并将主节点B的下线报告(failure report)添加到clusterNode结构的fail_reports链表里面:
struct clusterNode{
//...
//一个链表,记录了所有其他节点对该节点的下线报告
list *fail_reports;
//...
};
每个下线报告由一个clusterNodeFailReport结构表示:
struct clusterNodeFailReport{
//报告目标节点已经下线的节点
struct clusterNode *node;
//最后一次从node节点收到下线报告的时间
mstime_t time;
} typedef clusterNodeFailReport;
如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线,那么这个主节点x就被标记为已下线(FAIL),将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线。
6.3 故障转移
当一个从节点发现自己正在复制的主节点进入已下线状态时,从节点将开始对下线主节点进行故障转移,以下是故障转移的执行步骤:
1)复制下线主节点的所有从节点里面,会有一个从节点被选中
2)被选中的从节点会执行SLAVEOF no one命令,成为新的主节点
3)新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全都指派给自己
4)新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。
5)新的主节点开始接受和负责自己处理的槽有关的命令请求,故障转移完成
6.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)如果在一个配置纪元里面没有从节点能收集到足够多的的支持票,那么集群进入一个新的配置纪元,并在此进行选举,直到选出新的主节点为止
7. 消息
集群中的各个节点通过发送和接收消息(message)来进行通信,我们称发送消息的节点为发送者(sender),接收消息的节点为接受者(receiver)。节点发送的消息主要有以下五种:
1)MEET消息:当发送者接到客户端发送的CLUSTER MEET命令时,发送者会向接受者发送MEET消息,请求接收者加入到发送者当前所处的集群里面
2)PING消息:集群里面的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线。除此之外,如果节点A最后一次收到节点B发送的PONG消息的时间距离当前时间已经超过节点A的cluster-node-timeout选项设置时长的一半,那么节点A也会向节点B发送PING消息,这可以防止节点A因为长时间没有随机选中节点B作为PING消息的发送对象而导致对节点B的信息更新滞后
3)PONG消息:当接受者收到发送者发来的MEET消息或者PING消息时,为了向发送者确认这条MEET消息或者PING消息已到达,接受者会向发送者返回一条PONG消息。另外,一个节点也可以通过向集群广播自己的PONG消息来让集群中的其他节点立即刷新关于这个节点的认识。
4)FAIL消息:当一个主节点A判断另一个主节点B已经进入FAIL状态时,节点A会向集群广播一条关于节点B的FAIL消息。所有收到这个消息的节点都会立即将节点B标记为已下线
5)PUBLISH消息:当节点接收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息。所有接收到这条PUBLISH消息的节点都会执行相同的PUBLISH命令。
一条消息由消息头(header)和消息正文(data)组成
7.1 消息头
节点发送的所有消息都由一个消息头包裹,消息头除了包含消息正文之外,还记录了消息发送者自身的一些消息,所以消息头也可以认为是消息的一部分。
每个消息头都由一个cluster.h/clusterMsg结构表示:
typedef struct{
//消息的长度
uint32_t totlen;
//消息的类型
uint16_t type;
//消息正文包含的节点信息数量,只在发送MEET、PING、PONG时使用
uint16_t count;
//发送者所处的配置纪元
uint64_t currentEpoch;
//如果发送者是一个主节点,记录发送者的配置纪元
//如果发送者是一个从节点,记录发送者正在复制的主节点的配置纪元
uint64_t configEpoch;
//发送者的名字(ID)
char sender[REDIS_CLUSTER_NAMELEN];
//发送者目前的槽指派信息
unsigned char myslots[REDIS_CLUSTER_SLOTS/8];
//如果发送者是一个主节点,记录REDIS_NODE_NULL_NAME
//如果发送者是一个从节点,记录发送者正在复制的主节点的名字
char slaveof[REDIS_CLUSTER_NAMELEN];
//发送者的端口号
uint16_t port;
//发送者的标识值
uint16_t flags;
//发送者所处集群的状态
//unsigned char state;
//消息的正文
union clusterMsgData data;
}clusterMsg;
clusterMsg.data属性指向联合cluster.h/clusterMsgData,这个联合就是消息的正文:
union clusterMsgData{
//MEET、PING、PONG消息的正文
struct{
//每条MEET、PING、PONG消息都包含两个clusterMsgDataGossip结构;
clusterMsgDataGossip gossip[1];
} ping;
//FAIL消息的正文
struct{
clusterMsgDataFail about;
} fail;
//PUBLISH消息的正文
struct{
clusterMsgDataPublish msg;
} publish;
//其他消息正文
};
7.2 MEET、PING、PONG消息的实现
Redis集群中的各个各个节点通过Gossip协议进行交换各自关于不同节点的状态信息,其中Gossip协议由MEET、PING、PONG三种消息实现,这三种消息的正文都由两个cluster.h/clusterMsgDataGossip结构组成:
union clusterMsgData{
//...
struct{
//每条MEET、PING、PONG消息都包含两个clusterMsgDataGossip结构;
clusterMsgDataGossip gossip[1];
} ping;
};
通过表头type属性判断到底是三种中的哪一种。
每次发送MEET、PING、PONG消息时,发送者都从自己的已知节点列表中随机选择两个节点,并将这两个节点的信息分别保存在两个clusterMsgDataGossip 结构里。
clusterMsgDataGossip 结构记录了被选中节点的信息:
typedef struct {
//节点的名字
char nodename[REDIS_CLUSTER_NAMELEN];
//最后一次向该节点发送PING消息的时间戳
uint32_t ping_sent;
//最后一次从该节点接收到PONG消息的时间戳
uint32_t pong_sent;
//节点的IP
char ip[16];
//节点的端口号
uint16_t port;
//节点的标识值
uint16_t flags;
} clusterMsgDataGossip;
当接收者收到MEET、PING、PONG消息时,接收者会访问消息正文中的两个clusterMsgDataGossip结构,并根据自己是否认识clusterMsgDataGossip结构中记录的节点来进行如下操作:
1)如果不认识,那么接收者根据IP和端口号等进行握手
2)如果认识,更新对应的clusterNode结构
7.3 FAIL消息的实现
当集群里的主节点A将主节点B标记为已下线(FAIL),主节点A将向集群广播这个消息,所有接收到这个消息的节点都将B设置为已下线。
在集群的节点数量比较大时,单纯Gossip协议会有延时,因为传播到整个集群需要时间。发送FAIL消息可以让集群所有节点立即知道某个主节点已下线。
FAIL消息的正文由cluster.h/clusterMsgDataFail结构表示:
typedef struct {
char nodename[REDIS_CLUSTER_NAMELEN];
} clusterMsgDataFail;
因为集群里的所有节点都是一个独一无二的名字,所以FAIL消息里面只需要保存下线节点的名字,接收到消息的节点就可以根据这个名字来判断是哪个节点下线了。
7.4 PUBLISH消息的实现
当客户端向集群中的某个节点发送命令:
PUBLISH <channel> <message>
的时候,接收到PUBLISH命令的节点不仅会向channel频道发送message,它还会向集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会向channel频道发送message消息。
PUBLISH消息的正文由cluster.h/clusterMsgDataPublish结构表示:
typedef struct{
uint32_t channel_len;
uint32_t message_len;
//定义为8字节,只是为了对齐其他消息结构,实际的长度由保存的内容决定
unsigned char bulk_data[8];
} clusterMsgDataPublish;
clusterMsgDataPublish结构的bulk_data属性是一个字节数组,这个字节数组保存了客户端通过PUBLISH命令发送给节点的channel参数和message参数。
1)bulk_data的0-channel_len-1字节保存的是channel参数
2)后面保存的是message参数
注:为什么不直接向节点广播PUBLISH?
这样不符合Redis集群的“各个节点通过发送和接收消息来进行通信”这一规则