一、节点
一个Redis集群由多个节点组成,刚开始每个节点都是相互独立的,要组建集群需要将各个独立的节点连接起来
连接各个节点的工作可以使用CLUSTER MEET命令来完成:
CLUSTER MEET <ip> <port>
1.1 启动节点
一个节点就是运行在集群模式下的Redis服务器,Redis服务器在启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式
节点(运行在集群模式下的Redis服务器)会继续使用所在单机模式中使用的服务器组件:
- 节点会继续使用文件处理器来处理命令请求和返回命令回复
- 节点会继续使用RDB持久化模块和AOF持久化模块
1.2 集群数据结构
- clusterState:保存一个节点当前状态,包括节点的创建时间、名字、配置纪元、IP、端口等
- clusterNode:记录当前节点的视角下,集群目前所处的状态
每个节点都会使用一个clusterState结构记录自己的状态,并为集群中所有其他节点(包括主节点和从节点)都创建相应的clusterNode结构
clusterNode结构中由一个clusterLink结构,保存了连接节点所需要的有关信息,包括套接字描述符,输入缓冲区和输出缓冲区
1.3 CLUSTER MEET命令实现
客户端通过向节点A发送CLUSTER MEET命令,可以让接受命令的节点A将另一个节点B添加到节点A当前所在的集群中
- 节点A为节点B创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里
- 节点A根据CLUSTER MEET命令给定的IP和端口,向节点B发送一条MEET消息
- 节点B收到节点A的MEET消息,为节点A创建clusterNode,并添加到自己的clusterState.nodes字典里
- 节点B向节点A返回一条PONG消息
- 节点A收到节点B的PONG消息,知道节点B收到了自己的MEET消息
- 节点A向节点B返回一条PING消息
- 节点B收到节点A的PING消息,知道节点A收到了自己的PONG,握手完成
二、槽指派
Redis集群通过分片的方式来保存数据库中的键值对,集群的整个数据库被分为16384 个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽,一个槽可能存多个键的信息。
当数据库中的16384个槽都有节点在处理时,集群处于上线状态(ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail)。
通过向节点发送CLUSTER ADDSLOTS命令,将一个或多个槽指派给节点负责
CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000
2.1 记录节点的槽指派
clusterNode的slots属性和numslot属性记录了节点负责处理哪些槽
- slots:二进制位数组,包含16384个二进制位,索引 i 上的二进制位为 1 代表该节点负责处理槽 i ,0 代表该节点不负责处理槽 i
- 记录节点负责处理槽的数量,即 slots 数组中值为1的二进制位的数量
2.2 传播节点的槽指派信息
一个节点除了将自己负责的槽记录在 clusterNode 的 slots 和 numslots ,还会将 slots 数组发送给集群的其他节点。其他节点收到信息,会更新或保存发送消息的节点clusterNode结构的 slots 数组。因此,集群每个节点都知道16384个槽分别被指派给了集群的哪些节点。
2.3 记录集群所有槽的指派信息
typedef struct clusterState{
//....
clusterNode *slots[16384];
//....
}
clusterState 结构中的 slots 数组记录了16384个槽的指派信息
- slots[i] 指针指向NULL,槽 i 未指派给任何节点
- slots[i] 指向一个 clusterNode 结构,表示槽 i 指派给了该 clusterNode 结构代表的节点
clusterState.slots作用?(便于快速知道某个槽的指派信息)
如果节点只使用 clusterNode.slots 数组来记录槽的指派信息,那么为了知道槽 i 是否已经被指派,或者槽 i 被指派给了哪个节点,程序需要遍历 clusterState. nodes 字典中的所有clusterNode结构,检查这些结构的 slots 数组,直到找到负责处理槽i的节点为止,这个过程的复杂度为O(N),其中N为 clusterState.nodes 字典保存的 clusterNode 结构的数量。
而通过将所有槽的指派信息保存在 clusterState.slots 数组里面,程序要检查槽 i 是否已经被指派,又或者取得负责处理槽 i 的节点,只需要访问clusterState.slots[i]的值即可,这个操作的复杂度仅为O(1)。
有了clusterState.slots,还需要clusterNode.slots吗?(便于知道某个节点的槽指派信息)
因为当程序需要将某个节点的槽指派信息通过消息发送给其他节点时,程序只需要将相应节点的clusterNode.slots数组整个发送出去就可以了。
另一方面,如果Redis不使用clusterNode.slots数组,而单独使用clusterState.slots数组的话,那么每次要将节点A的槽指派信息传播给其他节点时,程序必须先遍历整个clusterState.slots数组,记录节点A负责处理哪些槽,然后才能发送节点A的槽指派信息,这比直接发送clusterNode.slots数组要麻烦和低效得多。
clusterState.slots数组记录了集群中所有槽的指派信息,而clusterNode.slots数组只记录了clusterNode结构所代表的节点的槽指派信息,这是两个slots数组的关键区别所在。
三、集群中执行命令
当客户端向节点发送与数据库有关的命令时,接受命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查槽是否指派给了自己
3.1 计算键属于哪个槽
CRC(key) & 16383
3.2 判断槽是否由当前节点处理
上一步计算出了键所在的槽 i ,查看 clusterState.slot[i] 是否等于 clusterState.myself
3.3 MOVED错误
集群模式的客户端(使用 redis-cli -c 命令连接节点) ,接收到MOVED错误时,不会打印错误,而是根据MOVED错误自动进行节点跳转
3.4 节点数据库的实现
集群节点保存键值对以及键值过期时间的方式与单机Redis服务器方式相同,但节点只能使用0号数据库
节点还会用clusterState结构中的跳跃表(slots_to_keys)来保存槽与键的关系,跳跃表中每个节点的 socre 都是一个槽号,每个节点的成员 member 都是一个数据键
通过跳跃表记录各个数据库键所属的槽,可以很方便对某个或某些槽的所有键进行批量操作
四、重新分片
重新分片:将任意数量已经指派给某个节点的(源节点)的槽改为指派给另一个节点(目标节点)
重新分片可以在线操作,不需要集群下线
redis-trib 通过向源节点和目标节点发送命令进行重新分片操作:
单个槽重新分片:
多个槽重新分片 :
五、ASK错误
与MOVED错误类似,集群模式的客户端接受到ASK不会打印错误,而是自动根据错误信息进行转向操作。
ASK错误:接到收客户端的键,计算键对应的槽,槽指派是当前节点,那么在节点数据库找该键
- 如果没找到并且对应的槽没有在迁移,返回键不存在
- 如果没找到并且对应的槽在迁移,返回ASK错误(会自动转向目标节点,同时携带ASKING标识,客户端不打印错误)
对于正在导入槽的节点处理命令过程:
六、复制与故障转移
Redis集群中的节点分为主节点和从节点,主节点复制处理槽,而从节点用于复制某个主节点,当主节点下线时,代替下线的主节点处理命令请求。原来的主节点重新上线,会成为新的主节点的从节点。、
6.1 设置从节点
CLUSTER REPLICATE node_id
接受客户端命令的节点成为node_id节点的从节点,并开始复制主节点,同时改变clusterState信息:将myself.slaveof指针指向这个结构,记录正在复制的主节点
一个节点成为从节点,并开始复制某个主节点(如7000)的消息会通过消息发送集群的其他节点,其他节点更新自己的clusterState中该主节点结构的slaves等属性,如下图,clusterNode是主节点对应结构
6.2 故障检测
集群中的每个节点都会定期地向集群中的其他节点发送PING消息,检测对方是否在线
6.3 故障转移
从节点发现自己正在复制的主节点下线,从节点开始对下线主节点进行故障转移
- 下线主节点的所有从节点会有一个被选中
- 被选中的从节点指向SLAVEOF no one 命令,成为新的主节点
- 新的主节点撤销下线主节点的槽指派,将这些槽指派给自己
- 新的主节点向集群广播一条PONG,让集群其他节点立即知道这个节点变为主节点
- 新的主节点开始接受和负责处理槽有关命令请求,故障转移完成