redis源码分析与思考(五)——跳跃表

    在计算机科学领域,跳跃链表是一种数据结构,允许快速查询一个有序连续元素的数据链表。快速查询是通过维护一个多层次的链表,且每一层链表中的元素是前一层链表元素的子集。一开始时,算法在最稀疏的层次进行搜索,直至需要查找的元素在该层两个相邻的元素中间。这时,算法将跳转到下一个层次,重复刚才的搜索,直到找到需要查找的元素为止。-------维基百科

    简单的来说,跳跃表是一个有序的数据集合,里面每个跳跃节点存贮着大量指向其它跳跃节点的指针,来实现快速访问其它节点的目的。它的性能可以媲美平衡树,且比平衡树更容易实现。如图所示(图片来自维基百科):
图片来自维基百科

       而在Redis中,跳跃表是作为有序集合的底层实现之一,当有序集合含有的数据比较多,又或者数据是比较长的字符串时,Redis就会采用跳跃表来实现有序集合。

定义

//跳跃表节点
typedef struct zskiplistNode {
    // 成员对象
    robj *obj;
    // 分值
    double score;
    // 后退指针
    struct zskiplistNode *backward;
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } level[];
} zskiplistNode;

    obj指向的是一个sds,而level中存贮着跳向另外节点的指针与它们之间的跨度,跨度指的是从该节点到目标节点跨过的多少节点。 无目标节点则为0,有目标节点从1开始计算。且跨度是用来计算该节点的排位值的,在遍历时该节点经历过的节点的跨度和即为该节点的排位值。

//跳跃表
typedef struct zskiplist {
    // 表头节点和表尾节点
    struct zskiplistNode *header, *tail;
    // 表中节点的数量
    unsigned long length;
    // 表中层数最大的节点的层数
    int level;
} zskiplist;

创建

//节点的创建
zskiplistNode *zslCreateNode(int level, double score, robj *obj) {
    // 分配空间
    zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
    // 设置属性
    zn->score = score;
    zn->obj = obj;
    return zn;
}

//跳跃表的创建
#define ZSKIPLIST_MAXLEVEL 32 //最大的层次,32
zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;
    // 分配空间
    zsl = zmalloc(sizeof(*zsl));
    // 设置高度和起始层数,默认最少只有一层level
    zsl->level = 1;
    zsl->length = 0;
    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;
}

    需注意的是,在Redis中节点的层次大小是随机的,是采用了幂次定律,即越大的数几率越小,它会产生一个1至32的数。

#define ZSKIPLIST_P 0.25  
int zslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))// 每往上提一层的概率为4分之一
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

释放

//释放节点
void zslFreeNode(zskiplistNode *node) {
    //释放对象
    decrRefCount(node->obj);
    zfree(node);
}
//释放链表
void zslFree(zskiplist *zsl) {
    zskiplistNode *node = zsl->header->level[0].forward, *next;
    // 释放表头
    zfree(zsl->header);
    // 释放表中所有节点
    while(node) {
        next = node->level[0].forward;
        zslFreeNode(node);
        node = next;
    }
    // 释放跳跃表结构
    zfree(zsl);
}

    值得注意的是释放节点函数中的decrRefCount函数,因为Redis自己用c实现了对象与对象的回收机制(计数回收),调用这个方法其实就是在调用该对象的数量上减一。

遍历

    跳跃表的遍历是采用前进指针来完成的。而采用后退指针可以完成从分值大的到小的一次遍历。遍历的过程如图所示(黑色表示前进遍历,红色表示后退遍历):
在这里插入图片描述

    跳跃表的查找都是每次从每个跳跃节点的最高层次开始查找的,是跳着查找的,查找的过程代码所示:

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 &&
                // 比对对象,T = O(N)
                compareStringObjects(x->level[i].forward->obj,obj) < 0)))
            // 沿着前进指针移动
            x = x->level[i].forward;
    }

    如图:

在这里插入图片描述

插入

zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;
    //确保score有值
    redisAssert(!isnan(score));
    // 在各个层查找节点的插入位置
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
       //每次从最高层开始找
       //rank用来记录与新节点相连的跨过的节点数
        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 &&
                // 比对成员
                compareStringObjects(x->level[i].forward->obj,obj) < 0))) {
            // 记录沿途跨越了多少个节点
            rank[i] += x->level[i].span;
            // 移动至下一指针
            x = x->level[i].forward;
        }
        // 记录要和新节点相连接的节点
        update[i] = x;
    }
    // 获取一个随机值作为新节点的层数
    level = zslRandomLevel();
    // 如果新节点的层数比表中其他节点的层数都要大
    // 那么初始化表头节点中未使用的层,并将它们记录到 update 数组中
    // 将来也指向新节点
    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;
    }
    // 创建新节点
    x = zslCreateNode(level,score,obj);
    // 将前面记录的指针指向新节点,并做相应的设置
    for (i = 0; i < level; i++) {
        // 设置新节点的 forward 指针
        x->level[i].forward = update[i]->level[i].forward;
        // 将沿途记录的各个节点的 forward 指针指向新节点
        update[i]->level[i].forward = x;
        // 计算新节点跨越的节点数量
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
        // 更新新节点插入之后,沿途节点的 span 值
        // 其中的 +1 计算的是新节点
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }
    // 未接触的节点的 span 值也需要增一,这些节点直接从表头指向新节点
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }
    // 设置新节点的后退指针
    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,另外一个是rank。update记载的是与新节点有关联的节点,而rank记载的是与update数组中相对应着的rank值,而且rank[0]表示着新节点的前驱节点的排位值,所以它是计算排位值与span值的关键。

删除

void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
    int i;
    // 更新所有和被删除节点 x 有关的节点的指针,解除它们之间的关系
    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;
            //如果不是,将span值减一
        } else {
            update[i]->level[i].span -= 1;
        }
    }
    // 更新被删除节点 x 的前进和后退指针
    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--;
}

//删除指定分数的、指定对象的节点
int zslDelete(zskiplist *zsl, double score, robj *obj) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    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 &&
                // 比对象
                compareStringObjects(x->level[i].forward->obj,obj) < 0)))
            // 沿着前进指针移动
            x = x->level[i].forward;
        // 记录与指定删除节点有关联的节点
        update[i] = x;
    }
     //检查找到的元素 x ,只有在它的分值和对象都相同时,才将它删除。    
    x = x->level[0].forward;
    if (x && score == x->score && equalStringObjects(x->obj,obj)) {
        zslDeleteNode(zsl, x, update);
        zslFreeNode(x);
        return 1;
    } else {
        return 0; /* not found */
    }
    return 0; /* not found */
}

总结

1. 跳跃表是有序集合的底层实现之一,每个跳跃表节点的高度是1至32之间的随机数。
2. 跳跃表的大小是随机的,采用了幂次定律来生成节点的层次大小。
3. 理论上,节点的层次大小越高,跳跃表的效率就越高。可以插入分值相同但是对象不同的节点

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值