• Redis 跳跃表


    跳跃表

    咱们可以想象一个场景,我们需要维护一个有序的集合,集合是按照某个字段进行排序的,集合会有添加和删除操作。

    如何在内存中存储存储并维护这样的集合呢,我们可以想到链表或者数组进行存储,他们都是线性的结构,我们分别分析下这两种接口存储有序集合的场景。

    • 数组

    使用数组维护集合,当插入一个元素时,首先需要找到插入位置,通过二分查找时间复杂度为O(logn),找到插入位置后,需要将后面的元素都移动一位,时间复杂度为O(n)。

    两个操作时间复杂度为O(n)。

    • 链表

    使用链表维护集合,当插入一个元素时,首先需要找到插入位置,链表不能二分查找,所以遍历链表直到找到元素时间复杂的为O(n),找到后插入元素复杂度为O(1)。

    两个操作时间复杂度为O(n)。

    当集合数据量很大时,会对性能产生影响。

    我们可以观察到,链表这种结构删除或者新增一个节点复杂度都为O(n),而找某个节点需要遍历,复杂度为O(n),我们能不能想办法把找某个节点的复杂度优化一下呢?

    跳跃表 就是优化了查找某个节点的复杂度。

    例如有如下的链表:

     我们找某个节点需要遍历链表,时间复杂度为O(n)。我们在这个链表之上再维护一个链表,为下面的偶数节点列表。

    例如下图链表:

     假如我要插入元素4,我遍历上面基数链表,定位到 3 和 5 之间,再返回到原来的链表定位,还是定位到 3 和 5 之间。

    数据量少不够明显,可以想象下如果数据量大,查找次数将减少一半。这就是跳跃表的思想。

    当然需要额外开辟空间来维护上面的链表,典型的空间换时间策略。

    再数据量足够大的情况下我们还可以添加索引,直到同一层节点只有两个元素(只有一个没有比较的意义),这种多层链表结构就是跳跃表。

    跳跃表新增节点:

    1. 新节点和各层索引节点逐一比较,确定原链表的插入位置。O(logn)
    2. 把索引插入到原链表。O(1)
    3. 利用抛硬币的随机方式,决定新节点是否提升为上一级索引。结果为“正”则提升并继续抛硬币,结果为“负”则停止。O(logn)

    总体跳跃表时间复杂度为O(logn),空间复杂度为O(n)。

    条约表删除节点:

    1. 自上而下,查找第一次出现节点的索引,并逐层找到每一层对应的节点。O(logn)
    2. 删除每一层查找到的节点,如果该层只剩下1个节点,删除整个一层(原链表除外)。O(logn)

    总体跳跃表删除操作的时间复杂度是O(logn)。

    Redis跳跃表

    Redis有序集合(zset)底层是由 ziplist 和 Redis 跳跃表两种方式实现的。

    满足一下条件使用 ziplist:

    • 有序集合保存的元素数量小于128个
    • 有序集合保存的所有元素的长度小于64字节

    接下来我们讨论Redis的跳跃表是如何实现的。

    Redis跳跃表的实现

    Redis 跳跃表由 redis.h/zskiplistNode 和 redis.h/zskiplist 两个结构定义。

    其中 zskiplistNode 结构用于表示跳跃表节点, 而 zskiplist 结构则用于保存跳跃表节点的相关信息。

    可参考如下图:

    跳跃表节点

    跳跃表节点的实现由 redis.h/zskiplistNode 结构定义:

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

    跳跃表节点的 level 数组可以包含多个元素, 每个元素都包含一个指向其他节点的指针, 程序可以通过这些层来加快访问其他节点的速度。

    • 前进指针

    每个层都有一个指向表尾方向的前进指针(level[i].forward 属性), 用于从表头向表尾方向访问节点。

    • 跨度

    层的跨度(level[i].span 属性)用于记录两个节点之间的距离, 两个节点之间的跨度越大, 它们相距得就越远。

    指向 NULL 的所有前进指针的跨度都为 0 , 因为它们没有连向任何节点。

    • 后退指针

    节点的后退指针(backward 属性)用于从表尾向表头方向访问节点: 跟可以一次跳过多个节点的前进指针不同, 因为每个节点只有一个后退指针, 所以每次只能后退至前一个节点。

    • 分值

    节点的分值(score 属性)是一个 double 类型的浮点数, 跳跃表中的所有节点都按分值从小到大来排序。

    • 成员

    节点的成员对象(obj 属性)是一个指针, 它指向一个字符串对象, 而字符串对象则保存着一个 SDS 值。

     在同一个跳跃表中, 各个节点保存的成员对象必须是唯一的, 但是多个节点保存的分值却可以是相同的。

    分值相同的节点将按照成员对象在字典序中的大小来进行排序, 成员对象较小的节点会排在前面(靠近表头的方向), 而成员对象较大的节点则会排在后面(靠近表尾的方向)。

    跳跃表

    zskiplist 结构的定义如下:

    typedef struct zskiplist {
    
        // 表头节点和表尾节点
        struct zskiplistNode *header, *tail;
    
        // 表中节点的数量
        unsigned long length;
    
        // 表中层数最大的节点的层数
        int level;
    
    } zskiplist;
    • header 和 tail 指针分别指向跳跃表的表头和表尾节点, 通过这两个指针, 程序定位表头节点和表尾节点的复杂度为 O(1) 。
    • 通过使用 length 属性来记录节点的数量, 程序可以在 O(1) 复杂度内返回跳跃表的长度。
    • level 属性则用于在 O(1) 复杂度内获取跳跃表中层高最大的那个节点的层数量, 注意表头节点的层高并不计算在内。

    ZSet在内存中的结构

    zset 的结构定义如下:

    /*
     * 有序集合
     */
    typedef struct zset {
    
        // 字典,键为成员,值为分值
        // 用于支持 O(1) 复杂度的按成员取分值操作
        dict *dict;
    
        // 跳跃表,按分值排序成员
        // 用于支持平均复杂度为 O(log N) 的按分值定位成员操作
        // 以及范围操作
        zskiplist *zsl;
    
    } zset;

    有了上面的内容,我们可以看看zset是怎么利用跳跃表进行存储数据的。

    例如,我们设置 这样的一个zset:

    key = language,value=(1-java,2-go,3-php)

    那么上述zset,对应下图:

     

     参口文献

    https://zhuanlan.zhihu.com/p/53975333

    Redis 设计与实现第二版

  • 相关阅读:
    vue项目锚点定位+滚动定位
    elementUI 弹出框添加可自定义拖拽和拉伸功能,并处理边界问题
    密码检验规则(字母数字和特殊字符组成的混合体)
    分布式版本控制系统git
    自动生成滚动条
    jq中append(),appendTo(),after(),before(),prepend(),prependTo()的用法
    清除浮动的几种方式
    王者荣耀周年福利活动绕过微信屏蔽
    看不懂源码?先来恶补一波Object原型吧
    Vue组件化开发
  • 原文地址:https://www.cnblogs.com/hulunbao/p/13963881.html
Copyright © 2020-2023  润新知