Redis高级之底层源码5——ZSet数据结构底层源码分析

1 概述

        Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,或者有序集合中的元素是比较长的字符串,Redis就会使用跳跃表来作为有序集合键的底层实现。

2 跳跃表数据结构解析

        

                如上图所示,跳跃表有很多层组成。头节点中有一个64层结构,每层结构都包含指向本层的下一个节点的指针,指向本层下一个节点中间所跨越的节点个数为层的跨度(span)。除了头节点外,层数最多的节点的层高为跳跃表的高度(level),每一层都是一个有序链表,即数据是递增的。另外除头节点外,如果一个元素在上层有序链表中出现,则它一定会在下层有序链表中出现。跳跃表每层最后一个节点指向null,表示本层有序链表的结束。跳跃表拥有一个尾指针,指向跳跃表最后一个节点。最底层的有序链表包含所有节点,最底层的节点个数为跳跃表的长度。跳跃表每个节点含有多个指向其他节点的指针,所以在跳跃表进行查找、插入和删除操作时可以跳过一些节点,快速找到需要操作的节点,这是以牺牲空间的形式来达到快速查找的目的。

        跳跃表节点结构体源码如下:

typedef struct zskiplistNode {
    //用于存储字符串类型的数据
    sds ele;
    //用于存储排序的分值
    double score;
    //后向指针,只能指向当前节点最底层的前一个节点,头结点和第一个节点
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        //指向本层下一个节点,尾节点的forward指向null
        struct zskiplistNode *forward;
        //forward指向的节点与本节点之间的元素个数。span的值越大,跳过的节点个数就越多
        unsigned long span;
    } level[];
} zskiplistNode;

        跳跃表是Redis有序集合的底层一种实现方式,所以每个节点的ele存储有序集合元素的值,score存储元素的score值。所有节点的score值是按照从小到大排序的。

        除了跳跃表节点外,还需要一个跳跃表结构来管理节点。Redis使用zskiplist结构体,源码如下:

typedef struct zskiplist {
    /*header:指向跳跃表头节点。头节点是跳跃表的一个特殊节点,它的level数组元素个数为64.
     * 头节点在有序集合中不存储任何元素和score值,ele值为null,score值为0;不计入
     * 跳跃表的总长度。头节点在初始化时,64个元素的forward都指向null,span值都为0
     *  */
    //tail 指向跳跃表尾节点
    struct zskiplistNode *header, *tail;
    //跳跃表长度
    unsigned long length;
    //跳跃表高度
    int level;
} zskiplist;

3 跳跃表初始化

        初始化源码如下:

zskiplistNode *zslCreateNode(int level, double score, sds ele) {
    zskiplistNode *zn =
        zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
    zn->score = score;
    zn->ele = ele;
    return zn;
}

/* Create a new skiplist. */
zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;
    //分配空间
    zsl = zmalloc(sizeof(*zsl));
    //默认层数等于1
    zsl->level = 1;
    //0个节点
    zsl->length = 0;
    //redis中最大32层级
    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
    for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
        zsl->header->level[j].forward = NULL;
        zsl->header->level[j].span = 0;
    }
    zsl->header->backward = NULL;
    zsl->tail = NULL;
    return zsl;
}

        如上zslCreate函数的源码用于初始化一个跳跃表,Redis中实现的跳跃表最高允许32层索引,这么做也是为了性能与内存之间的均衡,过多的索引层必然占用更多的内存空间,32是一个比较合适的值。

4 插入节点

        插入节点源码如下:

zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    //upate数组用于记录新节点在每一层索引的目标插入位置
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    //记录目标节点每一层的排名
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;

    serverAssert(!isnan(score));
    //指向哨兵节点
    x = zsl->header;
    //循环找到最后一个小于当前给定score值的节点
    for (i = zsl->level-1; i >= 0; i--) {
        /* store rank that is crossed to reach the insert position */
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
        while (x->level[i].forward &&
                (x->level[i].forward->score < score ||
                    (x->level[i].forward->score == score &&
                    sdscmp(x->level[i].forward->ele,ele) < 0)))
        {
            //最终会得到我们要找的目标节点和排名,也就是它前面有多少个节点
            rank[i] += x->level[i].span;
            x = x->level[i].forward;
        }
        update[i] = x;
    }
    /* 获取一个均衡跳跃表的level值,标志着新节点将要在哪些索引中出现 */
    level = zslRandomLevel();
    //如果产生值大于当前跳跃表的最高索引
    if (level > zsl->level) {
        //为搞出来的索引层赋初始值
        for (i = zsl->level; i < level; i++) {
            rank[i] = 0;
            update[i] = zsl->header;
            update[i]->level[i].span = zsl->length;
        }
        zsl->level = level;
    }
    //根据score和ele创建节点
    x = zslCreateNode(level,score,ele);
    //每一索引层都要插入新节点
    for (i = 0; i < level; i++) {
        x->level[i].forward = update[i]->level[i].forward;
        //rank[0]表示新节点在最底层链表的排名,就是它前面有多少个节点
        update[i]->level[i].forward = x;
        
        /* span记录的是目标节点与后一个索引节点之间的跨度,跨域了多少个节点,得到新插入节点与后一个索引节点之间的跨度 */
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
        //修改目标节点span值
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }

    /* 自增span值,因为新插入了一个节点,跨度自然要增加 */
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }
    //修改backward 指针与tail指针
    x->backward = (update[0] == zsl->header) ? NULL : update[0];
    if (x->level[0].forward)
        x->level[0].forward->backward = x;
    else
        zsl->tail = x;
    zsl->length++;
    return x;
}

        根据update的赋值过程,从源码中可以发现,新插入节点的前一个节点一定是update[0]。因为每个节点的后向指针只有一个,与此节点的层数无关,所以当插入节点不是最后一个节点时,需要更新被插入节点的backward以指向update[0]。如果新插入节点是最后一个节点,则需要更新跳转表的尾节点为新插入节点。插入节点后,更新跳跃表的长度加1。

5 删除节点

        删除节点的源码如下:

void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
    int i;
    for (i = 0; i < zsl->level; i++) {
        if (update[i]->level[i].forward == x) {
            update[i]->level[i].span += x->level[i].span - 1;
            update[i]->level[i].forward = x->level[i].forward;
        } else {
            update[i]->level[i].span -= 1;
        }
    }
    if (x->level[0].forward) {
        x->level[0].forward->backward = x->backward;
    } else {
        zsl->tail = x->backward;
    }
    while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
        zsl->level--;
    zsl->length--;
}

6 zslGetRank查询

        获取排名函数的源码如下:

unsigned long zslGetRank(zskiplist *zsl, double score, sds ele) {
    zskiplistNode *x;
    unsigned long rank = 0;
    int i;

    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        while (x->level[i].forward &&
            (x->level[i].forward->score < score ||
                (x->level[i].forward->score == score &&
                sdscmp(x->level[i].forward->ele,ele) <= 0))) {
            rank += x->level[i].span;
            x = x->level[i].forward;
        }

        /* x 有可能等于zsl->header 所以判断是否为非null*/
        if (x->ele && sdscmp(x->ele,ele) == 0) {
            return rank;
        }
    }
    return 0;
}

7 ZipList和跳跃表的选择

        在Redis中,跳跃表主要应用于有序集合的底层实现,而另一种事ZipList。

        Redis的Zset之所有这样设计,是因为当ZipList变得很大的时候,存在几个缺点:

        1、每次插入和修改引发的realloc操作会有更大的概率造成内存复制,从而减低性能。

        2、一旦发生内存复制,成本就会相应增加,因为要复制更大的一块数据。

        3、当ZipList数据项过多时,在它里面查找指定的数据项的性能就会变得很低,因为在ZipList中查找需要进行逐个节点的遍历。

        redis.conf相关配置如下:

        zset-max-ziplist-entries  128

        zset-max-ziplist-value  64

        zset-max-ziplist-entries:表示元素个数的最大值,默认值为128.

        zset-max-ziplist-value  :表示每个元素的字符串长度最大值,默认为64.

        如上配置表示当元素个数大于128或者每一个元素的长度大于64字节的时候,底层数据结构就会从ziplist切换到skiplist。

        底层数据结构切换的源码如下:

 if (server.zset_max_ziplist_entries == 0 ||
            server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
        {
            //创建跳跃表结构
            zobj = createZsetObject();
        } else {
            //创建ziplist压缩列表结构
            zobj = createZsetZiplistObject();
        }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

geminigoth

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值