Redis集群是Redis提供的分布式数据库方案,通过分片来进行数据共享,并提供复制和故障转移功能。
一:初始化
1:数据结构
在源码中,通过server.cluster记录整个集群当前的状态,比如集群中的所有节点;集群目前的状态,比如是上线还是下线;集群当前的纪元等等。该属性是一个clusterState类型的结构体。该结构体的定义如下:
typedef struct clusterState {
clusterNode *myself; /* This node */
...
int state; /* REDIS_CLUSTER_OK, REDIS_CLUSTER_FAIL, ... */
int size; /* Num of master nodes with at least one slot */
dict *nodes; /* Hash table of name -> clusterNode structures */
...
clusterNode *slots[REDIS_CLUSTER_SLOTS];
zskiplist *slots_to_keys;
...
} clusterState;
myself指向当前Redis实例所表示的节点;state表示集群状态;字典nodes中记录了,包括自己在内的所有集群节点,该字典以节点名为key,以结构体clusterNode为value。其他属性与具体的流程相关,后续在介绍集群各种流程时会介绍。
集群中的节点是由clusterNode表示的,该结构体的定义如下:
typedef struct clusterNode {
mstime_t ctime; /* Node object creation time. */
char name[REDIS_CLUSTER_NAMELEN]; /* Node name, hex string, sha1-size */
int flags; /* REDIS_NODE_... */
...
mstime_t ping_sent; /* Unix time we sent latest ping */
mstime_t pong_received; /* Unix time we received the pong */
...
char ip[REDIS_IP_STR_LEN]; /* Latest known IP address of this node */
int port; /* Latest known port of this node */
clusterLink *link; /* TCP/IP link with this node */
list *fail_reports; /* List of nodes signaling this as failing */
} clusterNode;
该结构体记录了节点的状态和属性。ctime表示节点的创建时间;name表示节点名,每个节点都有一个40字节长的随机字符串作为名字,该名字同时也作为该节点在字典server.cluster->nodes中的key;flags表示节点的类型和状态,比如节点是否下线,是主节点还是从节点等,都记录在标志位flags中;ip和port表示该节点的地址属性;link表示当前节点与该节点间的TCP连接,该结构中包含socket描述符、输入缓冲区和输出缓冲区等属性。在link所表示的TCP连接中,当前节点为客户端,clusterNode所表示的节点为服务端。其他属性与具体的流程相关,后续在介绍集群各种流程时会介绍。
2:初始化
Redis实例启动时,根据配置文件中的"cluster-enabled"选项,决定该Redis实例是否处于集群模式。如果该选项值为”yes”,则Redis实例中的server.cluster_enabled被置为1,表示当前处于集群模式。
在集群模式下,Redis实例启动时,首先会调用clusterInit函数,初始化集群需要使用的结构,并创建监听端口。该函数的代码如下:
void clusterInit(void) {
int saveconf = 0;
server.cluster = zmalloc(sizeof(clusterState));
server.cluster->myself = NULL;
server.cluster->currentEpoch = 0;
server.cluster->state = REDIS_CLUSTER_FAIL;
server.cluster->size = 1;
server.cluster->todo_before_sleep = 0;
server.cluster->nodes = dictCreate(&clusterNodesDictType,NULL);
server.cluster->nodes_black_list =
dictCreate(&clusterNodesBlackListDictType,NULL);
server.cluster->failover_auth_time = 0;
server.cluster->failover_auth_count = 0;
server.cluster->failover_auth_rank = 0;
server.cluster->failover_auth_epoch = 0;
server.cluster->cant_failover_reason = REDIS_CLUSTER_CANT_FAILOVER_NONE;
server.cluster->lastVoteEpoch = 0;
server.cluster->stats_bus_messages_sent = 0;
server.cluster->stats_bus_messages_received = 0;
memset(server.cluster->slots,0, sizeof(server.cluster->slots));
clusterCloseAllSlots();
/* Lock the cluster config file to make sure every node uses
* its own nodes.conf. */
if (clusterLockConfig(server.cluster_configfile) == REDIS_ERR)
exit(1);
/* Load or create a new nodes configuration. */
if (clusterLoadConfig(server.cluster_configfile) == REDIS_ERR) {
/* No configuration found. We will just use the random name provided
* by the createClusterNode() function. */
myself = server.cluster->myself =
createClusterNode(NULL,REDIS_NODE_MYSELF|REDIS_NODE_MASTER);
redisLog(REDIS_NOTICE,"No cluster configuration found, I'm %.40s",
myself->name);
clusterAddNode(myself);
saveconf = 1;
}
if (saveconf) clusterSaveConfigOrDie(1);
/* We need a listening TCP port for our cluster messaging needs. */
server.cfd_count = 0;
/* Port sanity check II
* The other handshake port check is triggered too late to stop
* us from trying to use a too-high cluster port number. */
if (server.port > (65535-REDIS_CLUSTER_PORT_INCR)) {
redisLog(REDIS_WARNING, "Redis port number too high. "
"Cluster communication port is 10,000 port "
"numbers higher than your Redis port. "
"Your Redis port number must be "
"lower than 55535.");
exit(1);
}
if (listenToPort(server.port+REDIS_CLUSTER_PORT_INCR,
server.cfd,&server.cfd_count) == REDIS_ERR)
{
exit(1);
} else {
int j;
for (j = 0; j < server.cfd_count; j++) {
if (aeCreateFileEvent(server.el, server.cfd[j], AE_READABLE,
clusterAcceptHandler, NULL) == AE_ERR)
redisPanic("Unrecoverable error creating Redis Cluster "
"file event.");
}
}
/* The slots -> keys map is a sorted set. Init it. */
server.cluster->slots_to_keys = zslCreate();
/* Set myself->port to my listening port, we'll just need to discover
* the IP address via MEET messages. */
myself->port = server.port;
server.cluster->mf_end = 0;
resetManualFailover();
}
在该函数中,首先初始化clusterState结构类型server.cluster中的各个属性;
如果在Redis配置文件中指定了"cluster-config-file"选项的值,则用server.cluster_configfile属性记录该选项值,表示集群配置文件。接下来,就根据配置文件的内容,初始化server.cluster中的各个属性;
如果加载集群配置文件失败(或者配置文件不存在),则以REDIS_NODE_MYSELF和REDIS_NODE_MASTER为标记,创建一个clusterNode结构表示自己本身,置为主节点,并设置自己的名字为一个40字节的随机串;然后将该节点添加到server.cluster->nodes中;
接下来,调用listenToPort函数,在集群监端口上创建socket描述符进行监听。该集群监听端口是在Redis监听端口基础上加10000,比如如果Redis监听客户端的端口为6379,则集群监听端口就是16379,该监听端口用于接收其他集群节点的TCP建链,集群中的每个节点,都会与其他节点进行建链,因此整个集群就形成了一个强连通网状图;
然后注册监听端口上的可读事件,事件回调函数为clusterAcceptHandler。
当当前节点收到其他集群节点发来的TCP建链请求之后,就会调用clusterAcceptHandler函数accept连接。在clusterAcceptHandler函数中,对于每个已经accept的链接,都会创建一个clusterLink结构表示该链接,并注册socket描述符上的可读事件,事件回调函数为clusterReadHandler。
二:集群节点间的握手
1:CLUSTER MEET命令
Redis实例以集群模式启动之后,此时,在它的视角中,当前集群只有他自己一个节点。如何认识集群中的其他节点呢,这就需要客户端发送”CLUSTER MEET”命令。
客户端向集群节点A发送命令” CLUSTER MEET nodeB_ip nodeB_port”, 其中的nodeB_ip和nodeB_port,表示节点B的ip和port。节点A收到客户端发来的该命令后,调用clusterCommand函数处理。这部分的代码如下:
if (!strcasecmp(c->argv[1]->ptr,"meet") && c->argc == 4) {
long long port;
if (getLongLongFromObject(c->argv[3], &port) != REDIS_OK) {
addReplyErrorFormat(c,"Invalid TCP port specified: %s",
(char*)c->argv[3]->ptr);
return;
}
if (clusterStartHandshake(c->argv[2]->ptr,port) == 0 &&
errno == EINVAL)
{
addReplyErrorFormat(c,"Invalid node address specified: %s:%s",
(char*)c->argv[2]->ptr, (char*)c->argv[3]->ptr);
} else {
addReply(c,shared.ok);
}
}
以命令中的ip和port为参数,调用clusterStartHandshake函数,节点A开始向节点B进行握手。
在clusterStartHandshake函数中,会以REDIS_NODE_HANDSHAKE|REDIS_NODE_MEET为标志,创建一个clusterNode结构表示节点B,该结构的ip和port属性分别置为节点B的ip和port,并将该节点插入到字典server.cluster->nodes中。 这部分的代码如下:
/* Add the node with a random address (NULL as first argument to
* createClusterNode()). Everything will be fixed during the
* handshake. */
n = createClusterNode(NULL,REDIS_NODE_HANDSHAKE|REDIS_NODE_MEET);
memcpy(n->ip,norm_ip,sizeof(n->ip));
n->port = port;
clusterAddNode(n);
注意,因为此时A还不知道节点B的名字,因此以NULL为参数调用函数createClusterNode,该函数中,会暂时以一个随机串当做B的名字,后续交互过程中,节点B会在PONG包中发来自己的名字。
2:TCP建链
在集群定时器函数clusterCron中,会轮训字典server.cluster->nodes中的每一个节点node,一旦发现node->link为NULL,就表示尚未向该节点建链(或是之前的连接已断开)。因此,开始向其集群端口发起TCP建链,这部分代码如下:
if (node->link == NULL) {
int fd;
mstime_t old_ping_sent;
clusterLink *link;
fd = anetTcpNonBlockBindConnect(server.neterr, node->ip,
node->port+REDIS_CLUSTER_PORT_INCR, REDIS_BIND_ADDR);
if (fd == -1) {
/* We got a synchronous erro