首页 > 代码库 > redis源码学习(集群)

redis源码学习(集群)

集群是一种分布式的思想,把数据存储到各个节点上去提供服务。分布式一个重要的步骤,就是分片。那redis集群是怎么分片的呢,以及集群服务的稳定性和可靠性怎么保证。下面就来剖析下。

1. 集群是怎么创建的,分片怎么设计的?

分片原理:
redis的核心数据是个hash table,分片是按照key值来划分,再reds里面叫槽(slot),集群中的各个节点都会分一些槽,用于存储它们的数据。
客户端在请求集群的服务时候,通过查询的key,算出一个hash值,再使用hash值%槽数得到第几个槽,再通过对应槽找对应节点,进行查询或者更新操作。

基本数据结构:
集群节点:
// 节点状态
struct clusterNode {

    // 创建节点的时间
    mstime_t ctime; /* Node object creation time. */

    // 节点的名字,由 40 个十六进制字符组成
    // 例如 68eef66df23420a5862208ef5b1a7005b806f2ff
    char name[REDIS_CLUSTER_NAMELEN]; /* Node name, hex string, sha1-size */

    // 节点标识
    // 使用各种不同的标识值记录节点的角色(比如主节点或者从节点),
    // 以及节点目前所处的状态(比如在线或者下线)。
    int flags;      /* REDIS_NODE_... */

    // 节点当前的配置纪元,用于实现故障转移
    uint64_t configEpoch; /* Last configEpoch observed for this node */

    // 由这个节点负责处理的槽
    // 一共有 REDIS_CLUSTER_SLOTS / 8 个字节长
    // 每个字节的每个位记录了一个槽的保存状态
    // 位的值为 1 表示槽正由本节点处理,值为 0 则表示槽并非本节点处理
    // 比如 slots[0] 的第一个位保存了槽 0 的保存情况
    // slots[0] 的第二个位保存了槽 1 的保存情况,以此类推
    unsigned char slots[REDIS_CLUSTER_SLOTS/8]; /* slots handled by this node */

    // 该节点负责处理的槽数量
    int numslots;   /* Number of slots handled by this node */

    // 如果本节点是主节点,那么用这个属性记录从节点的数量
    int numslaves;  /* Number of slave nodes, if this is a master */

    // 指针数组,指向各个从节点
    struct clusterNode **slaves; /* pointers to slave nodes */

    // 如果这是一个从节点,那么指向主节点
    struct clusterNode *slaveof; /* pointer to the master node */

    // 最后一次发送 PING 命令的时间
    mstime_t ping_sent;      /* Unix time we sent latest ping */

    // 最后一次接收 PONG 回复的时间戳
    mstime_t pong_received;  /* Unix time we received the pong */

    // 最后一次被设置为 FAIL 状态的时间
    mstime_t fail_time;      /* Unix time when FAIL flag was set */

    // 最后一次给某个从节点投票的时间
    mstime_t voted_time;     /* Last time we voted for a slave of this master */

    // 最后一次从这个节点接收到复制偏移量的时间
    mstime_t repl_offset_time;  /* Unix time we received offset for this node */

    // 这个节点的复制偏移量
    long long repl_offset;      /* Last known repl offset for this node. */

    // 节点的 IP 地址
    char ip[REDIS_IP_STR_LEN];  /* Latest known IP address of this node */

    // 节点的端口号
    int port;                   /* Latest known port of this node */

    // 保存连接节点所需的有关信息
    clusterLink *link;          /* TCP/IP link with this node */

