Redis 之跳表

跳表,又称跳跃表,在 Redis 中表现为 skiplist,是一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。

在正式介绍跳表前,先来看看 Redis 中的有序集合。

zadd class 87.5 alice 87.5 fred 65.5 charles 94.5 emily

向 class 有序集合里插入 4 条数据,查看下底层编码实现。

127.0.0.1:6379> object encoding class
"ziplist"

为压缩列表,在上一讲里详细介绍过,是由一系列特殊编码的连续内存块组成的顺序型数据结构。

// redis.conf

# Similarly to hashes and lists, sorted sets are also specially encoded in
# order to save a lot of space. This encoding is only used when the length and
# elements of a sorted set are below the following limits:
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

在 Redis 配置文件中,一旦有序集合里元素不超过 128 个或元素里的值最大长度不超过 64 时就用 ziplist。在 ziplist 中有序集合是数值在前,score 在后,数据和 score 是一一对应的,因此不超过 128 个,在 ziplist 中就是不超过 128*2 = 256 个,其内存布局大致如下。
在这里插入图片描述
为了进入到今天的主题,这里把配置文件中的 zset-max-ziplist-entries 调整为 1,也就是超过了 1 就是用 skiplist 实现。

127.0.0.1:6379> zadd class 87.5 alice 87.5 fred 65.5 charles 94.5 emily
(integer) 4
127.0.0.1:6379> object encoding class
"skiplist"

这时编码就显示为 skiplist 。

// server.h

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

Redis 中使用的是 zset 结构体来表存储有序集合,里面包含了字典和跳表两种结构。字典用来存储数值(键)和 score(值),从而实现 O(1) 复杂度获取数值对应的 score;跳跃表用来处理区间查询的相关操作。

字典在前面已经详细介绍过,那么接下来就看看 skiplist 的数据结构。

// server.h

#define ZSKIPLIST_MAXLEVEL 64 /* Should be enough for 2^64 elements */

/* 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[]; /* 柔性数组,存储节点层级相关数据,最大层高 64,可以存储 2^64 个节点数 */
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail; /* 跳表的表头和表尾,头节点是个特殊的节点,层高 64,在初始化时生成 */
    unsigned long length; /* 跳表节点长度,除了表头节点之外的总数 */
    int level; /* 跳表的高度,除了表头节点以外的最大高度 */
} zskiplist;

和字典类似,有个 zskiplistNode 结构来存放数值和分数,顺序是从小到大,如果分值一致时,则按照数值的字典序排序;有个方便操作的 zskiplist 结构,比如 O(1) 时间复杂度的获取跳表中节点的个数,依据表头或表尾来实现正向或逆向遍历的 header 和 tail 指针 。
Redis 之跳表_第1张图片
如图所示,zskiplist 是个 zskiplistNode 节点的概括和统筹。记录了有几个节点,最高的层级是多少,表头节点指向哪(方便正向遍历),表尾节点指向(方便反向遍历)。

zskiplistNode 的头节点和第一个存放数组分数的节点的 backward 皆为 NULL,同时头结点默认有 64 层,是为了避免后续高度增加时重新分配内存,其他属性都为默认值。层级箭头上面的数字就是 span ,是为了计算排名(rank)的,在 Redis 中排名是从 0 开始的。需要注意的是,这里 score 是按照由小到大排序的,比如要计算 charles 的排名,就是在查找路径中,把 span 累加起来再减一,1 - 1 为 0(这里减一就如前面提到的,在 Redis 中排名是从 0 开始的);要计算排名日常生活中的排名(由大到小),则需要总长度减去经过的 span 节点数,比如 emily 排名为 4-4 = 0。

127.0.0.1:6379> zrank class charles
(integer) 0
127.0.0.1:6379> zrevrank class emily
(integer) 0

关于 Redis 的跳表基础就介绍到这,接下来说说初始化及常用的 API。

// t_zset.c

/* 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;
}

/* Create a skiplist node with the specified number of levels.
 * The SDS string 'ele' is referenced by the node after the call. */
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;
}

初始化 skiplist 的两个函数的逻辑一目了然。先计算 zskiplist 结构占的内存然后申请对应的内存,默认级别为 1,节点数为 0,尽管后面申请了头部节点,但没算在内, 跳表的表尾节点指向 NULL。跳表的表头节点指向动态生成 zskiplistNode 节点,向前指针为 NULL,数值为 NULL,分值为 0,默认创建 64 层,每次都指向 NULL,跨度 为 0。初始化后内存布局如下。
Redis 之跳表_第2张图片
为了加深理解,这里详细介绍下给跳表添加节点。节点添加的顺序就如同一开始 zadd 命令添加顺序,节点的层级如同上上图中假设的来。

// t_zset.c

/* 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'. */
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; /* udpate 存储搜索路径 */
    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++) {
        x->level[i].forward = update[i]->level[i].forward;
        update[i]->level[i].forward = x;

        /* 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 */
    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;
}

/* Returns a random level for the new skiplist node we are going to create.
 * The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL
 * (both inclusive), with a powerlaw-alike distribution where higher
 * levels are less likely to be returned. */
