Redis源码解析:26集群(二)键的分配与迁移

本文深入探讨了Redis集群中键的分配和迁移过程。集群通过哈希函数将键映射到16384个槽位,每个节点负责一部分。键的分配和迁移涉及到数据结构如slots数组、migrating_slots_to和importing_slots_from等。键的迁移分为手动分配和节点间迁移两个阶段,通过`CLUSTER`命令实现。集群在槽位迁移时,使用`redis-trib.rb`辅助脚本和`MIGRATE`命令,确保数据一致性。整个过程中,节点会通过心跳包交换槽位信息,保证集群状态的同步。
摘要由CSDN通过智能技术生成

         Redis集群通过分片的方式来保存数据库中的键值对:一个集群中,每个键都通过哈希函数映射到一个槽位,整个集群共分16384个槽位,集群中每个主节点负责其中的一部分槽位。

         当数据库中的16384个槽位都有节点在处理时,集群处于上线状态;相反,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态。

         所谓键的分配,实际上就是指槽位在集群节点中的分配;所谓键的迁移,实际上指槽位在集群节点间的迁移。

 

一:数据结构   

         在集群最主要的数据结构,记录集群状态的clusterState结构体中,与槽位相关的属性有:

clusterNode *slots[16384];
clusterNode *migrating_slots_to[16384];
clusterNode *importing_slots_from[16384];
zskiplist *slots_to_keys;

         slots数组记录了16384个槽位,分别由哪个集群节点负责:比如server->cluster.slots[0] = node,这说明0号槽位由node节点负责;

         migrating_slots_to数组记录了16384个槽位中,当前节点所负责的槽位正在迁出到哪个节点。比如server.cluster->migrating_slots_to[0] = node,这说明当前节点负责的0号槽位,正在迁出到node节点;

         importing_slots_from数组记录了16384个槽位中,当前节点正在从哪个节点将某个槽位迁入到本节点中;比如server.cluster->importing_slots_from[0] = node,这说明当前节点正在从node节点处迁入0号槽位;

         通过以上这些属性,可以快速得到某个槽位由哪个节点负责,以及该槽位正在迁出或迁入到哪个节点。

         slots_to_keys是个跳跃表,该跳跃表中,以槽位号为分数进行排序。每个跳跃表节点保存了槽位号(分数),以及该槽位上的某个key。通过该跳跃表,可以快速得到当前节点所负责的每一个槽位中,都有哪些key。

 

         在表示集群节点的clusterNode结构体中,与槽位相关的属性有:

unsigned char slots[16384/8];
int numslots;

         slots记录了节点负责处理哪些槽位。它是个位数组,其中每一个比特位表示一个槽位号,如果该比特位置为1,则说明该槽位由该节点负责;

         numslots表示该节点负责的槽位总数;

         通过以上这些属性,可以快速得到某个节点负责哪些槽位。

 

二:分配槽位

         在集群刚建立时,需要手动为每个集群主节点分配其负责的槽位。这主要是通过向节点发送”CLUSTER  ADDSLOTS”命令实现的。该命令的格式是:”CLUSTER  ADDSLOTS  <slot>  [slot]  ...”。

         “CLUSTER”命令的处理函数是clusterCommand。在该函数中,处理” CLUSTER ADDSLOTS”部分的代码是:

else if ((!strcasecmp(c->argv[1]->ptr,"addslots") ||
               !strcasecmp(c->argv[1]->ptr,"delslots")) && c->argc >= 3)
    {
        /* CLUSTER ADDSLOTS <slot> [slot] ... */
        /* CLUSTER DELSLOTS <slot> [slot] ... */
        int j, slot;
        unsigned char *slots = zmalloc(REDIS_CLUSTER_SLOTS);
        int del = !strcasecmp(c->argv[1]->ptr,"delslots");

        memset(slots,0,REDIS_CLUSTER_SLOTS);
        /* Check that all the arguments are parseable and that all the
         * slots are not already busy. */
        for (j = 2; j < c->argc; j++) {
            if ((slot = getSlotOrReply(c,c->argv[j])) == -1) {
                zfree(slots);
                return;
            }
            if (del && server.cluster->slots[slot] == NULL) {
                addReplyErrorFormat(c,"Slot %d is already unassigned", slot);
                zfree(slots);
                return;
            } else if (!del && server.cluster->slots[slot]) {
                addReplyErrorFormat(c,"Slot %d is already busy", slot);
                zfree(slots);
                return;
            }
            if (slots[slot]++ == 1) {
                addReplyErrorFormat(c,"Slot %d specified multiple times",
                    (int)slot);
                zfree(slots);
                return;
            }
        }
        for (j = 0; j < REDIS_CLUSTER_SLOTS; j++) {
            if (slots[j]) {
                int retval;

                /* If this slot was set as importing we can clear this
                 * state as now we are the real owner of the slot. */
                if (server.cluster->importing_slots_from[j])
                    server.cluster->importing_slots_from[j] = NULL;

                retval = del ? clusterDelSlot(j) :
                               clusterAddSlot(myself,j);
                redisAssertWithInfo(c,NULL,retval == REDIS_OK);
            }
        }
        zfree(slots);
        clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|CLUSTER_TODO_SAVE_CONFIG);
        addReply(c,shared.ok);
    } 

         这里” CLUSTER  ADDSLOTS”和” CLUSTER  DELSLOTS”命令,采用类似的代码进行处理。ADDSLOTS和DELSLOTS,分别用于将槽位分配给节点,以及将槽位从节点中删除。ADDSLOTS命令常用于新建集群时,给每个主节点分配槽位;DELSLOTS常用于手动修改集群配置,或者用于DEBUG操作,实际中很少用到。

 

         在代码中,首先,依次检查命令参数中的槽位号:如果是DELSLOTS操作,但是数组server.cluster->slots中,记录负责该槽位号的节点为NULL,则反馈给客户端"unassigned"错误;如果是ADDSLOTS操作,但是数组server.cluster->slots中,记录已经有节点负责该槽位号了,则反馈给客户端"busy"错误;然后将参数中的槽位号记录到数组slots中,如果slots中该槽位已经设置过了,说明发来的命令中,该槽位号出现了多次,因此反馈给客户端"multiple times"错误;

         然后,依次轮训slots中记录的每一个槽位号进行处理:首先如果该槽位号在数组server.cluster->importing_slots_from中不为NULL,则将其置为NULL,因为该槽位已经由本节点负责了;然后根据是ADDSLOTS,还是DELSLOTS操作,调用clusterAddSlot或clusterDelSlot处理;

         最后,反馈给客户端"OK";

 

         因此,clusterAddSlot才是是实际用于分配槽位的函数,该函数的实现如下:

