redis之zskiplist

         跳跃表(zskiplist)在redis中作为sorted set的底层实现,今天来学习一下。

/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {//跳跃表节点
    sds ele;  // 成员对象[必须唯一]
    double score; // 分值[用来排序][可以相同]
    struct zskiplistNode *backward; // 后退指针用于从表头向表尾遍历
    
    struct zskiplistLevel { // 层
        struct zskiplistNode *forward; // 前进指针
        unsigned long span;// 跨度
    } level[];  //数组
	
} zskiplistNode;

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

zskiplist中有头指针和尾指针,尾指针方便从尾部遍历。zskiplistNode中有个结构体zskiplistLevel,这个结构体是跳跃表的实现,level是个结构体数组,其中forward指针有用跨度,可以指向非相邻的结点。span表示forward指向的结点距离本结点多远。

/* Create a new skiplist. */
zskiplist *zslCreate(void) {//创建并返回一个新的跳跃表
    int j;
    zskiplist *zsl;

    zsl = zmalloc(sizeof(*zsl));//跳跃表
    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;
}

刚开始默认结点最大层级是1,指针赋空。

int zslRandomLevel(void) {//返回一个随机值,用作新跳跃表节点的层数
    int level = 1;  //0xFFFF 65535 //random()返回一个[0...1)的随机数
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))//random()&0xFFFF始终小于65535
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

random是一个伪随机数,执行多次之后,返回的level也是一个固定的序列,后面会测试一下。

/* Insert a new node in the skiplist. Assumes the element does not already
 * exist (up to the caller to enforce that). The skiplist takes ownership
 * of the passed SDS string 'ele'. */
//创建一个成员为 ele ,分值为 score 的新节点,并将这个新节点插入到跳跃表 zsl 中
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {  //mapan
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;

    serverAssert(!isnan(score));
    x = zsl->header;  //取出头

    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;// 记录将要和新节点相连接的节点
    }
    /* we assume the element is not already inside, since we allow duplicated
     * scores, reinserting the same element should never happen since the
     * caller of zslInsert() should test in the hash table if the element is
     * already inside or not. */
    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; // 更新表中节点最大层数
    }
    x = zslCreateNode(level,score,ele);
    // 将前面记录的指针指向新节点,并做相应的设置
    for (i = 0; i < level; i++) {   //1
        x->level[i].forward = update[i]->level[i].forward;// 设置新节点的 forward 指针
        update[i]->level[i].forward = x;// 将沿途记录的各个节点的 forward 指针指向新节点

        /* update span covered by update[i] as x is inserted here */
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);// 计算新节点跨越的节点数量
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }

    /* increment span for untouched levels */
    // 未接触的节点的 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;
}

跳跃表的插入函数,我们写一个程序测试一下:

int t_zsetTest()        //mapan
{
   zskiplist *p = zslCreate();
   zslInsert(p,1.1,"a");
   zslInsert(p,3.3,"b");
   zslInsert(p,2.2,"c");
   zslInsert(p,4.4,"d");
   zslInsert(p,0.9,"e");
   zslInsert(p,3.8,"f");
   zslInsert(p,5.5,"g");
   printf("----------------\n");
   zslInsert(p,1.3,"h");
   
   return 0;
}

先看一下,执行对应插入操作所获取的层级:

mapan@mapan-virtual-machine:~/redis-5.0.5/src$ ./redis-server 
level=1,score=1.100000
level=2,score=3.300000
level=1,score=2.200000
level=1,score=4.400000
level=1,score=0.900000
level=1,score=3.800000
level=1,score=5.500000
----------------
level=2,score=1.300000
mapan@mapan-virtual-machine:~/redis-5.0.5/src$ ./redis-server 
level=1,score=1.100000
level=2,score=3.300000
level=1,score=2.200000
level=1,score=4.400000
level=1,score=0.900000
level=1,score=3.800000
level=1,score=5.500000
----------------
level=2,score=1.300000

