Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能
一个Redis集群通常由多个节点(node,一个Redis服务器)组成。刚开始每个节点都是一个集群,需要手动指定将其他节点添加到当前集群。
CLUSTER MEET <ip> <port>
向一个节点发送以上命令,让目标节点和命令中指定节点进行握手,握手成功则将指定节点添加到当前集群
Redis服务器在启动时,根据cluster-enabled配置选项是否为yes决定是否开启服务器的集群模式
每个节点使用一个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中的套接字和缓冲区用于连接其他节点。
每个节点中还保存了一个clusterState结构,记录了当前节点的视角下,集群目前所处的状态
typedef struct clusterState{ //指向当前节点的指针 clusterNode *myself; //集群当前的配置纪元,用于实现故障转移 uint64_t currentEpoch; //集群当前的状态,在线或下线 int state; //集群中至少处理着一个槽的节点数量 int size; //集群节点名单(包括myself节点) //字典的键位节点的名字,字典的值位节点对应的clusterNode结构 dict *nodes; }clusterState;
节点A会将节点B的信息通过Gossip协议传播给集群中的其他节点,让其他节点也跟节点B进行握手
Redis集群通过分片的方式保存数据库中的键值对,集群的整个数据库被分成16384个槽(slot),数据库中的每个键属于其中一个槽,集群中的每个节点可以处理0个或至多16384个槽。当数据库中的每个槽都有节点在处理,集群处于上线状态;反之处于下线状态。
CLUSTER ADDSLOTS <slot> [slot ...]
通过向节点发送CLUSTER ADDSLOTS命令,将一个或多个槽指派给节点负责。
clusterNode结构的solts属性和numslot属性记录了节点负责处理哪些槽:
struct clusterNode{ //长度2048个字节,包含16348个二进制位 //索引i记录了第i个处理槽由当前节点处理 unsigned char slots[16384/8]; int numslots; };
如图所示,代表当前节点处理槽0至槽7。检查和设置节点的处理槽,复杂度均为O(1)。numslots属性统计了节点负责处理的槽的数量。
每个节点会将自己的slots数组通过消息发送给集群中的其他节点,当节点收到其他节点发送的slots数组时,会在自己的clusterState.nodes字典中查找B节点对应的clusterNode结构,更新或保存其中的slots数组
clusterState结构中的slots数组记录了集群中所有16384个槽的指派信息:
typedef struct clusterState{ //... clusterNode *slots[16384]; //... }clusterState;
slots每项都是一个指向clusterNode结构的指针,如果指向NULL,则表示尚未分配。
在clusterState结构中保存slots数组的目的,是为了减少查找某个槽的分配信息的遍历操作,否则需要遍历nodes中保存的每个节点的slots数组,复杂度是O(N)。
在clusterNode结构中保存slots数组的目的,是为了方便发送某个节点负责的槽信息,不需要遍历
收到命令后,在clusterState.slots中查找命令中分配的槽是否已被使用,只要有一个槽已被使用,拒绝执行命令,向客户端发送错误。否则先将clusterState.slots对应的槽位分配完毕,再分配clusterNode.slot的中的槽位。分配完后,节点发送消息告知集群中的其他节点,自己目前负责处理的槽。
当全部槽分配完毕,集群上线,开始处理命令。
客户端连接上节点,向节点发送有关键操作的命令,节点会检查键所属的槽是否由自己负责,是则直接执行命令,否则返回客户端MOVED错误,指引客户端转向对应节点,之后由客户端重新发送命令。
def slot_number(key): return CRC16(key)&16383
CRC16(key)计算键key的CRC-16校验和,而&16383语句则用于计算处一个介于0和16383之间的整数作为键key的槽号
检查clusterState.slots数组对应槽关联的节点,如果不是当前节点,则访问对应节点的clusterNode结构,根据结构中记录的IP地址和端口号向客户端返回MOVED错误
MOVED <slot> <ip>:<port>
客户端根据MOVED错误中记录的地址和端口号转向负责的节点,并向该节点重新发送想要执行的命令。转换节点的操作实际是转换发送的套接字,倘若还没有建立连接,则先建立连接。集群模式的客户端不会打印出MOVED错误,而是自动转向。
节点只能使用0号数据库,保存键值对和键值对的过期方式跟单机无差别。此外,节点在clusterState结构中的slots_to_keys跳跃表保存槽和键之间的关系:
typedef struct clusterState{ //... zskiplist *slots_to_keys; //... }clusterState;
slots_to_keys跳跃表的每个节点的分值score都是一个槽号,每个节点的成员object是一个数据库键:
通过维护这样的跳跃表,可以方便的获取一些统计信息,譬如某个槽下的所有键,只需要遍历整个跳跃表即可
Redis集群的重新分片可以将任意数量已经指派给某个节点的槽改为指向另一个节点,并且相关槽所属的键值对也会从源节点移动到目标节点
重新分片操作可以在线进行,源节点和目标节点都可以继续处理命令请求
在重新分片的过程中,当客户端向源节点发送键命令时,该键可能已经转移到目标节点,这时源节点会返回一个错误,客户端会转而向目标节点请求
127.0.0.1:7002>GET "love"
(error)ASK 16198 127.0.0.1:7003
跟MOVED错误类似,在集群模式下,收到ASK错误的客户端也不对打印,而是自动根据提供的目标地址和端口进行转向
clusterState结构的importing_stots_from数组记录了当前节点正在从其他节点导入的槽:
typedef struct clusterState{ //... clusterNode *importing_slots_from[16384]; //... }clusterState;
如果importing_slot_from[i]的值不为NULL,而是指向一个clusterNode结构,表示正在从clusterNode导入槽i
对集群重新分片时,向目标节点发送命令:
CLUSTER SETSLOT <i> IMPORTING <source_id>
目标节点将importing_slots_from[i]设置为source_id代表的clusterNode结构
clusterState结构的migrating_slots_to数组记录了当前节点正在迁移至其他节点的槽,第i位若指向一个clusterNode结构,则代表该槽正迁移至该clusterNode
typedef struct clusterState{ //... clusterNode *migrating_slots_to[16384]; //... }clusterState;
在对集群进行重新分片时,向源节点发送命令:
CLUSTER SETSLOT <i> MIGRATING <target_id>
源节点收到命令后,将其migrating_slots_to[i]设置位target_id代表的clusterNode
一个节点收到键key命令,如果当前key对应的槽恰好分配在自己名下(17.3.2),在数据库中查找该键,如果没有找到,则寻找migrating_slots_to[i]位置是否指向了某个clusterNode,向客户端返回ASK错误。
客户端收到之后,转向新的节点,先发送一个ASKING命令,之后再重新发送原本想要执行的命令
ASKING命令会打开发送该命令的客户端的REDIS_ASKING标识(标识自己下一个请求是经过其他节点指引过来的,目标节点需要执行)。
如果客户端向目标节点发送键命令,而键对应的槽为 i
遇到MOVED错误后,代表所有关于槽 i 的指令,都已经转向了新的节点,客户端后续的所有关于槽 i 的命令都会转向新节点
遇到ASK错误,只会在本次请求中,转向新的节点,而后续仍然请求源节点。产生ASK错误,表示正处于重新分片的中间状态,槽 i 的键并非完全迁移完毕
Redis集群中的节点分为主节点(master)和从节点(slave),其中主节点负责处理槽,从节点复制某个主节点,在主节点下线时,代替主节点。负责主节点负责的槽,而其余从节点也会转向复制新的主节点。
向一个节点发送命令,让该节点成为命令中指定的节点的从节点,并开始对主节点进行复制
CLUSTER REPLICATE <node_id>
struct clusterNode{ //如果这是一个从节点,那么指向主节点 strut clusterNode *slaveof; //.... };
每个节点在clusterNode结构的slaves属性和numslaves属性中记录这个主节点的从节点名单:
struct clusterNode{ //正在复制这个主节点的从节点数量 int numslaves; //一个数组 //每个数组项指向一个正在复制这个主节点的从节点的clusterNode结构 struct clusterNode **slaves; }
集群中每个节点都会定期向集群中其他节点发送PING消息,以期在规定时间内收到回复,来检测对方是否在线。当半数以上的主节点都将主节点x标记为疑似下线,主节点x将被标记为已下线(FAIL),将主节点标记为下线的节点会发送一条广播,所有收到消息的节点都会立即将x标记为已下线
当某个从节点检测到主节点进入已下线状态,进入故障转移:
跟领头sentinel选举类似,但只有主节点可以投票。
集群中的节点通过发送和接收消息进行通信,消息主要有:
原文:https://www.cnblogs.com/walker993/p/14472602.html