Redis设计与实现(4-跳跃表)
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
Redis使用跳跃表作为有序集合键的底层实现之一:
- 有序集合包含的元素数量比较多。
- 有序集合中元素的成员(member)是比较长的字符串。
Redis只在两个地方用到了跳跃表:
- 实现有序集合键。
- 在集群节点中用作内部数据结构。
跳跃表实现
Redis的跳跃表由 redis.h/zskiplistNode 和 redis.h/zskiplist 两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,而zskiplist结构则用于保存跳跃表节点的相关信息。
跳跃表节点
跳跃表节点的实现由redis.h/zskiplistNode结构定义:
typedef struct zskiplistNode{ |
- 层
跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度。一般来说,层的数量越多,访问其他节点的速度就越快。
每次创建一个新跳跃表节点的时候,程序都根据幂次定律(power law,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的“高度”。
上图为不同层高节点
- 前进指针
每个层都有一个指向表尾方向的前进指针(level[i].forward属性),用于从表头向表尾方向访问节点。
上图用虚线表示出了程序从表头向表尾方向,遍历跳跃表中所有节点的路径
- 跨度
层的跨度(level[i].span属性)用于记录两个节点之间的距离。
- 两个节点之间的跨度越大,它们相距得就越远。
- 指向NULL的所有前进指针的跨度都为0,因为它们没有连向任何节点。
跨度是用来计算排位(rank)的:在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在跳跃表中的排位。
- 后退指针
后退指针用于从表尾向表头方向访问节点,跟可以一次跳过多个节点的前进指针不同,因为每个节点只有一个后退指针,所以每次只能后退至前一个节点。
- 分值和成员
节点的分值是一个double类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序。
节点的成员对象是一个指针,它指向一个字符串对象,而字符串对象则保存着一个SDS值。
在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值却可以是相同的。
分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象较小的节点会排在前面(靠近表头的方向),而成员对象较大的节点则会排在后面(靠近表尾的方向)。
跳跃表
仅靠多个跳跃表节点就可以组成一个跳跃表,但通过使用一个zskiplist结构来持有这些节点,程序可以更方便地对整个跳跃表进行处理,比如快速访问跳跃表的表头节点和表尾节点,或者快速地获取跳跃表节点的数量等信息。
zskiplist结构的定义如下:
typedef struct zskiplist{ |
表头节点不计算在内。
跳跃表API
跳跃表总结
- 跳跃表是有序集合的底层实现之一。
- Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而zskiplistNode则用于表示跳跃表节点。
- 每个跳跃表节点的层高都是1至32之间的随机数。
- 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。
- 跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象字典序的大小进行排序。