redis集群是redis提供分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移功能
节点
集群有多个节点组成,刚开始时,每个节点相互独立,都处于只包含自己的集群中,要组建一个集群,就需要将其余节点连接起来,构成一个包含多个节点的集群
通过如下命令查看当前节点以及集群中其余节点信息:
CLUSTER NODES
通过如下命令来完成连接各个节点:
CLUSTER MEET <ip> <port>
启动节点
根据配置文件中cluster-enabled配置选项决定是否启用集群模式
集群数据结构
每个节点会使用一个cluster结构来记录自己的状态,并为集群所有其余节点都创建一个相应的cluster结构,以此记录其他节点状态
struct clusterNode {
mstime_t ctime; // 创建节点时间
char name[REDIS_CLUSTER_NAMELEN]; // 节点名字,有40个十六进制字符组成
int flags; // 节点标识
uint64_t configEpoch; // 节点当前配置纪元,用于实现故障转移
char ip[REDIS_IP_STR_LEN]; // 节点的IP地址
int port; // 节点的端口号
clusterLink *link; // 保存连接节点所需的有关信息
...;
};
clusterNode结构的link属性是一个clusterLink结构,该结构报讯了连接节点所需的有关信息
typedef struct clusterLink {
mstime_t ctime; // 连接创建时间
int fd; // TCP套接字描述符
sds sndbuf; // 输出缓冲区
sds rcvbuf; // 输入缓冲区
struct clusterNode *node; // 与这个节点相关联的节点,如果没有的话为NULL
} clusterLink;
每个节点都保存着一个clusterState结构,这个结构记录了在当前节点的视角下,集群目前所处的状态
typedef struct clusterState {
clusterNode *myself; // 指向当前节点的指针
uint64_t currentEpoch; // 集群当前的配置纪元,用于实现故障转移
int state; // 集群当前状态:在线还是下线
int size; // 集群中至少处理着一个槽的节点数量
dict *nodes; // 集群节点名单(包括myself节点)
...;
} clusterState;
CLUSTER MEET命令的实现
通过向节点A发送CLUSTER MEET命令,客户端可以让接受命令的节点A将另一个节点B添加到节点A当前的集群当中:---- AB节点进行握手
A -> B : A为B创建clusterNode结构,向B节点发送MEET消息
B -> A : B接受到消息,为A创建clusterNode结构,向A节点回复PONG消息
A -> B : A接受到PONG消息后,向B发送PING消息
这样握手完成,之后,节点A将节点B的信息通过Gossip协议传播给集群中其余节点,让其他节点也与B节点进行握手
最终,一段时间后,节点B会被集群中所有节点认识
槽指派
redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽
当16384个槽都有节点处理时,集群处于上线状态;任何一个槽没有被处理,那么集群处于下线状态,通过如下命令查看:
CLUSTER INFO
通过如下命令指定槽:
CLUSTER ADDSLOTS <slot> [slot ...]
eg: CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000
记录节点的槽指派信息
struct clusterNode {
...;
unsigned char slots[16384/8]; // 二进制数组
int numslots; // 记录该节点负责处理的槽的数量
...;
};
数组中二进制位1时,表示该节点负责该槽,否则表示该节点不负责槽i
传播节点的槽指派信息
一个节点除了会将自己负责的槽记录在clusterNode结构的slots属性和numslots属性之外,还会将自己的slots数组通过消息发送给集群中的其他节点,以此来告知其他节点自己目前负责哪些槽
记录集群所有槽的指派信息
typedef struct clusterState {
...;
clusterNode *slots[16384];
...;
} clusterState;
solts数组包含16384个项,每个数组项都是一个指向clusterNode结构指针。注意如果slots[i]为NULL,表示这个槽没有指派给任何节点;
虽然clusterState.slots数组记录了集群中所有槽指派信息,但使用clusterNode结构的slots数组记录单个节点的槽指派信息仍然是有必要的
CLUSTER ADDSLOTS命令的实现
需要同时设置clusterState.slots数组以及该节点的clusterNode.slots数组二进制
先检查输入槽是否被指派,否则就向客户端返回错误,并终止命令执行
在集群中执行命令
当数据库所有的槽(16384)都指派之后,集群进入上线状态
这时候,当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令演出里的数据库键属于哪个槽,并检查这个槽是否指派给了自己:
- 如果键所在的槽正好就指派了当前节点,那么节点直接执行这个命令
- 如果键所在的槽并没有指派给当前节点,那么节点会向客户端返回一个MOVED错误,指引客户端转向至正确的节点,并再次发送之前想要执行的命令
那么怎么计算某个键属于哪个槽、怎么判断某个槽是否由自己负责,以及MOVED错误的实现方法
计算键属于哪个槽
def slot_number(key):
return CRC16(key) & 16383
其中CRC16(key)用于计算键key的CRC-16校验和,而 ”& 16383“则相当于“ % 16384”;
这里对于N为2的次幂,那么:
x % n = x & (n - 1)
CLUSTER KEYSLOT命令是查看键属于哪个槽:
判断槽是否由当前节点负责处理
当节点计算出键所属的槽 i 之后,节点会检查自己在clusterState.slots数组中的项 i,判断键所在的槽是否由自己负责:
- 若clusterState.slots[i] == clusterState.myself,说明槽 i 由当前节点负责,节点可以执行客户端发送的命令
- 若clusterState.slots[i] != clusterState.myself,说明槽 i不 由当前节点负责,会根据clusterState.slots[i]指向的clusterNode结构所记录的节点IP和端口号,向客户端返回MOVED错误,指引客户端转向至正在处理槽 i 的节点
MOVED错误
MOVED错误的格式为:
MOVED <slot> <ip>:<port>
一个集群客户端通常会与集群中的多个节点创建套接字连接,而所谓的节点转向实际上就是还一个套接字来发送命令
如果客户端尚未与想要转向的节点创建套接字连接,那么客户端会先根据MOVED错误提供的IP地址与端口号来连接节点,然后进行转向
集群模式下的redis-cli客户端在接收到MOVED错误时,并不会打印MOVED错误,而是根据MOVED错误自动进行转向
但是如果是单机模式下的redis-cli,MOVED错误会被客户端打印出来
节点数据库的实现
集群节点保存键值对以及键值对过期时间方式同单机服务器完全相同;
在数据库方面的一个区别是,集群节点只能使用0号数据库,而单机服务没有这个限制。
另外,除了将键值对保存在数据库之外,节点还会通过跳跃表保存槽与键之间的关系:
typedef struct clusterState {
...;
zskiplist *slots_to_keys; // 每个节点的分值是一个槽号,每个节点的成员是一个数据库键
...;
} clusterState;
当往数据库中添加或删除键值对时,会更新这个跳跃表
有了这个跳跃表节点就可以很方便的对某个或某些槽的所有数据库键进行批量操作