概念
跳跃表类似一个多层的链表,首先从最高层开始查找,如果下一个节点的值大于要查找的值或者下一个节点为null,则往下一层查找。通过空间换时间的策略,将时间复杂度控制在O(logn)。
图例
例如查找51这个数
首先从第一层开始查找,找到第二个节点,发现后面为null。
从第二层查找 查找到第四个节点,发现后面的节点为61,大于当前的数。
从第三层查找 查找到第六个节点 结束 一共查找四次,比遍历一次少了两次。数据量大的情况下,这个性能会提升的很明显。
核心概念
- 表头:负责维护跳跃表的节点指针。
- 跳跃表节点:保存着元素值,以及多个层。
- 层:保存着指向其他元素的指针。高层的指针越过的元素数量大于等于低层的指针,为了提高查找的效率,程序总是从高层先开始访问,然后随着元素值范围的缩小,慢慢降低层次。
- 表尾:全部由
NULL
组成,表示跳跃表的末尾。
基本结构
zskiplistNode
1 | typedef struct zskiplistNode { |
zskiplist
1 | typedef struct zskiplist { |
其中,头节点是跳跃表的一个特殊节点,它的level数组元素个数为64。头节点在有序集合中不存储任何member和score值,ele值为NULL, score值为0;也不计入跳跃表的总长度。头节点在初始化时,64个元素的forward都指向NULL, span值都为0。
创建跳跃表
zslCreate
1 | zskiplist *zslCreate(void) { |
ZSKIPLIST_MAXLEVEL
跳跃表最大等级(默认值为32),可以容纳2^64个元素- 为跳跃表创建了一个初始的头节点,并设置了一个32层高的索引
创建表节点
zslCreateNode
1 | /* Create a skiplist node with the specified number of levels. |
又是先创建节点再赋值
随机层高
zslRandomLevel
1 | int zslRandomLevel(void) { |
在首节点直接拉满设置了一个层高的最大值32,但是如果每一个元素的层高都是32跳跃表也就没意义了。这个方法就是确定节点的层高,返回一个1 ~ ZSKIPLIST_MAXLEVEL
的数。
插入节点
插入节点总的来说一共四步
- 查找插入位置
- 调整高度
- 插入节点
- 调整
backward
zslInsert
1 | zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) { |
删除节点
- 删除相比较会简单一些,如果插入或者查询时需要先查到删除节点的位置。
- 删除节点(其实就是一个链表删除元素)
- 是否为唯一的高节点,如果是则更新
level
,不是则无需额外处理
其他
为什么不使用红黑树
引用一下原作者的话
There are a few reasons: They are not very memory intensive. It’s up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees.
A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees.
They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.About the Append Only durability & speed, I don’t think it is a good idea to optimize Redis at cost of more code and more complexity for a use case that IMHO should be rare for the Redis target (fsync() at every command). Almost no one is using this feature even with ACID SQL databases, as the performance hint is big anyway.About threads: our experience shows that Redis is mostly I/O bound. I’m using threads to serve things from Virtual Memory. The long term solution to exploit all the cores, assuming your link is so fast that you can saturate a single core, is running multiple instances of Redis (no locks, almost fully scalable linearly with number of cores), and using the “Redis Cluster” solution that I plan to develop in the future.
- 简单总结一下:
- 这并不会浪费太多的空间,并且树的高度可以动态调整的。
- ZRANGE 和 ZREVRANGE命令,跳表性能比红黑树好
- 红黑树比较复杂…作者懒得实现
总结
- Redis使用跳跃表结构存储有序集合数据,通过概率平衡实现近似平衡p叉书的存取效率。
致谢
- 本文标题:Redis数据结构源码解析(三) 跳跃表
- 本文作者:SunRan
- 创建时间:2022-01-19 15:47:22
- 本文链接:https://lksun.cn/2022/01/19/Redis数据结构源码解析-三-跳跃表/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!