Redis的跳跃表
在Redis中,有一种高效的数据结构叫做有序集合(zset
)
它是一种集合,其中每个成员(member
)都会关联一个分数(score
)。
zset
既可以快速地通过member
找到该成员对应的分数,又可以按照分数的大小进行范围查询,这对于实现排行榜等功能非常有用。那么zset
是如何实现这样的功能的呢?答案是跳跃表(skiplist
)。
1. zset
的底层实现
思考这么几个问题:
zset
是如何快速的根据成员取得对应的分数?zset
是如何快速的取得指定排行区间的所有成员和分数?zset
的zrevrange
(逆序)又是如何实现的?zset
如何计算一个成员的排位?- …
第一个问题我们很容易想到使用哈希表,实际上zset
的底层实现,就是一个哈希表+跳跃表,看看结构:(对于元素数量少的情况,zset
会使用另一种实现方式,我们这里不作讨论)
1 |
|
哈希表(dict
)大部分人都了解,那么对于这个跳跃表(zskiplist
),又是怎样的结构呢?
2.跳跃表的结构和特性
跳跃表是一种有序链表+多层索引的结构,使得包含n个元素的有序序列的查找和插入操作的平均时间复杂度都是 。
跳跃表是按层建造的,最底层是一个普通的有序链表。
众所周知,如果是一个有序的数组,我们可以通过二分查找,很快的找到其中的某个元素。那么对于一个有序的链表,我们有没有办法也使用二分查找呢?
跳跃表的想法就是用空间换时间:在最底层的原始链表上面,加多一层索引,元素数量为原始链表的一半,如图所示:
这样查找的时候,原本要比较n个元素,通过使用索引,减少到 。
但是这样还不够,所以我们在上面再多加几层索引,如图所示:
理论上,对于元素数量为n的链表,我们可以建立 层索引。
这样,我们就能通过从最高层开始,通过二分查找的方式,快速的查找某个数值在链表中的位置。
3.实现细节
Redis的跳跃表实现主要有两个结构:zskiplistNode
和zskiplist
,其中zskiplistNode
是跳跃表的结点,而zskiplist
则记录整个表的一些信息,如结点的数量等。
上图展示了一个跳跃表的实例。左边代表着跳跃表的整体结构:zskiplist
zskiplist
的代码如下:
1 |
|
header
和tail
:分别指向跳跃表头结点和尾结点,其中头结点在zskiplist
创建时生成,并不实际存储数据。
length
:跳跃表的长度,通过这个变量,可以在O(1)时间内获取长度。
level
:当前跳跃表中最高的层,不包括头结点。
每次创建新结点的时候,
redis
根据幂次定律
(越大的数出现概率越小)随机生成一个高度(介于1和32),作为当前结点的level
数组的长度,也就是这个结点的高度。
结点zskiplistNode
的代码如下:
1 |
|
ele
:每个结点的成员,sds
是redis
内置的简单动态字符串结构(Simple Dynamic String
)
score
:每个结点的分数,是double
类型
backward
:回退指针,用于从表尾向表头遍历。用来实现zrevrange
。
level
:每个层级上的索引。
注意level
中的每一层,除了指向下一个结点的指针以外,还有一个span
表示跨度,即下一个结点和当前结点的距离。(示例图中,箭头上面的数字)
这个跨度用来计算排行,由于redis
的跳跃表并不是严格的按照每层数量减半的实现,所以在高层,每一次跨越几个原始结点,只能存储在这个span
中,而在遍历的时候,通过计算跨度的和,就可以知道当前排在第几位了。
上图中红色和绿色都是
zskiplistNode
结构,其中header
的backward
/ele
/score
都是没用的,所以省略了没画出来
4 与其他结构的对比
和红黑树这类平衡树结构相比,跳跃表的算法复杂度一样,而主要优势在于实现简单,且在并发环境下更加高效;
与哈希表相比,跳跃表支持有序数据的快速检索和范围查询,但牺牲了一些插入和删除操作的性能。
总的来说,跳跃表是一种空间换时间,以及用随机化处理平衡的高效数据结构。