int clusterAddSlot(clusterNode *n, int slot) {
    if (server.cluster->slots[slot]) return REDIS_ERR;
    clusterNodeSetSlotBit(n,slot);
    server.cluster->slots[slot] = n;
    return REDIS_OK;
}

         该函数的实现很简单,就是要设置位数组n->slots中的相应位,以及server.cluster->slots[slot]。

         首先,根据server.cluster->slots[slot]的值,判断该槽位是否已经分配给其他节点了,若是,则直接返回REDIS_ERR;

         然后调用clusterNodeSetSlotBit,在位数组n->slots中设置相应的位;

         最后,将server.cluster->slots[slot]置为n;

         以上,就相当于把slot槽位分配给了节点n。

 

         顺便看一下删除槽位的函数clusterDelSlot的实现:

int clusterDelSlot(int slot) {
    clusterNode *n = server.cluster->slots[slot];

    if (!n) return REDIS_ERR;
    redisAssert(clusterNodeClearSlotBit(n,slot) == 1);
    server.cluster->slots[slot] = NULL;
    return REDIS_OK;
}

         该函数清除slot槽位的信息,将其置为未分配的。成功返回REDIS_OK;否则若该槽位已经被置为未分配的了,则返回REDIS_ERR;

         该函数的实现很简单,就是清除位数组n->slots中的相应位,以及将server.cluster->slots[slot]置为NULL。

         首先从server.cluster->slots[slot]取得当前负责该槽位的节点n;如果n为NULL,则返回REDIS_ERR;

         然后调用clusterNodeClearSlotBit,将该槽位从位数组n->slots中清除;

         最后置server.cluster->slots[slot]为NULL;

         以上,就相当于把slot槽位置为未分配状态了。


         集群节点在发送心跳包时,会附带自己当前记录的槽位信息(clusterNode结构中的位数组slots),这样,最终集群中的每个节点都会知道所有槽位的分配情况。


三:槽位迁移(重新分片)

         在集群稳定一段时间之后,如果有新的集群节点加入,或者某个集群节点下线了。此时就涉及到将某个节点上的槽位迁移到另一个节点上的问题。

         该过程也是需要手动完成的,Redis提供了辅助脚本redis-trib.rb,以”reshard”参数调用该脚本就可以实现重新分片的操作。但是本质上,该脚本就是通过向迁入节点和迁出节点发送一些命令实现的。

         槽位迁移的步骤是:

 

1:向迁入节点发送” CLUSTER  SETSLOT  <slot>  IMPORTING  <node>”命令

         其中<slot>是要迁入的槽位号,<node>是当前负责该槽位的节点。在函数clusterCommand中,处理该命令的代码如下:

    else if (!strcasecmp(c->argv[1]->ptr,"setslot") && c->argc >= 4) {
        /* SETSLOT 10 MIGRATING <node ID> */
        /* SETSLOT 10 IMPORTING <node ID> */
        /* SETSLOT 10 STABLE */
        /* SETSLOT 10 NODE <node ID> */
        int slot;
        clusterNode *n;

        if ((slot = getSlotOrReply(c,c->argv[2])) == -1) return;

        if (!strcasecmp(c->argv[3]->ptr,"migrating") && c->argc == 5) {
            ...
        } else if (!strcasecmp(c->argv[3]->ptr,"importing") && c->argc == 5) {
            if (server.cluster->slots[slot] == myself) {
                addReplyErrorFormat(c,
                    "I'm already the owner of hash slot %u",slot);
                return;
            }
            if ((n = clusterLookupNode(c->argv[4]->ptr)) == NULL) {
                addReplyErrorFormat(c,"I don't know about node %s",
                    (char*)c->argv[3]->ptr);
                return;
            }
            server.cluster->importing_slots_from[slot] = n;
        } else if (!strcasecmp(c->argv[3]->ptr,"stable") && c->argc == 4) {
            ...
        } else if (!strcasecmp(c->argv[3]->ptr,"node") && c->argc == 5) {
            ...
        } else {
            addReplyError(c,
                "Invalid CLUSTER SETSLOT action or number of arguments");
            return;
        }
        clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|CLUSTER_TODO_UPDATE_STATE);
        addReply(c,shared.ok);
    }

         针对"CLUSTER SETSLOT"命令,首先从命令参数中取得槽位号slot,如果解析错误,则回复给客户端错误信息,然后直接返回;

         如果收到的是" CLUSTER  SETSLOT <SLOT>  IMPORTING  <node>"命令,说明本节点需要迁入槽位。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值