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属性,是否开启了集群。开启了,就把自己设为一个节点,没有就是单机模式。

redis集群端口开启 redis 集群启动_客户端

  作为节点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信息。此处是三次握手。

redis集群端口开启 redis 集群启动_redis集群端口开启_02

  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个字节

redis集群端口开启 redis 集群启动_redis集群端口开启_03

  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节点发送命令。

    这里存在一个问题?那么如果要发送多个命令,是否会存在多次跳转的问题,那么客户端操作就显得很麻烦!

redis集群端口开启 redis 集群启动_redis集群端口开启_04

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信息

redis集群端口开启 redis 集群启动_redis集群端口开启_05

  client根据moved信息,转向到7001节点,重新发送set msg "happy new year!"命令。

  通常一个client会和集群中的多个node建立socket连接,所以转向,就是换一个socket重新发送信息。当client收到moved错误信息的时候,并不会打印出来,而是提示转向信息:

redis集群端口开启 redis 集群启动_客户端_06

4.数据库实现:集群中的数据库仍然和单机版本模式相同,使用dict来保存的,但是集群只能使用0号数据库。而且clusterState有一个成员:zskiplist *slots_to_keys 来映射槽号--->键。跳跃表中的score都是一个槽号:比如book键的 槽号就是1337。

redis集群端口开启 redis 集群启动_redis_07

 

四.重新分片

  将已经分配的槽指定给其他的节点,它可以在线进行,比如新增一个节点,那么就需要从槽点里面分一部分出来给新节点。

重新分片实现原理:

  redis集群的重新分片槽指是交给redis集群管理软件redis-trib负责执行的,redis提供了所有关于重新分片的命令,redis-trib通过发送命令实现重新分片。

  

redis集群端口开启 redis 集群启动_redis集群端口开启_08

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错误,处理该错误的方法任然是重写指向。

redis集群端口开启 redis 集群启动_redis集群端口开启_09

六 复制与故障转移

    分为主节点和从节点,主节点用来处理槽,从节点备份,在主下线的时候顶上。

七 消息

节点直接的通信,有五种类型的信息,消息头由clusterMsg结构表示

 

 

 

参考:《redis设计与实现》读书笔记