基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

Redis 有序集合(ZSET)为何选用跳表

知识点图片

Redis有序集合使用跳表,因其实现比红黑树更简单,易于维护。同时,它提供了接近的 O(log N) 高性能,且对范围查询十分高效,是性能与实现复杂度的最佳平衡。

这是一个非常经典且重要的问题。Redis 选择使用跳表(Skip List)来实现有序集合(Sorted Set / ZSET),而不是更常见的平衡树(如红黑树),是基于多方面权衡的结果。

简单来说,核心原因在于:跳表在实现简单性、性能表现和易于扩展等方面,提供了一个优于平衡树的综合方案。

下面我们从几个维度来详细拆解这个原因。

1. 什么是 Redis 的有序集合(ZSET)?

首先,我们要明白 ZSET 的需求是什么。它是一个集合,需要保证元素的唯一性,同时每个元素都有一个与之关联的浮点数分数(score)。ZSET 的核心功能是根据这个分数进行排序。

它需要高效地支持以下操作:

  • 添加/更新元素 (ZADD): O(log N)
  • 删除元素 (ZREM): O(log N)
  • 通过成员查找分数 (ZSCORE): O(1)
  • 通过排名范围查找成员 (ZRANGE): O(log N + M),其中 M 是返回的元素数量
  • 通过分数范围查找成员 (ZRANGEBYSCORE): O(log N + M)
  • 获取成员的排名 (ZRANK): O(log N)

2. Redis 的实际实现:跳表 + 哈希表

在深入比较之前,必须了解 Redis 的 ZSET 并非单独由跳表实现。它是一个复合结构,由一个哈希表(Hash Table)和一个跳表(Skip List)共同构成。

  • 哈希表 (dict):存储了从 成员(member)分数(score) 的映射。这使得 ZSCORE 这类通过成员查找分数的操作,时间复杂度可以达到 O(1)。如果没有哈希表,单纯在跳表中查找一个成员需要 O(log N)
  • 跳表 (zskiplist):存储了从 分数(score)成员(member) 的映射,并按照分数进行排序。这使得所有和排序、范围相关的操作(如 ZRANGE, ZRANGEBYSCORE)都能高效执行。

这两个结构通过指针共享成员和分数的数据,避免了数据冗余。

现在,我们来分析为什么在需要排序的部分,Redis 选择了跳表而不是平衡树。

3. 跳表 vs. 平衡树(如红黑树)

特性 跳表 (Skip List) 平衡二叉搜索树 (如红黑树) 在 Redis 场景下的优劣分析
实现复杂度 更简单 非常复杂 [跳表胜出] 这是最关键的优势之一。跳表的插入和删除逻辑比红黑树的旋转和颜色翻转等再平衡(rebalancing)操作要简单得多。代码更简洁,意味着更易于理解、维护和调试,也更不容易出错。
性能 (时间复杂度) 查找、插入、删除的平均复杂度为 O(log N) 查找、插入、删除的严格最坏复杂度为 O(log N) 理论上,红黑树的性能更稳定,没有“最坏情况”。但跳表的概率性保证了其性能在实践中极少退化,其 O(log N) 的表现已经足够优秀和稳定。对于 Redis 这种工程项目,常数项和实现效率同样重要。
范围查询效率 非常高效 高效 [跳表略胜] ZRANGEBYSCORE 这类操作,在跳表中找到起始节点后,只需在最底层的链表上进行一次线性遍历即可。这种遍历方式对 CPU 缓存非常友好。而红黑树需要进行中序遍历,这涉及到指针在树中的上下移动,缓存命中率可能不如跳表。
内存占用 相对较高 相对较低 [红黑树略胜] 跳表的每个节点包含多个指针(层数是随机的),平均指针数比红黑树节点(通常是父、左、右子节点指针)要多,因此内存占用会稍高一些。但对于现代服务器来说,这点内存开销通常在可接受范围内。
并发控制 更容易 更困难 [跳表胜出] 尽管 Redis 核心命令处理是单线程的,但数据结构的设计仍然会考虑并发友好性。跳表的更新操作通常只影响局部节点,锁的粒度可以很小。而红黑树的再平衡操作可能涉及从叶节点到根节点的路径,需要锁住更大范围的节点,并发性能较差。

4. 总结:为什么是跳表?

综合以上对比,Redis 选择跳表的原因可以归结为以下几点:

  1. 实现简单,易于维护:这是最主要的原因。相比红黑树复杂精妙的平衡操作,跳表的实现更加直观和简洁,大大降低了开发和维护成本。
  2. 性能优秀且足够稳定:虽然跳表的性能是概率性的 O(log N),但实践证明其性能非常稳定,与红黑树在同一量级,完全能满足 Redis 的高性能需求。
  3. 范围查询友好:跳表的链式结构使得范围查询(如 ZRANGE)的实现天然高效,且对 CPU 缓存友好。
  4. 易于扩展和实现额外功能:跳表的结构使其很容易实现 ZRANK(获取排名)功能。每个节点的跨度(span)信息记录了到下一个节点的距离,通过累加跨度可以快速计算出排名。

最终,Redis 的作者 Antirez 在设计时做出了一个非常务实的工程决策:选择一个实现简单、性能优异、且能完美满足业务需求的数据结构。跳表正是在这个权衡下的最佳选择。


补充:Ziplist / Listpack 优化

值得一提的是,当有序集合的元素数量较少且每个元素的值不大时,Redis 并不会直接使用“哈希表+跳表”的结构,因为这样太耗费内存。

在这种情况下,Redis 会使用一种叫做 Ziplist(在较新的版本中被 Listpack 替代)的紧凑列表结构来存储。Ziplist/Listpack 是一块连续的内存,它将所有元素(成员和分数)紧凑地排列在一起,极大地节省了内存。

当 ZSET 中的元素数量或元素大小超过预设的阈值时,Redis 会自动将其内部编码从 ziplist/listpack 转换为 skiplist(即哈希表+跳表)。这种根据数据规模自动转换底层实现的优化策略,体现了 Redis 对性能和内存效率的极致追求。

00:00
00:00