Redis初识-跳跃表| 8月更文挑战

282 阅读10分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

1. 简介

1.1 基本思想

通过将有序集合的部分节点分层,由最上层开始依次向后查找,如果本层的 next 节点大于待查找目标值或 next 节点为 NULL ,则从本节点降低一层向后继续查找,依次类推,找到目标值则返回节点,否则返回 NULL

下图中,总共查找了4次就可以找到55,比有序链表少了两次。当数据量大时,效果会更加明显

分成有序链表

1.2 实现过程

跳跃表的实现过程

跳跃表的性质:

  • 跳跃表由多层构成
  • 跳跃表有一个头节点 header ,头节点中有64层
  • 节点每层的结构包含指向下个节点的指针,指向本层下个节点所跨越的节点个数为本层的跨度 span
  • 除头结点外,层数最多的节点的层高为跳跃表的高度 level ,如图示高度为3
  • 每层都是一个有序链表,数据递增
  • 除头节点外,如果一个节点在上层的有序链表出现过,则下层一定也会出现该节点
  • 每层最后一个节点指向 NULL ,表示本层有序链表的结束
  • 跳跃表拥有一个尾结点 tail ,指向跳跃表最后一个节点
  • 最底层的有序链表包含所有节点,该层节点个数为跳跃表的长度 length ,如图示长度为7
  • 每个节点包含一个后退指针,指向最底层的前一个节点,头节点和第一个节点的后退指针为 NULL

跳跃表中每个节点维护了多个指向其他节点指针,所以在进行查找、插入、删除操作时可以跳过一些节点,快速找到需要操作的节点。跳跃表以牺牲空间换取时间的方式达到快速查找的目的

2. 数据存储结构

跳跃表为 Redis 有序集合的底层实现之一,每个节点的 ele 存储有序集合的成员 member 值,score 存储成员的 score 值。所有节点的分值从小到大排序,当分值相同时,节点会根据 member 的字典序排序

跳跃表能在 O(1) 的时间复杂度下,快速获取到头节点、尾结点、长度、高度

2.1 跳跃表

跳跃表的 zskiplist 结构体

// server.h
typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;
  • header:指向跳跃表头节点的指针
    • 头节点为特殊的节点,它的 level 大小为64
    • 头节点不存储任何 member 和 score 值,ele 值为 NULL ,score 为0
    • 不计入跳跃表的长度
    • 头节点初始化时,64个元素的 forward 都指向 NULL ,span 为0
  • tail:指向跳跃表尾结点的指针
  • length:跳跃表的长度,表示除头节点外的节点总数
  • level:跳跃表的高度

2.2 跳跃表节点

跳跃表节点的 zskiplistNode 结构体

// server.h
typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;
  • ele:存储字符串类型的数据
  • score:存储排序的分值
  • backward:后退指针,指向当前节点在最底层的前一个节点,头节点和第一个节点的 backward 指向 NULL 。从后向前遍历跳跃表时使用
  • level:柔性数组。每个节点的 level 的大小不一样,在创建节点时,随机生成一个1~64的值决定 level 的大小,值越大出现概率越小
    • forward:指向本层下一个节点的指针,尾结点的 forward 指向 NULL
    • span:当前节点指向本层下个节点所跨越的节点个数。span 越大,跨越的节点越多

3. 源码浅析

3.1 创建跳跃表

3.1.1 节点层高

创建节点时,会通过 zslRandomLevel 函数随机生成一个1~64的值作为节点的高度,值越大概率越小。节点的高度确定后就不会更改

// server.h
#define ZSKIPLIST_MAXLEVEL 64
#define ZSKIPLIST_P 0.25

