数据库与缓存11 分钟阅读更新于 2026-07-30

Redis Zset 为什么用跳表:范围查询比“看起来高级”更重要

Redis 为什么不使用红黑树而使用跳表的内容,讲清 Zset 的有序集合需求、跳表的范围查询、插入删除和实现复杂度。

相关工具

Zset 需要的不是一棵“更高级”的树

很多人第一次看到 Redis Zset 底层用跳表,会本能地问:为什么不用红黑树?红黑树也是有序结构,查找、插入、删除都是 O(logN),听起来很稳。这个问题本身没错,但如果只盯着复杂度,就会漏掉 Zset 的真实使用方式。

相关基础内容 对这个问题给出四个原因:跳表相对于红黑树实现更加简单;跳表这种数据结构更容易理解和调试;跳表对范围查询的支持较好,符合有序集合的特性;跳表在插入和删除操作上的性能表现较好,尤其适合有序集合场景。

这四点放在 Zset 里就很自然。Zset 不是只为了判断一个成员是否存在,它经常要按 score 排序,取一段排名,按分数范围拿成员,更新成员分数,删除成员。也就是说,它既要能快速定位,又要能顺着有序结果往后扫。跳表刚好把这两件事放在一个相对简单的结构里完成。

Redis Zset 为什么使用跳表而不是红黑树的图解,展示跳表多层有序链表、Zset 按 score 存储成员、ZRANGE 和 ZRANGEBYSCORE 范围查询,以及红黑树实现复杂和范围遍历不如跳表直观
跳表为什么适合 Redis Zset

跳表通过多层有序链表快速定位范围起点,再在底层链表顺序扫描结果,天然贴合 Zset 的范围查询和有序遍历。

先理解 Zset 的工作场景

Zset 是有序集合。它里面的元素有 member,也有 score。member 用来标识成员,score 用来排序。排行榜是最容易理解的例子:用户是 member,积分是 score;按积分从高到低取前 100 名,就是一个典型的有序范围读取。

除了排行榜,Zset 还常用于延迟队列、权重排序、时间线、热门内容排序。延迟队列可以把执行时间当 score,定时取出已经到期的一批任务;内容热度可以把热度分当 score,按分数范围或排名范围取数据。这些操作都不是单点查找,而是“定位一段,再连续取出”。

所以讨论底层结构时,要从这些命令出发。ZADD 要插入或更新分数,ZREM 要删除成员,ZRANGE 要按排名取范围,ZRANGEBYSCORE 要按 score 范围取成员。Zset 的关键不只是有序,而是经常围绕有序结果做范围操作。

跳表是什么样的结构

跳表可以理解成多层有序链表。最底层是一条完整的有序链表,包含所有节点;上面的层会抽取一部分节点作为索引。查找时从高层开始走,发现下一个节点还没超过目标,就继续向右;如果快超过了,就往下一层走。这样一步步缩小范围,最后落到底层找到目标位置。

它不像红黑树那样依赖旋转来维持平衡,而是通过随机层高形成近似平衡的索引层。这个设计很朴素:底层保证完整顺序,上层帮助跳过一段段节点。名字里的“跳”,说的就是查找时不用一个节点一个节点慢慢走,而是能从高层跨过去。

对 Redis 来说,这种结构有两个好处。第一,逻辑直接,代码和调试都相对容易。第二,底层本来就是链表,一旦定位到范围起点,后面顺序遍历就很自然。Zset 的很多命令正好需要这种“先找起点,再拿一段”的能力。

范围查询是跳表的主场

资料 特别提到跳表对范围查询支持较好,这符合有序集合数据类型的特性。有序集合里的元素是有序的,而跳表天然支持按范围查找。这个判断很关键,因为 Zset 的常用命令里,范围查询并不是边角功能。

假设要执行 ZRANGEBYSCORE,取 score 在 100 到 200 之间的成员。跳表可以先从高层索引快速定位到第一个大于等于 100 的节点,然后在底层链表向后扫描,直到 score 超过 200。返回过程和链表顺序天然一致,不需要额外把结果重新排一遍。

红黑树也能做范围查询,但遍历过程没有跳表这么直观。它要先在树里定位起点,再按中序后继继续遍历。可实现上会牵涉更多指针关系、父子节点和旋转维护。单看复杂度可能差不多,落到工程实现和范围遍历,跳表更贴合 Zset 的访问方式。

插入和删除不只看复杂度

同时提到,跳表在插入和删除操作上的性能表现较好,尤其是在有序集合场景下。Zset 里成员分数会变化,分数变化往往意味着节点在有序结构里的位置也要变。实现上通常可以先删除旧位置,再插入新位置。

跳表插入时,先找到每一层应该更新的前驱节点,再把新节点接进去。删除时,也是在各层把指针绕过目标节点。它没有红黑树插入删除后的旋转修复过程,逻辑更线性。对 Redis 这种追求命令执行短、行为可预测的系统,少一点复杂平衡逻辑,就是实实在在的工程收益。

当然,跳表不是没有成本。它需要额外的索引层指针,也依赖随机层高来维持平均性能。但相比红黑树的严格平衡和旋转规则,跳表的实现更容易控制。Redis 选择跳表,不是说红黑树不优秀,而是跳表在 Zset 这个场景里更顺手。

