[TOC]
基于 Redis 6.2.1
参考资料:
https://www.jianshu.com/p/9d8296562806
画图工具是真的难用啊 , 找不到好用的工具。。。
跳表 跳表(skiplist)是一种有序的数据结构,它通过在每个节点中维护多个指向其他节点的指针,从而达到快速访问的目的。
跳跃表支持平均O ( l o g N ) O(log N) O ( l o g N ) , 最坏O ( N ) O(N) O ( N ) 复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
跳表的数据结构 跳表既然是叫什么什么表,那么本质上还是链表。下面先看一下普通单链表的结构:
图片graphviz源码 展开/收起 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 digraph link { rankdir = LR; //让图片横过来 node[shape = record]; //record形状是专门用来做类似”结构体“的东西的 1[label = "{1|}"];//每个的'|'都是一列 2[label = "{2|}"]; 3[label = "{3|}"]; 4[label = "{4|}"]; 5[label = "{5|}"]; 6[label = "{6|}"]; 7[label = "{7|}"]; 8[label = "{8|NULL}"]; 1->2:w; 2->3:w; 3->4:w; 4->5:w; 5->6:w; 6->7:w; 7->8:w; }
对于一个单链表只能依次遍历,但跳表存储的是有序的节点。
众所周知,利用有序这个单调性是可以二分的,因此可以优化查找操作。二分需要不断的折半,也就是找当前操作区间的正中间的元素,递归下去直到这个区间长度变为1。其实二叉查找树(BST,Binary Search Tree)就是这么玩儿的。像下图这样:
图片graphviz源码 展开/收起 1 2 3 4 5 6 7 8 9 digraph G{ graph [ordering="out"]; 4 -> 2; 4 -> 6; 2 -> 1; 2 -> 3; 6 -> 5; 6 -> 7; }
这里看起来链表和二叉树差别很大,链表相当于只能存二叉树叶子结点的内容,对于非叶子结点无能为力了。但办法还是有的,上图是一个无重复结点的二叉查找树。稍微换个样子就可以了。如下图:
图片graphviz源码 展开/收起 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 digraph G{ graph [ordering="out"]; l11[label = "1"]; l21[label = "1"]; l31[label = "1"]; l41[label = "1"]; l25[label = "5"]; l33[label = "3"]; l35[label = "5"]; l42[label = "2"]; l43[label = "3"]; l44[label = "4"]; l45[label = "5"]; l46[label = "6"]; l47[label = "7"]; l11->l21; l11->l25; l21->l31; l21->l33; l25->l35; l25->l47; l31->l41; l31->l42; l33->l43; l33->l44; l35->l45; l35->l46; }
当该二叉查找树维护的序列只在叶子时就差不多一样了。跳表结构和该二叉查找树一样采用冗余节点存储二分查找中间值的时候,就能够实现O ( l o g N ) O(log N) O ( l o g N ) 的查找效率了。
现在可以看下跳表的真正模样了。
图片graphviz源码 展开/收起 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 digraph link { rankdir = LR; //让图片横过来 node[shape = record];//record形状是专门用来做类似”结构体“的东西的 1[label = "{1|}"];//每个的'|'都是一列 2[label = "{2|}"]; 3[label = "{3|}"]; 4[label = "{4|}"]; 5[label = "{5|}"]; 6[label = "{6|}"]; 7[label = "{7|}"]; 8[label = "{8|NULL}"]; 1->2:w; 2->3:w; 3->4:w; 4->5:w; 5->6:w; 6->7:w; 7->8:w; }
跳表的增删改查 随机函数 1 2 3 4 5 6 7 8 9 10 int zslRandomLevel (void ) { int level = 1 ; while ((random()&0xFFFF ) < (ZSKIPLIST_P * 0xFFFF )) level += 1 ; return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL; }
跳表的优势 Redis 中跳表的实现 跳表节点在 server.h
文件中:
1 2 3 4 5 6 7 8 9 typedef struct zskiplistNode { sds ele; double score; struct zskiplistNode *backward ; struct zskiplistLevel { struct zskiplistNode *forward ; unsigned long span; } level[]; } zskiplistNode;
ele
元素的值,采用sds
类型。(老版本这里用的robj
,后面统一改成了sds
)score
分数,浮点型*backward
后退指针,从表尾向前遍历,这里只能一个一个遍历。zskiplistLevel
跳表层级*forward
前进指针,它指向的下一个节点。span
跨度, 到它指向的下一个节点的距离。跳表结构体:
1 2 3 4 5 typedef struct zskiplist { struct zskiplistNode *header , *tail ; unsigned long length; int level; } zskiplist;
*header
跳表头节点。*tail
跳表尾节点。length
跳表的节点数量level
最大节点的层级数。看到这里又个疑惑,Redis
用跳表实现的Sorted Set
, 那么 ZSCORE
命令啥的查找岂不是要O ( N ) O(N) O ( N ) 了?
到了后面看代码才知道,每个Redis
的上层数据类型(String
, Hash
, Set
, List
,Sorted Set
)等的实现都是多个底层数据结构复合而成的,比如Sorted Set
在数量多了的时候就是用了skiplist
和dict
一起实现的。
1 2 3 4 typedef struct zset { dict *dict; zskiplist *zsl; } zset;