// t_zset.c
int zslRandomLevel(void) {
  // 初始高度为1  
  int level = 1;
    // 随机获取一个值,取该值的低16位作为 x
    // 当 x 小于0.25倍 0xFFFF 时,高度加1,否则退出循环
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    // 取 level 和 ZSKIPLIST_MAXLEVEL 中的最小值作为节点高度
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
3.1.2 创建节点

创建节点时,需要确定高度、分值、member

// t_zset.c
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;
}
3.1.3 创建跳跃表
// t_zset.c
zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;

    // 申请内存
    zsl = zmalloc(sizeof(*zsl));
    // 初始层高 level=1
    zsl->level = 1;
    // 初始长度 length=0
    zsl->length = 0;
    // 创建头节点,头节点的分值 score=0 、字符串内容 ele=NULL 、层高 level=64
    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
    // 遍历头节点的层级,设置每层指向的下一个节点 forward=NULL ,跨度 span=0
    for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
        zsl->header->level[j].forward = NULL;
        zsl->header->level[j].span = 0;
    }
    // 设置头节点的后退节点 backward=NULL
    zsl->header->backward = NULL;
    // 设置尾节点 tail=NULL
    zsl->tail = NULL;
    return zsl;
}

3.2 插入节点

插入节点的步骤:

  • 查找插入的位置
  • 调整跳跃表的高度(有需要时)
  • 插入节点
  • 调整节点的 backward(有需要的节点)

假设已存在一个跳跃表,需要插入一个 score=40 的节点,下面为初始跳跃表:

初始跳跃表

3.2.1 查找插入的位置
// t_zset.c
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    // update[]:记录待插入节点在每层的前一个节点
    // rank[]:记录每层的头节点到 update[i] 节点的跨度
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;

    serverAssert(!isnan(score));
    x = zsl->header;
    // 从头节点开始,遍历跳跃表的层级(高到低),统计出 update[] 和 rank[]
    for (i = zsl->level-1; i >= 0; i--) {
        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;
    }
    // ......
}

根据上述代码统计出 update 和 rank:

update和rank赋值后跳跃表

3.2.2 调整跳跃表高度

节点的层高是随机的,假设待插入节点的层高为3,大于跳跃表当前高度2,则需要调整跳跃表的高度

// t_zset.c
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    // ......
  
    // 随机获取待插入节点高度
    level = zslRandomLevel();
    // 如果待插入节点高度大于跳跃表当前高度,则需要调整跳跃表高度
    if (level > zsl->level) {
        // 遍历跳跃表当前高度到待插入节点高度
        // 统计新增层级的 update 和 rank
        // 设置新增层级的头节点的跨度 span 为跳跃表当前长度,后续使用
        for (i = zsl->level; i < level; i++) {
            rank[i] = 0;
            update[i] = zsl->header;
            update[i]->level[i].span = zsl->length;
        }
        // 更新跳跃表高度
        zsl->level = level;
    }
    // ......
}

根据上述代码调整跳跃表高度、统计和设置相关值:

调整高度后跳跃表

3.2.3 插入节点
// t_zset.c
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    // ......
  
    // 创建新节点
    x = zslCreateNode(level,score,ele);
    // 从低到高遍历层级,插入新节点(类似链表插入节点)
    for (i = 0; i < level; i++) {
        // 设置新节点在该层级的 forward
        x->level[i].forward = update[i]->level[i].forward;
        // 设置新节点在该层级的前一个节点的 forward
        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;
    }
  
    // 当新节点的层高小于跳跃表高度时
    // 需要调整新节点层高到跳跃表高度之间层级的 update[i] 的跨度 span
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }
    // ......
}

根据上述代码插入节点:

插入节点后跳跃表

3.2.4 调整 backward
// t_zset.c
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    // ......

    // 设置新节点的 backward
    // 如果新节点在最底层的前一个节点为头节点,则 backward=NULL
    // 否则 backward=update[0]
    x->backward = (update[0] == zsl->header) ? NULL : update[0];
    // 如果新节点在最底层的下一个节点存在,则下一个节点的 backward 为该新节点
    // 否则设置跳跃表的尾结点 tail 为该新节点
    if (x->level[0].forward)
        x->level[0].forward->backward = x;
    else
        zsl->tail = x;
    // 跳跃表长度加1
    zsl->length++;
    return x;
}