你每次运行所对应的层级是一个固定的序列。3.3和1.3对应的层级是2,其他都是1。当执行到zslInsert(p,5.5,"g")时,我们看一下跳跃表的结构。

最下面一层对应都是L1。这时候在执行zslInsert(p,1.3,"h");操作,就会先遍历L2对应的那层,在遍历L1对应的那层。

/* Delete an element with matching score/element from the skiplist.
 * The function returns 1 if the node was found and deleted, otherwise
 * 0 is returned.
 *
 * If 'node' is NULL the deleted node is freed by zslFreeNode(), otherwise
 * it is not freed (but just unlinked) and *node is set to the node pointer,
 * so that it is possible for the caller to reuse the node (including the
 * referenced SDS string at node->ele). */
 //从跳跃表 zsl 中删除包含给定节点 score 并且带有指定对象 ele 的节点
int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    int i;

    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) { //逐层遍历,遍历完后要么x指向对应节点,要么找不到
        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)))
        {
            x = x->level[i].forward;
        }
        update[i] = x;// 记录沿途节点
        printf("score=%lf,i=%d\n",update[i]->score,i);
    }
    /* We may have multiple elements with the same score, what we need
     * is to find the element with both the right score and object. */
    x = x->level[0].forward;
    if (x && score == x->score && sdscmp(x->ele,ele) == 0) {
        printf("x->score=%lf\n",x->score);
        zslDeleteNode(zsl, x, update);
        if (!node)
            zslFreeNode(x);
        else
            *node = x; //node用来保存结点,用于重复使用
        return 1;
    }
    return 0; /* not found */
}

删除结点首先也要遍历结点。

/* Update the score of an elmenent inside the sorted set skiplist.
 * Note that the element must exist and must match 'score'.
 * This function does not update the score in the hash table side, the
 * caller should take care of it.
 *
 * Note that this function attempts to just update the node, in case after
 * the score update, the node would be exactly at the same position.
 * Otherwise the skiplist is modified by removing and re-adding a new
 * element, which is more costly.
 *
 * The function returns the updated element skiplist node pointer. */
 //更新排序后的跳跃表中元素的score。
zskiplistNode *zslUpdateScore(zskiplist *zsl, double curscore, sds ele, double newscore) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    int i;

    /* We need to seek to element to update to start: this is useful anyway,
     * we'll have to update or remove it. */
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        while (x->level[i].forward &&
                (x->level[i].forward->score < curscore ||
                    (x->level[i].forward->score == curscore &&
                     sdscmp(x->level[i].forward->ele,ele) < 0)))
        {
            x = x->level[i].forward;
        }
        update[i] = x;
    }

    /* Jump to our element: note that this function assumes that the
     * element with the matching score exists. */
    x = x->level[0].forward;
    serverAssert(x && curscore == x->score && sdscmp(x->ele,ele) == 0);  //确保找到对应的结点
    printf("assert\n");
    /* If the node, after the score update, would be still exactly
     * at the same position, we can just update the score without
     * actually removing and re-inserting the element in the skiplist. */
    if ((x->backward == NULL || x->backward->score < newscore) &&
        (x->level[0].forward == NULL || x->level[0].forward->score > newscore)) //newscore修正后,要注意集合的顺序
    {
        x->score = newscore;
        return x;
    }

    /* No way to reuse the old node: we need to remove and insert a new
     * one at a different place. */
    zslDeleteNode(zsl, x, update);  
    zskiplistNode *newnode = zslInsert(zsl,newscore,x->ele);
    /* We reused the old node x->ele SDS string, free the node now
     * since zslInsert created a new one. */
    x->ele = NULL;
    zslFreeNode(x);
    return newnode;
}

更新跳跃表中的元素时,要保证更新后还是有序的,这就可能需要删除原来的阶段,重新插入新的结点。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

盼盼编程

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

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

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

打赏作者

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

抵扣说明:

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

余额充值