Redis6.0.6源码阅读——基础数据结构(rax基数树)【下】

414 阅读8分钟

前言

上篇文章介绍了rax树的插入操作原理,这篇文章看看删除代码。插入涉及到了压缩节点的分裂,那么删除则是会导致节点的合并。

正文

在看源码之前在脑海中试想以下删除流程,首先删除的条件是得完全匹配字符串,并且匹配只有两种情况:要么完全匹配一个压缩节点,要么匹配一个普通节点。 看一下源码是怎么处理的

int raxRemove(rax *rax, unsigned char *s, size_t len, void **old) {
    raxNode *h;
    raxStack ts;

    debugf("### Delete: %.*s\n", (int)len, s);
    //用一个栈来记录访问过的节点
    raxStackInit(&ts);
    int splitpos = 0;
    size_t i = raxLowWalk(rax,s,len,&h,NULL,&splitpos,&ts);
    //表示没有匹配到节点
    if (i != len || (h->iscompr && splitpos != 0) || !h->iskey) {
        raxStackFree(&ts);
        return 0;
    }
    if (old) *old = raxGetData(h);
    h->iskey = 0;
    rax->numele--;

还是使用了raxLowWalk代码来匹配节点并且获取信息,这里比Insert多了一步就是创建了一个Stack,将查找过程中访问节点给记录下来

if (ts) raxStackPush(ts,h);

raxLowWalk中记录栈的代码

能够达成删除条件表示这个节点必须是以前Insert过的真实节点,具有以下三个条件:

  • 必须是一个key
  • 如果是压缩节点,那么匹配到分割位置必须是0
  • 字符串得被完全匹配

然后继续执行

int trycompress = 0;

    if (h->size == 0) {
        debugf("Key deleted in node without children. Cleanup needed.\n");
        raxNode *child = NULL;
        while(h != rax->head) {
            child = h;
            debugf("Freeing child %p [%.*s] key:%d\n", (void*)child,
                (int)child->size, (char*)child->data, child->iskey);
            rax_free(child);
            rax->numnodes--;
            h = raxStackPop(&ts);
            /* 不断获取已访问过的节点进行释放
             * 直到是一个key节点 或者非压缩节点的子节点大于1
             */
            if (h->iskey || (!h->iscompr && h->size != 1)) break;
        }

trycompress表示能否尝试压缩节点

首先开始从匹配到的节点向上遍历,通过Stack来访问。因为一个字符串是分散到各个节点上的,所以要不断删除直到不满足条件为止,一个key节点或者多个子节点的普通节点是不能够直接删除的。

if (child) {
            debugf("Unlinking child %p from parent %p\n",
                (void*)child, (void*)h);
            //这里返回的是断开child后的h
            raxNode *new = raxRemoveChild(h,child);
            if (new != h) {
                raxNode *parent = raxStackPeek(&ts);
                raxNode **parentlink;
                if (parent == NULL) {
                    parentlink = &rax->head;
                } else {
                    parentlink = raxFindParentLink(parent,h);
                }
                memcpy(parentlink,&new,sizeof(new));
            }

            //如果删除后节点只有一个child 而且不是key的话 可以变成一个压缩节点
            if (new->size == 1 && new->iskey == 0) {
                trycompress = 1;
                h = new;
            }
        }
}else if (h->size == 1) {
        //如果这个节点有child 删除后可能会压缩
        trycompress = 1;
    }

    //OOM后不压缩
    if (trycompress && ts.oom) trycompress = 0;

这段代码将上面访问到不符合条件的节点与父节点断开,同同时保存父节点的指针,接下来就要开始对这个节点动刀了,会用全新的节点来替换,先看一下raxRemoveChild断开父子节点的代码

raxNode *raxRemoveChild(raxNode *parent, raxNode *child) {
    debugnode("raxRemoveChild before", parent);

    //paren节点如果是压缩的 则可以直接删除子节点
    if (parent->iscompr) {
        void *data = NULL;
        if (parent->iskey) data = raxGetData(parent);
        parent->isnull = 0;
        parent->iscompr = 0;
        parent->size = 0;
        if (parent->iskey) raxSetData(parent,data);
        debugnode("raxRemoveChild after", parent);
        return parent;
    }

    //获取第一个字节点指针 然后不断循环到等于child节点的指针位置 就能获取到计数
    raxNode **cp = raxNodeFirstChildPtr(parent);
    raxNode **c = cp;
    unsigned char *e = parent->data;
    
    while(1) {
        raxNode *aux;
        memcpy(&aux,c,sizeof(aux));
        if (aux == child) break;
        c++;
        e++;
    }

    //e是child位置 将e+1后面剩余的数据复制到e的位置 等于删除了e
    int taillen = parent->size - (e - parent->data) - 1;
    debugf("raxRemoveChild tail len: %d\n", taillen);
    memmove(e,e+1,taillen);

    //这里是重新对齐
    size_t shift = ((parent->size+4) % sizeof(void*)) == 1 ? sizeof(void*) : 0;

    if (shift)
        memmove(((char*)cp)-shift,cp,(parent->size-taillen-1)*sizeof(raxNode**));

    /* Move the remaining "tail" pointers at the right position as well. */
    size_t valuelen = (parent->iskey && !parent->isnull) ? sizeof(void*) : 0;
    memmove(((char*)c)-shift,c+1,taillen*sizeof(raxNode**)+valuelen);

    //减少计数
    parent->size--;


    raxNode *newnode = rax_realloc(parent,raxNodeCurrentLength(parent));
    if (newnode) {
        debugnode("raxRemoveChild after", newnode);
    }
    return newnode ? newnode : parent;
}

看注释

     * "FOO" -> "BAR" -> [] (2)
     *删除FOO
     * "FOOBAR" -> [] (2)


     *          |B| -> "AR" -> [] (1)
     * "FOO" -> |-|
     *          |T| -> "ER" -> [] (2)
     *
     * 删除FOOTER
     *
     * "FOO" -> |B| -> "AR" -> [] (1)
     *
     * 压缩
     *
     * "FOOBAR" -> [] (1)

redis注释给了两种情况,意思是,删除了节点后会出现多个非key的压缩节点,可以合并为一个节点。

if (trycompress) {
        debugf("After removing %.*s:\n", (int)len, s);
        debugnode("Compression may be needed",h);
        debugf("Seek start node\n");

        /* Try to reach the upper node that is compressible.
         * At the end of the loop 'h' will point to the first node we
         * can try to compress and 'parent' to its parent. */
        raxNode *parent;
        //从stack获取节点 直到以下条件满足为止
        while(1) {
            parent = raxStackPop(&ts);
            //压缩的条件是 非key节点 或者 size为1的压缩节点
            if (!parent || parent->iskey ||
                (!parent->iscompr && parent->size != 1)) break;
            h = parent;
            debugnode("Going up to",h);
        }

        raxNode *start = h;

        /* Scan chain of nodes we can compress. */
        size_t comprsize = h->size;
        int nodes = 1;
        //从h开始计算可以合并的节点数量
        while(h->size != 0) {
            //不断获取child 直到不满足合并条件
            raxNode **cp = raxNodeLastChildPtr(h);
            memcpy(&h,cp,sizeof(h));
            if (h->iskey || (!h->iscompr && h->size != 1)) break;
            if (comprsize + h->size > RAX_NODE_MAX_SIZE) break;
            nodes++;
            comprsize += h->size;
        }
        if (nodes > 1) {
            //可以压缩nodes个节点 创建新的节点来保存
            size_t nodesize =
                sizeof(raxNode)+comprsize+raxPadding(comprsize)+sizeof(raxNode*);
            raxNode *new = rax_malloc(nodesize);
            //创建不了节点不能调整树 但是要保留树以便后面查询
            if (new == NULL) {
                raxStackFree(&ts);
                return 1;
            }
            new->iskey = 0;
            new->isnull = 0;
            new->iscompr = 1;
            new->size = comprsize;
            rax->numnodes++;

            //重新再遍历一遍
            comprsize = 0;
            h = start;
            while(h->size != 0) {
                //拷贝节点数据
                memcpy(new->data+comprsize,h->data,h->size);
                comprsize += h->size;
                raxNode **cp = raxNodeLastChildPtr(h);
                raxNode *tofree = h;
                memcpy(&h,cp,sizeof(h));
                //释放原节点内存
                rax_free(tofree); rax->numnodes--;
                if (h->iskey || (!h->iscompr && h->size != 1)) break;
            }
            debugnode("New node",new);

            //修改指针 指向新节点
            raxNode **cp = raxNodeLastChildPtr(new);
            memcpy(cp,&h,sizeof(h));

            //修改父节点
            if (parent) {
                raxNode **parentlink = raxFindParentLink(parent,start);
                memcpy(parentlink,&new,sizeof(new));
            } else {
                rax->head = new;
            }

            debugf("Compressed %d nodes, %d total bytes\n",
                nodes, (int)comprsize);
        }
    }

 raxStackFree(&ts);
    return 1;

步骤如下:

  1. 从Stack取出节点,不断向上获取直到不满足压缩条件
  2. 从这个节点不断向下获取,统计可以被压缩的节点数量
  3. 创建新节点,再次向下遍历将节点数据添加进新节点,并释放旧节点
  4. 新节点连接进rax树

通过以上步骤,可以将多个压缩节点合并成一个压缩节点,裁剪了树完成了节点的删除。

遍历节点

由于rax内部的key是按照字典序排序的,某个节点是key节点,左右两边都是按照字典序排列的。遍历方法如下:

void raxStart(raxIterator *it, rax *rt);
int raxSeek(raxIterator *it, const char *op, unsigned char *ele, size_t len);
int raxNext(raxIterator *it);
int raxPrev(raxIterator *it);

通过raxStart来创建迭代器,然后调用raxSeek来初始化迭代器位置

int raxSeek(raxIterator *it, const char *op, unsigned char *ele, size_t len) {
    int eq = 0, lt = 0, gt = 0, first = 0, last = 0;

    it->stack.items = 0; 
    it->flags |= RAX_ITER_JUST_SEEKED;
    it->flags &= ~RAX_ITER_EOF;
    it->key_len = 0;
    it->node = NULL;

    /* Set flags according to the operator used to perform the seek. */
    if (op[0] == '>') {
        gt = 1;
        if (op[1] == '=') eq = 1;
    } else if (op[0] == '<') {
        lt = 1;
        if (op[1] == '=') eq = 1;
    } else if (op[0] == '=') {
        eq = 1;
    } else if (op[0] == '^') {
        first = 1;
    } else if (op[0] == '$') {
        last = 1;
    } else {
        errno = 0;
        return 0; /* Error. */
    }

ele表示元素,op为查找操作,支持:>、>=、<、<=、=、^(查找首个元素)、$(查找尾部元素)

上述代码解析op命令,并创建标志符

if (it->rt->numele == 0) {
        it->flags |= RAX_ITER_EOF;
        return 1;
    }

    if (first) {
        return raxSeek(it,">=",NULL,0);
    }

如果numele为0,表示没有元素并且设置EOF返回,如果查找首个元素,改写为大于等于NULL的元素

 if (last) {
        it->node = it->rt->head;
        if (!raxSeekGreatest(it)) return 0;
        assert(it->node->iskey);
        it->data = raxGetData(it->node);
        return 1;
    }
int raxSeekGreatest(raxIterator *it) {
    while(it->node->size) {
        if (it->node->iscompr) {
            if (!raxIteratorAddChars(it,it->node->data,
                it->node->size)) return 0;
        } else {
            if (!raxIteratorAddChars(it,it->node->data+it->node->size-1,1))
                return 0;
        }
        raxNode **cp = raxNodeLastChildPtr(it->node);
        if (!raxStackPush(&it->stack,it->node)) return 0;
        memcpy(&it->node,cp,sizeof(it->node));
    }
    return 1;
}

如果是last获取最后一个元素,那么只需要一直遍历每个元素的最右节点即可。

int splitpos = 0;
    size_t i = raxLowWalk(it->rt,ele,len,&it->node,NULL,&splitpos,&it->stack);

    /* Return OOM on incomplete stack info. */
    if (it->stack.oom) return 0;

    //设置了"="符号 后面的条件表示找到了对应的值 可以获取
    if (eq && i == len && (!it->node->iscompr || splitpos == 0) &&
        it->node->iskey)
    {
        if (!raxIteratorAddChars(it,ele,len)) return 0;
        it->data = raxGetData(it->node);
     

然后通过raxLowWalk寻找节点,找到即可返回,如果没有找到节点,但是设置了<或者>,就要借用stack来进行辅助,核心是利用了raxIteratorPrevStep和raxIteratorNextStep函数,raxNext和raxPrev方法也是用到这两个。

int raxIteratorNextStep(raxIterator *it, int noup) {
    if (it->flags & RAX_ITER_EOF) {
        return 1;
    } else if (it->flags & RAX_ITER_JUST_SEEKED) {
        it->flags &= ~RAX_ITER_JUST_SEEKED;
        return 1;
    }
    
    size_t orig_key_len = it->key_len;
    size_t orig_stack_items = it->stack.items;
    raxNode *orig_node = it->node;

raxIteratorNextStep首先处理flag情况比如到底了,然后保存原始key_len和item

while(1) {
        int children = it->node->iscompr ? 1 : it->node->size;
        //当前节点有字节点 返回
        if (!noup && children) {
            debugf("GO DEEPER\n");
            if (!raxStackPush(&it->stack,it->node)) return 0;
            raxNode **cp = raxNodeFirstChildPtr(it->node);
            if (!raxIteratorAddChars(it,it->node->data,
                it->node->iscompr ? it->node->size : 1)) return 0;
            memcpy(&it->node,cp,sizeof(it->node));
            if (it->node_cb && it->node_cb(&it->node))
                memcpy(cp,&it->node,sizeof(it->node));
            
            if (it->node->iskey) {
                it->data = raxGetData(it->node);
                return 1;
            }
        } else {
            //找父节点
            while(1) {
                int old_noup = noup;

                //已经循环到头部 无法找了 返回
                if (!noup && it->node == it->rt->head) {
                    it->flags |= RAX_ITER_EOF;
                    it->stack.items = orig_stack_items;
                    it->key_len = orig_key_len;
                    it->node = orig_node;
                    return 1;
                }
                //如果当前节点没有字节点 寻找父节点的下一个子节点
                unsigned char prevchild = it->key[it->key_len-1];
                if (!noup) {
                    it->node = raxStackPop(&it->stack);
                } else {
                    noup = 0;
                }
                /* Adjust the current key to represent the node we are
                 * at. */
                int todel = it->node->iscompr ? it->node->size : 1;
                raxIteratorDelChars(it,todel);

                //至少又一个额外的子节点 尝试
                if (!it->node->iscompr && it->node->size > (old_noup ? 0 : 1)) {
                    raxNode **cp = raxNodeFirstChildPtr(it->node);
                    int i = 0;
                    while (i < it->node->size) {
                        //遍历所有子节点 找到比当前key大的第一个
                        debugf("SCAN NEXT %c\n", it->node->data[i]);
                        if (it->node->data[i] > prevchild) break;
                        i++;
                        cp++;
                    }
                    //如果找到了子节点比当前大
                    if (i != it->node->size) {
                        debugf("SCAN found a new node\n");
                        raxIteratorAddChars(it,it->node->data+i,1);
                        if (!raxStackPush(&it->stack,it->node)) return 0;
                        memcpy(&it->node,cp,sizeof(it->node));
                        if (it->node_cb && it->node_cb(&it->node))
                            memcpy(cp,&it->node,sizeof(it->node));
                        //如果当前节点是key节点 就可以获取值返回 不然则终止内循环
                        if (it->node->iskey) {
                            it->data = raxGetData(it->node);
                            return 1;
                        }
                        break;
                    }
                }
            }
        }
    }

详细细节见注释

由于raxIteratorPrevStep是next的反方向,所以就不分析了,逻辑都是差不多的。

总结

rax的删除以及遍历分析完了,stream是基于rax编写的,后续会分析stream源码