调整 backward:

调整backward后跳跃表

3.3 删除节点

删除节点的步骤:

  • 查找需要更新的节点
  • 调整 span、forward、backward

假设存在3.2.4节的跳跃表,删除 score=40 的节点 x

3.3.1 查找需要更新的节点
// t_zset.c
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--) {
        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;
    }
    // 获取最底层可能是待删除的节点 x
    x = x->level[0].forward;
    // 如果 x 节点存在,且是待删除节点,则进一步执行删除逻辑
    if (x && score == x->score && sdscmp(x->ele,ele) == 0) {
        // 删除节点
        zslDeleteNode(zsl, x, update);
        // node 存在时,需要将删除节点 x 赋值给 node
        // node 不存在时,释放删除节点 x
        if (!node)
            zslFreeNode(x);
        else
            *node = x;
        return 1;
    }
    // 未找到待删除节点,返回0
    return 0;
}

删除节点前:

删除节点前的跳跃表

3.3.2 调整 span、forward、backward
// t_zset.c
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] 的下一个节点是待删除节点 x ,则更改其下一个节点 forward 和跨度 span
            update[i]->level[i].span += x->level[i].span - 1;
            update[i]->level[i].forward = x->level[i].forward;
        } else {
            // 如果 update[i] 的下一个节点不是待删除节点 x ,则更改其跨度 span(待删除节点的高度小于跳跃表高度的场景)
            update[i]->level[i].span -= 1;
        }
    }
    if (x->level[0].forward) {
        // 如果待删除节点在最底层的下一个节点存在,则更新其 backward
        x->level[0].forward->backward = x->backward;
    } else {
        // 如果待删除节点在最底层的下一个节点为 NULL ,则更新跳跃表尾结点
        zsl->tail = x->backward;
    }
    // 遍历跳跃表层级(高到低),调整跳跃表高度
    // 如果当前层级头节点的下一个节点为 NULL ,则高度减1,直到不满足该条件是停止调整
    while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
        zsl->level--;
    // 跳跃表长度减1
    zsl->length--;
}

调整 span、forward、backward 后:

删除节点后的跳跃表

3.4 删除跳跃表

// t_zset.c
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);
}

4. 跳跃表的应用

Redis 中,跳跃表主要应用于有序集合的实现(另一种实现为压缩列表)

关于有序集合的配置:

  • zset_max_ziplist_entries:128
  • zset_max_ziplist_value:64

4.1 初始有序集合

// t_zset.c
void zaddGenericCommand(client *c, int flags) {
    // ......
    if (server.zset_max_ziplist_entries == 0 ||
        server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
    {
        // 创建跳跃表结构
        zobj = createZsetObject();
    } else {
        // 创建压缩链表结构
        zobj = createZsetZiplistObject();
    }
    // ......
}
  • 当 zset_max_ziplist_entries 设置为 0
  • 插入元素的字符串长度大于 zset_max_ziplist_value

上面两个条件满足其一时,初始的 zset 为跳跃表结构。一般情况下都不满足,所以默认结构为压缩链表

4.1 有序集合变更结构

// t_zset.c
int zsetAdd(robj *zobj, double score, sds ele, int *flags, double *newscore) {
    // ......
    if (zzlLength(zobj->ptr) > server.zset_max_ziplist_entries)
        zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
    if (sdslen(ele) > server.zset_max_ziplist_value)
        zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
            
}
  • 跳跃表长度大于 zset_max_ziplist_entries
  • 插入元素的长度大于 zset_max_ziplist_value

上面两个条件满足其一时,zset 的压缩链表结构变更为跳跃表。变更结构后,删除节点时也不会重新转变为压缩链表

跳跃表的查询、插入、删除操作的平均时间复杂度为 O(logN)

学自《Redis 5设计与源码分析》