实现简单也是性能的一部分

很多资料讲数据结构时,会把“实现简单”说得像一个不太技术的理由。实际上在数据库和缓存系统里,实现简单很重要。结构越复杂,边界情况越多,调试越难,线上问题越不容易定位。Redis 的很多设计都偏实用:能满足复杂度要求,又能让实现保持清楚,就不会为了形式上更漂亮而增加维护成本。

跳表的节点关系比较直观:向右走、向下走、更新前驱指针。定位路径能清楚地解释出来,范围扫描也能顺着链表看出来。红黑树的旋转、染色、父子关系维护更复杂,代码正确性依赖更多细节。对一个需要长期稳定运行的内存数据库来说,少一层难以调试的复杂性,价值不小。

这也是 文中同时强调“实现简单”和“更容易理解和调试”的原因。不是编写程序的人偷懒,而是系统设计本来就要考虑可维护性。Zset 的需求没有逼着 Redis 必须使用红黑树,跳表已经能把核心操作做得很好。

跳表和哈希表一起工作

理解 Zset 时还要补一层:Redis 的有序集合并不是只靠跳表解决所有问题。典型实现里,Zset 会结合字典和跳表。字典负责按 member 快速找到对应 score,跳表负责按 score 保持有序并支持范围查询。

这样分工很合理。如果只是查某个 member 的分数,用字典就很快;如果要按排名或分数范围取一段结果,就走跳表。一个结构负责点查,一个结构负责有序范围。代价是同一份数据要在两个结构里维护,但换来的是常见命令都能有不错表现。

这也解释了为什么不能只说“跳表查找 O(logN),所以 Redis 用跳表”。真正的设计是组合拳:字典解决 member 到 score 的快速映射,跳表解决 score 维度的有序访问。Zset 的好用,来自这两个方向都被照顾到了。

什么时候会感受到跳表的优势

如果业务只是偶尔存一个分数,几乎不做范围查询,你可能感受不到跳表有什么特别。可一旦页面需要频繁取榜单、翻页、按分数筛选、取某个时间窗口内的任务,跳表的优势就出来了。

比如排行榜要取第 101 到 120 名,或者取积分在某个区间的用户。跳表可以快速定位范围附近,再沿底层链表顺序取出。这个过程和业务需求很贴近。返回结果本身就是有序的,不需要应用再拿一堆数据排序。

延迟队列也类似。把执行时间戳作为 score 后,系统每次取 score 小于当前时间的一批任务。跳表能找到第一个到期任务,再顺序扫描到未到期为止。这个模型简单、稳定,也容易解释为什么 Zset 常被拿来做轻量延迟任务。

不要把跳表理解成万能索引

跳表适合 Zset,不代表它适合所有数据结构。Redis 的 String、Hash、List、Set、Zset 背后有不同实现,前面讲底层数据结构时也提到过 SDS、quicklist、listpack、整数集合、哈希表等。每一种结构都是围绕自己的访问方式选出来的。

如果业务要做复杂多条件查询、跨字段筛选、聚合统计,跳表也救不了。那是 MySQL 或搜索引擎更擅长的工作。Zset 擅长的是围绕一个 score 维度做有序访问,不适合被当成通用查询引擎。

使用 Redis 时,最稳的方式是让数据结构和业务访问路径对齐。需要按分数排序和范围读取,就用 Zset;只做去重和集合判断,就用 Set;对象字段局部读写,就用 Hash。跳表的价值,正是在 Zset 的访问路径里被放大。

面试可以这样回答

如果被问 Redis 为什么用跳表而不是红黑树实现 Zset,可以先说 Zset 的核心需求:它要按 score 保持有序,支持成员插入删除、分数更新、按排名范围读取和按 score 范围读取。范围查询是高频需求,不只是单点查找。

然后按 相关的四个理由展开:跳表比红黑树实现更简单,也更容易理解和调试;跳表对范围查询支持更自然,定位到范围起点后,可以沿底层链表顺序扫描;插入和删除时主要更新多层指针,不需要像红黑树那样处理复杂旋转和染色;在有序集合场景下,跳表的整体表现更贴合 Redis 的需求。

最后补充一句更工程化的理解:Redis 不是因为红黑树不好才不用,而是因为跳表在 Zset 的范围查询、实现复杂度和维护成本之间取得了更合适的平衡。这个回答比只背 O(logN) 更完整。

常见问题

Redis Zset 为什么不用红黑树?

因为 Zset 很多操作是范围查询。跳表实现更简单、容易调试,定位范围起点后可以沿底层链表顺序扫描,更贴合有序集合的访问方式。

跳表查询复杂度是多少?

跳表在平均情况下查找、插入、删除都是 O(logN)。范围查询通常先定位起点,再顺序扫描范围内节点。

红黑树是不是比跳表差?

不是。红黑树也是优秀的平衡树。Redis 选择跳表,主要是因为 Zset 场景更看重范围遍历、实现简单和维护成本。

Zset 只靠跳表实现吗?

不是。常见实现会结合字典和跳表:字典负责按 member 快速查 score,跳表负责按 score 有序排列和范围查询。

MySQL 与 Redis

继续阅读

返回专题