redis学习系列(六)--redis基础跳跃表的构造

本文介绍了Redis中使用的跳跃表数据结构,详细解释了其组成部分包括zskiplist和zskiplistNode,以及如何通过前进指针和跨度实现高效查找。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

看redis的时候第一次看见跳跃表这种数据结构,说实话,之前还真不知道有这么个结构,哎。。。。。。。。。。。


跳跃表其实也是一个链表,不过子啊实现上有点区别,也是由一个一个的Node组成,zskiplistNode是组成,zsklist则是保存了跳跃表节点的相关信息,例如节点的数量,头结点指针,尾节点指针。

digraph {    rankdir = LR;    node [shape = record, width = "0.5"];    //    l [label = " <header> header | <tail> tail | level \n 5 | length \n 3 "];    subgraph cluster_nodes {        style = invisible;        header [label = " <l32> L32 | ... | <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 "];        bw_null [label = "NULL", shape = plaintext];        level_null [label = "NULL", shape = plaintext];        A [label = " <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 1.0 | o1 "];        B [label = " <l2> L2 | <l1> L1 | <backward> BW | 2.0 | o2 "];        C [label = " <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 3.0 | o3 "];    }    subgraph cluster_nulls {        style = invisible;        n1 [label = "NULL", shape = plaintext];        n2 [label = "NULL", shape = plaintext];        n3 [label = "NULL", shape = plaintext];        n4 [label = "NULL", shape = plaintext];        n5 [label = "NULL", shape = plaintext];    }    //    l:header -> header;    l:tail -> C;    header:l32 -> level_null [label = "0"];    header:l5 -> C:l5 [label = "3"];    header:l4 -> A:l4 [label = "1"];    header:l3 -> A:l3 [label = "1"];    header:l2 -> A:l2 [label = "1"];    header:l1 -> A:l1 [label = "1"];    A:l4 -> C:l4 [label = "2"];    A:l3 -> C:l3 [label = "2"];    A:l2 -> B:l2 [label = "1"];    A:l1 -> B:l1 [label = "1"];    B:l2 -> C:l2 [label = "1"];    B:l1 -> C:l1 [label = "1"];    C:l5 -> n5 [label = "0"];    C:l4 -> n4 [label = "0"];    C:l3 -> n3 [label = "0"];    C:l2 -> n2 [label = "0"];    C:l1 -> n1 [label = "0"];    bw_null -> A:backward -> B:backward -> C:backward [dir = back];    label = "\n 图 5-1    一个跳跃表";}

zskiplist

最左边的就是zskiplist结构:

header: 指向表头节点

tail: 指向表尾节点

lever: 记录跳跃表内,层级最大的那个节点的层级(表头节点的层级不计算在内)

length:记录跳跃表的长度,就是跳跃表包含的节点的数量(表头节点不在计算内)

zskiplistNode

右边的是四个zskiplistNode结构:

层(lever):节点中用L1,L2,L3等字样标记各个层,L1代表第一层,L2代表第二层,每层有两个属性:前进指针和跨度

前进指针:用于访问表尾方向的其他节点

跨度:记录了前进指针所指向节点和当前节点的距离

具体看上图:连线上带有数字的箭头就代表前进指针,而数字就是跨度

程序在从表头遍历想表尾遍历时,访问会沿着层的前进指针进行

后退指针:节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点,因此它在从表尾向表头遍历时使用。

分值:节点中保存的值 1.0 ,2.0, 3.0

成员对象:保存的成员对象,o1,o2,o3



zskiplistNode的构造

typedef struct zskiplistNode {

    // 后退指针
    struct zskiplistNode *backward;

    // 分值
    double score;

    // 成员对象
    robj *obj;

    // 层
    struct zskiplistLevel {

        // 前进指针
        struct zskiplistNode *forward;

        // 跨度
        unsigned int span;

    } level[];

} zskiplistNode;

跳跃表节点的lever数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序就是通过这些指针实现快速访问

digraph {    label = "\n 图 5-2    带有不同层高的节点";    rankdir = LR;    //    node [shape = record];    n1 [label = " zskiplistNode | level[0] | backward | score | obj "];    n2 [label = " zskiplistNode | level[2] | level[1] | level[0] | backward | score | obj "];    n3 [label = " zskiplistNode | level[4] | level[3] | level[2] | level[1] | level[0] | backward | score | obj "];    //    edge [style = invis];    n1 -> n2 -> n3;}

前进指针:

每一层都会有一个前进指针,表名遍历的方向;

图5-3中用虚线表示出从表头向表尾的方向,遍历所有节点的路径。

  1.迭代程序首先访问跳跃表的第一个节点(表头),然后从L4的前进指针移动到表中的第二个节点

2.在第二个节点时,程序沿着第二层的前进指针移动到表中的第三个节点

3.在第三个节点时,程序同样沿着第二层的前进指针移动到表中的第四个节点。

4.当程序在第四个节点的前进指针移动时,会碰到一个null,此时已经到达了跳跃表的表尾,

digraph {    rankdir = LR;    node [shape = record, width = "0.5"];    //    l [label = " <header> header | <tail> tail | level \n 5 | length \n 3 "];    subgraph cluster_nodes {        style = invisible;        header [label = " <l32> L32 | ... | <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 "];        bw_null [label = "NULL", shape = plaintext];        level_null [label = "NULL", shape = plaintext];        A [label = " <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 1.0 | o1 "];        B [label = " <l2> L2 | <l1> L1 | <backward> BW | 2.0 | o2 "];        C [label = " <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 3.0 | o3 "];    }    subgraph cluster_nulls {        style = invisible;        n1 [label = "NULL", shape = plaintext];        n2 [label = "NULL", shape = plaintext];        n3 [label = "NULL", shape = plaintext];        n4 [label = "NULL", shape = plaintext];        n5 [label = "NULL", shape = plaintext];    }    //    l:header -> header [style = dashed];    l:tail -> C;    header:l32 -> level_null [label = "0"];    header:l5 -> C:l5 [label = "3"];    header:l4 -> A:l4 [label = "1", style = dashed];    header:l3 -> A:l3 [label = "1"];    header:l2 -> A:l2 [label = "1"];    header:l1 -> A:l1 [label = "1"];    A:l4 -> C:l4 [label = "2"];    A:l3 -> C:l3 [label = "2"];    A:l2 -> B:l2 [label = "1", style = dashed];    A:l1 -> B:l1 [label = "1"];    B:l2 -> C:l2 [label = "1", style = dashed];    B:l1 -> C:l1 [label = "1"];    C:l5 -> n5 [label = "0"];    C:l4 -> n4 [label = "0"];    C:l3 -> n3 [label = "0"];    C:l2 -> n2 [label = "0", style = dashed];    C:l1 -> n1 [label = "0"];    bw_null -> A:backward -> B:backward -> C:backward [dir = back];    label = "\n 图 5-3    遍历整个跳跃表";}

跨度

层的跨度用于记录两个节点之间的距离

1.两个节点之间的跨度越大,说明它们相隔的越远

2.指向null的所有前进指针的跨度为0,因为它们没有连上任何节点。

跨度是计算排位的: 排位是指在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是跳跃表中的排位。

下图例子中,虚线标注了在跳跃表中查找分值为3.0,成员对象为o3的节点时,沿途经历的层,由图可知只经历过一层,跨度为3

digraph {    rankdir = LR;    node [shape = record, width = "0.5"];    //    l [label = " <header> header | <tail> tail | level \n 5 | length \n 3 "];    subgraph cluster_nodes {        style = invisible;        header [label = " <l32> L32 | ... | <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 "];        bw_null [label = "NULL", shape = plaintext];        level_null [label = "NULL", shape = plaintext];        A [label = " <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 1.0 | o1 "];        B [label = " <l2> L2 | <l1> L1 | <backward> BW | 2.0 | o2 "];        C [label = " <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 3.0 | o3 "];    }    subgraph cluster_nulls {        style = invisible;        n1 [label = "NULL", shape = plaintext];        n2 [label = "NULL", shape = plaintext];        n3 [label = "NULL", shape = plaintext];        n4 [label = "NULL", shape = plaintext];        n5 [label = "NULL", shape = plaintext];    }    //    l:header -> header [style = dashed];    l:tail -> C;    header:l32 -> level_null [label = "0"];    header:l5 -> C:l5 [label = "3", style = dashed];    header:l4 -> A:l4 [label = "1"];    header:l3 -> A:l3 [label = "1"];    header:l2 -> A:l2 [label = "1"];    header:l1 -> A:l1 [label = "1"];    A:l4 -> C:l4 [label = "2"];    A:l3 -> C:l3 [label = "2"];    A:l2 -> B:l2 [label = "1"];    A:l1 -> B:l1 [label = "1"];    B:l2 -> C:l2 [label = "1"];    B:l1 -> C:l1 [label = "1"];    C:l5 -> n5 [label = "0"];    C:l4 -> n4 [label = "0"];    C:l3 -> n3 [label = "0"];    C:l2 -> n2 [label = "0"];    C:l1 -> n1 [label = "0"];    bw_null -> A:backward -> B:backward -> C:backward [dir = back];    label = "\n 图 5-4    计算节点的排位";}

后退指针

bw属性,用于表尾向表头访问节点时,与前进指针不同的是,每个节点只有一个后退指针,所以每次只能后退一个节点

图5-6表示的是从tail节点从表尾向表头遍历,直至遇到null节点,结束

digraph {    rankdir = LR;    node [shape = record, width = "0.5"];    //    l [label = " <header> header | <tail> tail | level \n 5 | length \n 3 "];    subgraph cluster_nodes {        style = invisible;        header [label = " <l32> L32 | ... | <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 "];        bw_null [label = "NULL", shape = plaintext];        level_null [label = "NULL", shape = plaintext];        A [label = " <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 1.0 | o1 "];        B [label = " <l2> L2 | <l1> L1 | <backward> BW | 2.0 | o2 "];        C [label = " <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 3.0 | o3 "];    }    subgraph cluster_nulls {        style = invisible;        n1 [label = "NULL", shape = plaintext];        n2 [label = "NULL", shape = plaintext];        n3 [label = "NULL", shape = plaintext];        n4 [label = "NULL", shape = plaintext];        n5 [label = "NULL", shape = plaintext];    }    //    l:header -> header;    l:tail -> C [style = dashed];    header:l32 -> level_null [label = "0"];    header:l5 -> C:l5 [label = "3"];    header:l4 -> A:l4 [label = "1"];    header:l3 -> A:l3 [label = "1"];    header:l2 -> A:l2 [label = "1"];    header:l1 -> A:l1 [label = "1"];    A:l4 -> C:l4 [label = "2"];    A:l3 -> C:l3 [label = "2"];    A:l2 -> B:l2 [label = "1"];    A:l1 -> B:l1 [label = "1"];    B:l2 -> C:l2 [label = "1"];    B:l1 -> C:l1 [label = "1"];    C:l5 -> n5 [label = "0"];    C:l4 -> n4 [label = "0"];    C:l3 -> n3 [label = "0"];    C:l2 -> n2 [label = "0"];    C:l1 -> n1 [label = "0"];    bw_null -> A:backward -> B:backward -> C:backward [dir = back, style = dashed];    label = "\n 图 5-6    从表尾向表头方向遍历跳跃表";}

分值和成员

节点的分值都是按照分值大小从小到大来排序

节点的成员对象是一个指针,指向一个字符串对象,而字符串保存着一个SDS值。


在同一个跳跃表中,保存的成员对象是唯一的,但是分值确实可以相同的,分值相同的节点将按照在字典序中来排序,成员对象小的节点排在前面。

digraph {    rankdir = LR;    node [shape = record, width = "0.5"];    //    l [label = " <header> header | <tail> tail | level \n 5 | length \n 3 "];    subgraph cluster_nodes {        style = invisible;        header [label = " <l32> L32 | ... | <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 "];        bw_null [label = "NULL", shape = plaintext];        level_null [label = "NULL", shape = plaintext];        A [label = " <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 10086.0 | o1 "];        B [label = " <l2> L2 | <l1> L1 | <backward> BW | 10086.0 | o2 "];        C [label = " <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 10086.0 | o3 "];    }    subgraph cluster_nulls {        style = invisible;        n1 [label = "NULL", shape = plaintext];        n2 [label = "NULL", shape = plaintext];        n3 [label = "NULL", shape = plaintext];        n4 [label = "NULL", shape = plaintext];        n5 [label = "NULL", shape = plaintext];    }    //    l:header -> header;    l:tail -> C;    header:l32 -> level_null [label = "0"];    header:l5 -> C:l5 [label = "3"];    header:l4 -> A:l4 [label = "1"];    header:l3 -> A:l3 [label = "1"];    header:l2 -> A:l2 [label = "1"];    header:l1 -> A:l1 [label = "1"];    A:l4 -> C:l4 [label = "2"];    A:l3 -> C:l3 [label = "2"];    A:l2 -> B:l2 [label = "1"];    A:l1 -> B:l1 [label = "1"];    B:l2 -> C:l2 [label = "1"];    B:l1 -> C:l1 [label = "1"];    C:l5 -> n5 [label = "0"];    C:l4 -> n4 [label = "0"];    C:l3 -> n3 [label = "0"];    C:l2 -> n2 [label = "0"];    C:l1 -> n1 [label = "0"];    bw_null -> A:backward -> B:backward -> C:backward [dir = back];    label = "\n 图 5-7    三个带有相同分值的跳跃表节点";}


zskiplist 结构的定义如下:

typedef struct zskiplist {

    // 表头节点和表尾节点
    struct zskiplistNode *header, *tail;

    // 表中节点的数量
    unsigned long length;

    // 表中层数最大的节点的层数
    int level;

} zskiplist
头指针和尾指针定位表头和表尾,时间复杂度为O(1)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值