深入理解Redis—跳跃表

1,545 阅读4分钟

写在前

本系列文章已被收录到专栏中,如果各位看官有兴趣,可以移步深入理解Redis专栏


正文

话说,小张去参加了一场面试。。

面试官:小张我看你表现的不错啊,最后一个问题,回答的让我满意了我就让你过😎😎

小张:瑟瑟发抖.jpg

面试官:现在给你个场景题:有个某博大V,有1000w粉丝,我有时候会去按照用户id等等排序,你去设计一个数据结构维护这些数据。

小张:这当然是要用Redis中的zset存储了、😁

面试官:那你给我讲讲为啥🙄🙄

小张:行勒,

skipList,跳跃表, 是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。 跳表就是在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。

我们可以先思考一下数组和链表的优缺点:

  • 数组查找可以用二分查找,时间复杂度O(logN),但是插入元素会导致后边的元素集体向后移动。
  • 单链表查找只能从头节点向后查找,时间复杂度O(N),但是插入元素只需要改变前序节点的指针即可。

单向链表

基于上述特点,人们提出了一个跳跃表的概念,节点分层,上层节点之间有跨度span,每个节点都有指向前边一个节点的指针backword(头节点和第一个节点除外)

跳跃表

面试官:(这小子有点东西)行了,那你给我讲讲他的实现细节吧。

小张:(这面试官真能抠,说着一道代码甩出 (′д` )…彡…彡),我们先从数据结构入手,跳表的数据结构相较而言比较简单,容易实现,在Redis源码中有写道:

/* 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[]; // 这是node的一个属性,代表这个node有几层,每一层都有详细的forward、span
} zskiplistNode;

这是跳表node的结构体,跟我们普通的node差不多,其中要额外注意的是:

  • backword:只能指向当前节点最底层的前一个节点,头节点和第一个节点的backword指向NULL,从后向前遍历跳跃表时使用。
  • level[]是一个柔性数组,在生成节点的时候会调用一个方法获取他的长度,这个我们之后再说。

node节点

除此之外,还需要定义一个跳跃表的结构体:

typedef struct zskiplist {
    struct zskiplistNode *header, *tail; // 首尾指针
    unsigned long length; // 跳表长度
    int level; // 最大等级
} zskiplist;

我们可以看到在O(1)的时间复杂度下,能够快速获取到跳跃表的头节点、尾节点、长度和高度。

跳表结构

一个个节点连接起来,就能构成我们前边的那个完整的跳跃表,这样子查找元素先从最高level查询,总的时间复杂度就会是O(logN)。

面试官:不错,那么说一说跳跃表节点level层高是怎么计算的吧

小张: Redis中设置了最底层高是1,最大值是一个常量**ZSKIPLIST_MAXLEVEL,这个值在Redis 5以前是32**,Redis 5的时候是64

Redis 6又改为了32。

为啥一直变呢?我在这个里找到了答案 github.com/redis/redis…

然后我们来看一下层数计算的方法:

/* 为我们将要创建的新跳过列表节点返回一个随机级别。
* 此函数的返回值介于 1 和 ZSKIPLIST_MAXLEVEL
*(包括两者)之间,具有类似幂律的分布,其中
* 级别越高,返回的可能性就越小。 */
int zslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))  // ZSKIPLIST_P=0.25
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

要注意的是 ZSKIPLIST_P表示的意思是当前节点出现在下一层的概率,代码中设置的是1/4

level的初始值为1,通过while循环,每次生成一个随机值,取这个值的低16位作为x,当x小于0.25倍的0xFFFF时,level的值加1;否则退出while循环。最终返回level和ZSKIPLIST_MAXLEVEL两者中的最小值。

面试官:我来抠一抠,跳跃表的首节点有什么特点?

小张:跳跃表的首节点是一个空节点,看代码最后创建了一个 满层数,score=0,ele=null的节点,用来管理各个层数的节点。

/* Create a new skiplist. */
zskiplist *zslCreate(void) {
    zskiplist *zsl;
...
    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);  // 设置头节点,这里把MAXLEVEL传进去了
...
}
zskiplistNode *zslCreateNode(int level, double score, sds ele) {
    zskiplistNode *zn =
        zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
    zn->score = score; // 0,跳跃表中保证score都是整数
    zn->ele = ele; // null
    return zn;
}

面试官:(这小子基础竟然如此扎实),我再问你最后一个问题:zset底层除了跳跃表实现还有什么??

小张:zset还有另一种实现叫做压缩列表,在插入第一个元素的时候,redis会进行判断

if (server.zset_max_ziplist_entries == 0 ||
            server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr)) // 插入的字符串长度
        {
            zobj = createZsetObject(); // 跳跃表
        } else {
            zobj = createZsetZiplistObject();  // 压缩列表
        }

当然,当zset中元素个数大于zset_max_ziplist_entries,或者插入的字符串长度大于zset_max_ziplist_value的时候会转换成跳跃表,此操作不可逆

面试官:小伙子可以的,期待接下来的面试。

小张:太难了,差点就说不上来了,我得回去补一补,争取吊打面试官。

Over~


参考资料


end~ 🌹🌹🌹🌹