int zslRandomLevel(void) { /* 随机返回 1~64 的层级 */
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

首先添加 87.5 alice

x = zsl->header; /* 表头赋给 x */
for (i = zsl->level-1; i >= 0; i--) { /* 遍历头节点的每个层级,从下标最大层减 1 到 0。由于是首次写入, zsl->level 为 1,那么 i 的值为 1-1=0 */
    /* store rank that is crossed to reach the insert position */
    rank[i] = i == (zsl->level-1) ? 0 : rank[i+1]; /* 和上面分析一样, rank[0] = 0 */
    while (x->level[i].forward && /* 由于是首次写入,头节点 x 的 forward 节点都指向 NULL, 退出循环 */
            (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[0] = x */
}

第一步,查找要插入的位置。由于是首次插入节点,update[0] 指向 header 节点(update 数组存放搜索路径),rank[0] 为 0(rank 数组存放 update 对应节点到待插入节点的 span 值)。
Redis 之跳表_第3张图片

/* 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(); /* 获取层级,范围在 1~64,假设返回 2 */
if (level > zsl->level) { /* 主要是存储超过表头层级的排名及节点 */
    for (i = zsl->level; i < level; i++) { /* 1 < 2 满足 for 循环 */
        rank[i] = 0; /* rank[1] = 0 */
        update[i] = zsl->header; /* udpate[1] 存储头节点 */
        update[i]->level[i].span = zsl->length; /* 更新头节点的 span 值为当前节点个数,刨除头部节点 */
    }
    zsl->level = level; /* 更新 zskiplist 的层级为最新的 2 */
}

第二步,调整跳表高度。如果要插入节点的高度大于跳表的高度,那么就分别用 rank 和 update 存储高出的那部分层级的节点信息。
Redis 之跳表_第4张图片

x = zslCreateNode(level,score,ele); /* 创建 87.5 alice 节点 */
for (i = 0; i < level; i++) { /* 这里 level 为 2 */
    x->level[i].forward = update[i]->level[i].forward; /* x->level[0].forward = NULL,因为 update[0] 为头结点,而这又是首次写入 */
    update[i]->level[i].forward = x; /* 这里很巧妙,更新头结点的后置指针 */

    /* update span covered by update[i] as x is inserted here */
    x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]); /* 这里为 0 */
    update[i]->level[i].span = (rank[0] - rank[i]) + 1; /* 这里为 1 */
}

/* increment span for untouched levels */
for (i = level; i < zsl->level; i++) { /* 如果添加的节点小于默认层级,则更新层级对应的排名 */
    update[i]->level[i].span++;
}

第三步,插入节点。
Redis 之跳表_第5张图片

x->backward = (update[0] == zsl->header) ? NULL : update[0]; // 指向 NULL
if (x->level[0].forward)
    x->level[0].forward->backward = x;
else
    zsl->tail = x; // 走这里,尾节点执行 x
zsl->length++;  // 节点数加 1

第四步,调整 backward、 zskiplist 的尾结点指针、节点数量。
Redis 之跳表_第6张图片
接下来插入 87.5 fred 节点。

x = zsl->header; /* 表头赋给 x */
for (i = zsl->level-1; i >= 0; i--) { /* 遍历头节点的每个层级,从下标最大层减 1 到 0;i=2-1=1 */
    /* store rank that is crossed to reach the insert position */
    rank[i] = i == (zsl->level-1) ? 0 : rank[i+1]; /* rank[1] = 0,rank[0] = rank[1] = 0 */
    while (x->level[i].forward && /* 前置节点不为 NULL */
            (x->level[i].forward->score < score || /* 如果前置节点的分值小于当前要插入的节点的分值 */
                (x->level[i].forward->score == score && /* 当分数相同时,则按照字典序来比较两个数值 */
                sdscmp(x->level[i].forward->ele,ele) < 0))) /* 因为字典序 fred 大于 alice,因此进入 while 循环  */
    {
        rank[i] += x->level[i].span; /* while 内第一遍历,rank[1] = 0 + 1 = 1,第二次遍历 rank[0] = 0 + 1 = 1 */
        x = x->level[i].forward; /* x 此时指向了 alice 节点  */
    }
    update[i] = x; /* update[i] 指向各层的本层下一个节点 */
}

第一步,查找要插入的位置。由于分值相同,都是 87.5,那么用字典序比较数值,发现 fred 比 alice 大,那么位置就在 alice 后面。

第二步,更新跳表的高度。由于 fred 的层级为 1,这一步跳过。

第三步,插入节点。

Redis 之跳表_第7张图片

第四步,调整 backward、 zskiplist 的尾结点指针、节点数量。
Redis 之跳表_第8张图片
写入就介绍到这了,这里主要说下,跳表写入节点,时间都耗在查找上面了。这里着重说下两个变量,update 数组存放的节点都是 forward 指向要插入的节点,rank 存放的都是 update 里节点距离要插入节点的跨度。

最后说下删除节点。

// t_zset.c

/* Internal function used by zslDelete, zslDeleteByScore and zslDeleteByRank */
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; /* 这里之所以减一,是因为 udpate[i] 虽然没有直接指向删除节点,但高度上超过了 */
        }
    }
    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--;
}

/* 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). */
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;
    }
    /* 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) {
        zslDeleteNode(zsl, x, update);
        if (!node)
            zslFreeNode(x);
        else
            *node = x;
        return 1;
    }
    return 0; /* not found */
}

/* Free the specified skiplist node. The referenced SDS string representation
 * of the element is freed too, unless node->ele is set to NULL before calling
 * this function. */
void zslFreeNode(zskiplistNode *node) {
    sdsfree(node->ele);
    zfree(node);
}

这里分三步:第一步,依据层级高度,遍历头结点,把指向删除节点的各层级节点放入 update 路径搜索数组;第二步,依据层级高度,遍历路径搜索数组,更新对应的 forward 和 span(因为下一步要释放删除节点所占内存),如要删除的节点是跳表的最大高度,则调整跳表高度;第三步,释放节点内存。

【注】 此博文中的 Redis 版本为 5.0。

参考书籍 :

【1】redis设计与实现(第二版)
【2】Redis 5设计与源码分析

你可能感兴趣的:(Redis)