Redis Sorted Set 跳表的实现原理与分析
哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛
今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。
我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!
前言
在 Redis 中,Sorted Set(有序集合) 是一种非常重要且高效的数据结构。与普通的 Set 不同,Sorted Set 为每个元素关联了一个分数(score),并按照这个分数进行排序。在许多需要快速插入、删除、查找和范围查询的场景下,Sorted Set 提供了理想的解决方案。而其底层的核心数据结构之一便是 跳表(Skip List),这种结构有效地支持了有序集合的高效操作。
本文将深入剖析跳表的实现原理,并通过 Redis 中 Sorted Set 的具体案例进行分析,帮助你更好地理解 Redis Sorted Set 的强大性能来源。
什么是跳表?
跳表(Skip List)是一种 平衡数据结构,它通过多级索引加快了查找操作的效率,最早由 William Pugh 在 1990 年提出。它的时间复杂度与平衡二叉树类似,均为 O(log n),但跳表的实现相对简单,操作灵活。
跳表的基本思想是通过在链表基础上建立多层索引,提升数据的查找效率。每一层都是下一层的 “缩略图”,这样在查找时,能通过跳跃方式快速逼近目标元素。
跳表的结构
跳表的结构类似于多层链表:
- 第一层 是完整的有序链表,所有数据都在这一层。
- 第二层及以上 则是对部分数据的抽象,使得每一层的数据量逐步减少,但依然保持有序。
每个元素会以一定概率决定是否提升到上层。因此,跳表有多条指向不同节点的指针,最低层类似普通链表,而上层指针可以跨越多个节点。
跳表的时间复杂度
在最优情况下,跳表通过减少层数并允许跳跃式查找,平均查找、插入和删除的时间复杂度都为 O(log n),空间复杂度为 O(n)。这种特性使得跳表非常适合用于实现有序集合,尤其是像 Redis 这样要求高效范围查询的场景。
Redis 中的 Sorted Set
Redis 的 Sorted Set 是通过两个核心数据结构实现的:
- 跳表(Skip List):用于按照分数(score)排序元素,支持按顺序查找、范围查找等操作。
- 哈希表:用于根据元素的唯一标识符(member)快速定位元素,避免重复插入。
Redis 通过这两种数据结构的结合,实现了有序集合的高效性和灵活性。
Sorted Set 的存储结构
Redis Sorted Set 的存储结构是 zset
,它内部由一个 dict
和一个 zskiplist
组成:
dict
:保存成员和对应分数的映射关系,确保成员唯一性,插入和删除操作时间复杂度为 O(1)。zskiplist
:用于按分数排序的跳表,支持按分数范围查找、范围删除等操作,时间复杂度为 O(log n)。
这种设计确保了插入、删除和范围查询等操作在高效性和稳定性上的平衡。
跳表在 Redis Sorted Set 中的实现
1. 跳表节点(zskiplistNode
)
在 Redis 中,每个跳表节点不仅包含元素的分数和成员信息,还包含多个指向其他节点的指针,用于支持多层索引的实现。Redis 中 zskiplistNode
的定义如下:
typedef struct zskiplistNode {
sds ele; // 成员(member)
double score; // 分数(score)
struct zskiplistNode *backward; // 后退指针,用于反向遍历
struct zskiplistLevel {
struct zskiplistNode *forward; // 前进指针,指向下一节点
unsigned int span; // 跨度,用于快速定位节点
} level[]; // 每个节点的多层指针
} zskiplistNode;
ele
:保存成员数据。score
:保存对应成员的分数。backward
:用于反向遍历,支持双向链表的功能。level
:是一个数组,每个元素对应一层,保存指向下一个节点的指针。
2. 跳表结构(zskiplist
)
Redis 中的 zskiplist
结构表示整个跳表,它由头节点、尾节点、最大层数和节点总数组成:
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 跳表头和尾节点
unsigned long length; // 跳表中的节点数量
int level; // 跳表的当前最大层数
} zskiplist;
3. 插入操作
插入新元素时,跳表通过从高层向低层逐层查找插入位置,并将新节点插入到相应层级的链表中。每插入一个新节点时,会随机决定它提升到哪一层,这种随机性保证了跳表的平衡性。
Redis 使用的随机层数生成算法如下:
int zslRandomLevel(void) {
int level = 1;
while ((random() & 0xFFFF) < (0.25 * 0xFFFF)) {
level += 1;
}
return (level < ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
zslRandomLevel
函数根据概率随机生成节点的层数,使得跳表在理论上能够保持 log 级别的复杂度。
插入示例:
假设我们向 Redis 中的 Sorted Set 插入一个元素:
ZADD myzset 1 "apple"
- Redis 首先通过随机算法为该元素分配一个层数。
- 然后,从最高层逐步查找合适的插入点,依次更新前后节点的指针,完成插入。
4. 查找操作
跳表的查找操作通过从上到下逐层遍历实现。每一层跳过若干节点进行快速查找,直到在最低层找到目标元素。由于跳表的层级设计,查找操作的时间复杂度为 O(log n)。
查找示例:
例如,我们要查询分数为 3 的元素:
ZRANGE myzset 3 3
跳表将从最高层开始,跳过一部分节点,快速找到分数为 3 的节点。
5. 删除操作
删除元素时,跳表会先找到目标元素所在的位置,然后从每一层中移除节点。为了保持数据结构的平衡,删除时会调整各层级指针,并在必要时降低跳表的层数。
删除示例:
假设我们删除 myzset
中分数为 2 的元素:
ZREM myzset "banana"
跳表会从头节点开始,逐层找到 banana
所在的位置,并移除该元素,同时更新前后节点的指针关系。
Redis 跳表的优势与局限
优势
- 高效的范围查询:跳表通过分层索引,可以快速完成范围查找、排名等操作。
- 插入、删除操作高效:跳表保持了 O(log n) 的插入和删除复杂度,适合大规模数据的动态增删。
- 实现简单:相比平衡树,跳表结构相对简单,更易于实现和维护。
局限
- 内存占用:跳表需要为每个节点维护多层指针,在内存占用上相对链表较大。
- 随机性导致的性能波动:由于跳表使用随机算法决定节点层数,可能导致性能出现波动,虽然这种概率极低。
总结
Redis 中的跳表作为 Sorted Set 的核心数据结构,提供了高效的排序、插入、删除以及范围查找等功能。通过跳表的多层索引机制,Redis 在处理有序集合时能够维持 O(log n) 的时间复杂度,同时结合哈希表确保了成员的唯一性和快速访问。
跳表的设计简单却强大,是 Redis 高性能的关键之一。在理解了其实现原理后,我们可以更好地运用 Redis 的 Sorted Set,优化高并发环境中的数据查询与操作。
如果你在构建需要排序、范围查询或动态调整的数据系统时,Redis 的 Sorted Set 和跳表的组合无疑是一个值得选择的高效解决方案。
… …
文末
好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。
… …
学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!
wished for you successed !!!
⭐️若喜欢我,就请关注我叭。
⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。