跳跃表是一种有序数据结构,它通过在每个节点维持多个指向其他节点的指针,从而达到快速访问节点的目的
Redis使用跳跃表作为有序集合键的底层实现之一。
如果一个有序集合包含的元素较多或者有序集合中元素的成员是较长的字符串时,Redis就会使用跳跃表作为有序集合键的底层实现。
Redis只在两个地方使用了跳跃表:
- 实现有序集合键
- 集群节点中用作内部数据结构
跳跃表的实现
Redis的跳跃表由redis.h
中的zskiplistNode
和zskiplist
两个结构定义。
zskiplistNode
表示跳跃表节点,zskiplist
保存跳跃表节点的相关信息,比如节点的数量,以及指向表头节点和表尾节点的指针等等。
跳跃表节点
由redis.h
中的zskiplistNode
结构定义
typedef struct zskiplistNode {
// 层:节点中的各个层,每个层都带有前进指针和跨度两个属性。
struct zskiplistLevel {
// 前进指针:用于访问位于表尾方向的其他节点
struct zskiplistNode *forward;
// 跨度:记录前进指针指向的节点和当前节点的距离
unsigned int span;
} level[];
// 后退指针:指向位于当前节点的前一个节点
struct zskiplistNode *backward;
// 分值:节点保存的分值,跳跃表中节点按照各自保存的分值从小到大排列
double score;
// 成员对象:节点所保存的成员对象
robj *robj;
} zskiplistNode;
层
包含多个元素,每个元素包含一个指向其他节点的指针。一般来说,层的数量越多,访问其他节点的速度越快。
每次创建一个新跳跃表节点的时候,程序都根据幂次定律随机生成一个介于1-32的值作为level数组的大小,这个大小即层的“高度”
前进指针
每个层都有一个前进指针,用于从表头向表尾方向访问节点
跨度
层的跨度用于记录两个节点之间的距离
- 跨度越大,相距越远
- 指向NULL的所有前进指针的跨度都为0,因为它们没有连向任何节点
跨度是用来计算排位(Rank)的:查找某个节点的过程中,将途经的所有层的跨度累计起来就是目标节点在跳跃表中的排位。
后退指针
用于从表尾向表头方向访问节点。每次只能后退至前一个节点
分值和成员
跳跃表中的所有节点都按分值从小到大排序
成员对象是一个指针,指向一个字符串对象,而字符串对象则保存着一个SDS值
同一个跳跃表中,各节点保存的成员对象必须是唯一的,但是多个节点保存的分值可以是相同的
跳跃表
仅靠多个跳跃表节点就可以组成一个跳跃表,但是Redis通过一个zskiplist结构来持有这些节点,可以更方便地对整个跳跃表进行处理。
typedef struct zskiplist {
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数(不包括表头)
int level;
} zskiplist;