    // 一个链表,记录了所有其他节点对该节点的下线报告
    list *fail_reports;         /* List of nodes signaling this as failing */

};
这个是集群的原子结构
每个集群节点都有一个对应集群状态的记录,并且server端属性纪录的是集群的状态。
// 集群状态,每个节点都保存着一个这样的状态,记录了它们眼中的集群的样子。
// 另外,虽然这个结构主要用于记录集群的属性,但是为了节约资源,
// 有些与节点有关的属性,比如 slots_to_keys 、 failover_auth_count 
// 也被放到了这个结构里面。
typedef struct clusterState {

    // 指向当前节点的指针
    clusterNode *myself;  /* This node */

    // 集群当前的配置纪元,用于实现故障转移
    uint64_t currentEpoch;

    // 集群当前的状态:是在线还是下线
    int state;            /* REDIS_CLUSTER_OK, REDIS_CLUSTER_FAIL, ... */

    // 集群中至少处理着一个槽的节点的数量。
    int size;             /* Num of master nodes with at least one slot */

    // 集群节点名单(包括 myself 节点)
    // 字典的键为节点的名字,字典的值为 clusterNode 结构
    dict *nodes;          /* Hash table of name -> clusterNode structures */

    // 节点黑名单,用于 CLUSTER FORGET 命令
    // 防止被 FORGET 的命令重新被添加到集群里面
    // (不过现在似乎没有在使用的样子,已废弃?还是尚未实现?)
    dict *nodes_black_list; /* Nodes we don't re-add for a few seconds. */

    // 记录要从当前节点迁移到目标节点的槽,以及迁移的目标节点
    // migrating_slots_to[i] = NULL 表示槽 i 未被迁移
    // migrating_slots_to[i] = clusterNode_A 表示槽 i 要从本节点迁移至节点 A
    clusterNode *migrating_slots_to[REDIS_CLUSTER_SLOTS];

    // 记录要从源节点迁移到本节点的槽,以及进行迁移的源节点
    // importing_slots_from[i] = NULL 表示槽 i 未进行导入
    // importing_slots_from[i] = clusterNode_A 表示正从节点 A 中导入槽 i
    clusterNode *importing_slots_from[REDIS_CLUSTER_SLOTS];

    // 负责处理各个槽的节点
    // 例如 slots[i] = clusterNode_A 表示槽 i 由节点 A 处理
    clusterNode *slots[REDIS_CLUSTER_SLOTS];

  
} clusterState;
创建过程:
redis.c--->int main--->initServer--->clusterInit函数:
// 初始化集群
void clusterInit(void) {
    int saveconf = 0;

    // 初始化配置
    server.cluster = zmalloc(sizeof(clusterState));
    server.cluster->myself = NULL;
    server.cluster->currentEpoch = 0;
    server.cluster->state = REDIS_CLUSTER_FAIL;
    server.cluster->size = 1;
    server.cluster->todo_before_sleep = 0;
    server.cluster->nodes = dictCreate(&clusterNodesDictType,NULL);
    server.cluster->nodes_black_list =
        dictCreate(&clusterNodesBlackListDictType,NULL);
    server.cluster->failover_auth_time = 0;
    server.cluster->failover_auth_count = 0;
    server.cluster->failover_auth_rank = 0;
    server.cluster->failover_auth_epoch = 0;
    server.cluster->lastVoteEpoch = 0;
    server.cluster->stats_bus_messages_sent = 0;
    server.cluster->stats_bus_messages_received = 0;
    memset(server.cluster->slots,0, sizeof(server.cluster->slots));
    clusterCloseAllSlots();
    //...
}

2. 集群是如何工作的,但一个命令过来,集群是如何执行命令的?

数据库中的所有的槽,比如说是16384个,都进行了指派,集群就会进入上线状态,这时客户端可以像集群中的节点发送数据命令了。
详细的工作原理:
1)一个命令过来,首先得到此命令要查的key,通过key计算它属于哪个槽。
2)先看看当前节点有没有这个槽,如果有直接查询或操作此节点,并返回消息给客户端。
3)如果此节点没有,那么节点会向客户端返回一个MOVED错误,指引客户端转向至正确的节点,并且再次发送命令,并返回结果。

3. 故障转移

    集群中的节点可以设置为主从模式,这样实现了重要数据的备份,单一个节点发生故障的时候,可以把此节点下线,使用它的备份节点(从节点)继续工作,不影响集群的操作。


4. 为什么要用集群

集群的好处有两点,
1)能存储更大的数据量,redis是内存数据库,内存的资源有限,使用集群可以达到大数据量的存储。
2)效率更高,当然这个是对于一定规模的数据来说。数据量小的话,也要分库,中间调度的损耗也挺大的。分库集群,相当于多点处理,能够并发进行,效率当然高多了。

5. 为啥mysql没法实现集群,只能使用分表,这种复杂的处理逻辑?

这个更数据存储结构有关系,redis是hash table结构,而mysql底层结构是B+树,hash表好拆分,B+树不好拆。

mysql分表逻辑就比较复杂了,有垂直拆分和水平拆分,垂直简单点,水平复杂些,但是所有的拆分,都涉及到调用的修改。如果前端直接调用的话,那改动量就大了。


redis源码学习(集群)