Redis集群是Redis提供的分布式数据库方案,集群通过切片来实现数据共享。
一.节点
最开始各个节点都是独立的,要进行连接,使用 cluster meet <ip> <port> 命令。
cluster nodes 查看集群中的Node信息
例如: A,B,C三个节点,A,向B发送cluster meet Bip,Bport,AB集群成立,A再向C发送命令,ABC集群成立。
1.启动节点:判断配置文件中cluster-enabled属性,是否开启了集群。开启了,就把自己设为一个节点,没有就是单机模式。
作为节点Node启动时,会按照单机模式的配置组件继续使用,比如redisServer和redisClient结构继续保存信息,但是与cluster有关的信息,保存在了clusterNode,clusterLink,clusterState结构里面。
2.集群数据结构:
clusterNode,保存一个点的当前状态,创建时间,名字,ip,端口号等等。它不仅记录集群中其他节点,还记录了自己。里面存在一个clusterlink结构,它和redisClient一个概念,是连接状态。还有一个cluseterState结构,保存了当前节点视角任务的集群状态。
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;
...
};
typedef struct clusterLink{
//连接的创建时间
mstime_t ctime;
//tcp套接字描述符
int fd;
//输出缓冲区,保存着等待发送给其他节点的信息 message
sds sndbuf;
//输入缓冲区,保存着从其他节点接收到的信息
sds rcvbuf;
//与这个连接相关联的节点,如果没有的化就为Null
struct clusterNode *node;
}clusterLink;
每个节点都保存着一个clusterState结构,该结构记录了当前节点的视觉下,集群目前所处的状态:
typedef struct clusterState{
//指向当前节点的指针
clusterNode *myself;
//集群当前的配置纪元,用于实现故障转移
uint64_t currentEpoch;
//集群当前的状态: 在线or 下线
int state;
//集群中至少 处理着一个槽的节点 的数量(节点的数量)
int size;
//集群节点的名单,包括自己,字典的key是node的name,value对应clusterNode结构
dict *nodes;
...
}clusterState;
3.cluster meet命令的实现
A命令发送,B接收,握手,A创建一个clusterNode,添加到clusterState的nodes里面,然后发送一条meet信息,B收到,B创建一个clusterNode结构,添加到自己的ClusterState.nodes字典中。然后B向A发送pong,A收到B返回的pong信息,就B能收到A的信息,A再返回一条ping信息。此处是三次握手。
A与B连接之后,A节点把B的信息传播给集群中其他节点,进行连接通信。
二.槽指派
集群通过分片来处理数据库中键值对,将整个数据库分为16384个槽slot,每个键都是其中的一个,当数据库中的每个槽都有存在节点处理的时候,集群上线成功,反之失败。节点连接建立,例如A,B,C,下一步就需要给这三个节点分配槽点。
使用命令:cluster addslots index index ... index 直接指派该node 需要负责的槽点。
struct clusterNode{
unsigned char slots[16384/8];
int numslots;
...
}
1.记录节点中的槽指派信息
clusterNode中的属性char slots[],二进制位数组,包含16384个二进制位(一个字节占8位),这里就是位图法。为1,表示该节点复制该槽点。存在 16384/8 = 2048个字节
2.传播节点的槽指派信息
node除了将自己负责的槽点,保存在clusterNode结构的slots属性和numslots属性外,还将自己负责的槽点发送给集群中其他节点。
接收方收到信息后,在本node中存在clusterState的nodes属性,里面保存了各个node,接收方将收到的信息,保存到对应node的clusterNode中的slots[]中!
3.记录集群所有槽的指派信息
当分配完成,收到所有other node的solts[]信息之后,clusterState中nodes就记录了集群16384个槽的指派信息,同时clusterState的直接成员 clusterNode *slots[16384]也记录了槽对应的节点信息。每一槽都指向一个node信息。这也做的目的是快速访问,不仅能得到node负责了哪些slots,而且还能快速获得slot被哪个node负责。
4.cluster addslots命令的实现
这个命令接收一个or多个槽位作为参数,收到该命令的server将负责参数给出的slots。
三.在集群中执行命令
现在集群上线成功,可以发送命令了。当客户端发送命令,有一个节点负责接收,然后计算键的位置,如果该slot是属于本node处理的,那么就直接进行处理,如果发现不是本node,那么会通过ClusterState的slots[]属性,找到该slot指向的node,得到该node的ip:port信息,返回给客户端,也就是MOVED错误,客户端获得该错误之后,根据moved信息指向正确的node节点发送命令。
这里存在一个问题?那么如果要发送多个命令,是否会存在多次跳转的问题,那么客户端操作就显得很麻烦!
1.计算slot:根据key 的crc-16效验和 & 16383获得槽值,使用CLUSTER KEYSLOT "xxx" 可以求得xxx的槽值。
2.判断slot是否属于本Node管理:通过clusterState.slots[]属性获得指向的node值,与clusterState.myself值进行比较,是否是同一个,如果是同一个,那么就属于本节点
3.moved错误:当节点发现键所在的槽不是自己负责的,那么node会返回给client一个moved错误,并且附带正确Node的ip:port信息
client根据moved信息,转向到7001节点,重新发送set msg "happy new year!"命令。
通常一个client会和集群中的多个node建立socket连接,所以转向,就是换一个socket重新发送信息。当client收到moved错误信息的时候,并不会打印出来,而是提示转向信息:
4.数据库实现:集群中的数据库仍然和单机版本模式相同,使用dict来保存的,但是集群只能使用0号数据库。而且clusterState有一个成员:zskiplist *slots_to_keys 来映射槽号--->键。跳跃表中的score都是一个槽号:比如book键的 槽号就是1337。
四.重新分片
将已经分配的槽指定给其他的节点,它可以在线进行,比如新增一个节点,那么就需要从槽点里面分一部分出来给新节点。
重新分片实现原理:
redis集群的重新分片槽指是交给redis集群管理软件redis-trib负责执行的,redis提供了所有关于重新分片的命令,redis-trib通过发送命令实现重新分片。
1.发送命令:获得最多count个属于slot键值对的 键名
2.对于属于slot键的 key name,redis-trib都向源节点发送一个migrate 命令,将被选中的键 从源节点,迁移到目标节点。
3.转移之后,redis-trib向集群中任意节点发送 新的指派信息,最终集群都直到slot已经指派给了新的节点。
分片的重点是把源node 管理的slot转交给新的node,此处的slot不再是简单的一个属于值的重写,而是包括了数据库的具体信息。
五 ASK错误
当发现槽迁移的时候,槽的一部分键转移到了新的node,还有一部分任然存在于源node中,这个时候针对该slot的命令发来了,那么如果该命令涉及到的键,还存在于源node中,还可以正常处理,如果键不存在,(已经迁移到新的node中),那么就会返回ask错误,处理该错误的方法任然是重写指向。
六 复制与故障转移
分为主节点和从节点,主节点用来处理槽,从节点备份,在主下线的时候顶上。
七 消息
节点直接的通信,有五种类型的信息,消息头由clusterMsg结构表示
参考:《redis设计与实现》读书笔记