跳表为核:串联 Redis、ES 与业务架构的底层思想复用

摘要

跳表是一种用多层有序链表 + 概率索引 实现的高性能有序数据结构,在 Redis、Elasticsearch 等主流中间件底层广泛使用。本文结合跳表结构图,从跳表核心原理出发,对比红黑树的工程取舍,拆解 Redis ZSet「哈希表 + 跳表」双结构实现逻辑,串联 ES 深度分页 / 排序的底层实现,完善架构选型理论依据,重点拆解跳表思想在业务开发中的落地复用

一、跳表核心原理

1.1 红黑树的行业代表性:它是有序数据结构的 "行业标杆"

很多读者会疑惑:讲解跳表,为何开篇就要和红黑树做对比?

红黑树是工业界最经典的平衡二叉树 ,Java TreeMap、C++ std::set 底层全部采用红黑树实现,是内存有序数据存储的标杆方案。它通过严格的节点变色、树旋转 ,保证树结构平衡,实现增删查平均 O(logn) 时间复杂度,是所有有序结构的 "对照组天花板"。

而跳表的诞生,本质就是工程界为了规避红黑树的缺陷,设计的平替方案 ,二者都是解决「内存有序数据存储」问题,对比二者,能瞬间看懂跳表的设计优势与适用场景。

1.2 拆解跳表核心结构

跳表结构实例:

  • Level 0(最底层) :完整有序链表,存储全量数据:15→32→34→35→36→45→50→60→66→85→95→96→97→98→99,对应业务全量数据;
  • Level1~Level5(上层索引层) :稀疏高速索引,层级越高,节点越少。例如 36、45、50、85、99 向上延伸,在高层级直接作为索引节点;
  • HD 头节点 :每一层链表的起点,统一管理多层索引。

1.3 核心思想:空间换时间,概率平衡代替严格平衡

普通有序链表查询效率 O(n),海量数据遍历极慢;红黑树效率高但实现复杂、并发锁竞争严重。跳表核心解决思路:

  1. 分层索引 :上层索引快速缩小查询范围,下层链表精准遍历;
  2. 随机层级 :插入节点时,以固定概率(Redis 默认 0.5)随机决定节点最高层级,不需要像红黑树一样旋转、变色 ,实现极简;
  3. 天然有序 :所有节点全局有序,天生适配范围查询、分页、有序遍历

1.4 时间 / 空间复杂度

  • 平均查询 / 插入 / 删除:O(logn),和红黑树持平;
  • 最坏情况:O(n),但随机概率设计下概率极低,工程上完全可接受;
  • 空间复杂度:O(n),索引层仅多占用约 50% 内存,内存开销可控。

1.5 跳表 vs 红黑树

|----------|-----------------------|--------------------------|
| 对比维度 | 跳表 | 红黑树 |
| 实现难度 | 极简,链表 + 随机数,无复杂树操作 | 极复杂,旋转、变色规则繁琐,易出错 |
| 有序范围查询 | 极强,链表结构可连续遍历,分页友好 | 较弱,树结构只能中序遍历,分页成本高 |
| 并发友好性 | 优秀,可实现无锁跳表,修改局部指针 | 差,树修改易触发大范围锁,并发性能差 |
| 工程场景 | 内存有序、分页、中间件(Redis/ES) | 少量内存有序精准查询(Java TreeMap) |

一句话总结:单点精准查询,红黑树够用;有序范围查询、分页、海量遍历,跳表碾压红黑树

二、落地一:Redis ZSet 底层实现拆解

2.1 核心误区澄清:教学跳表≠工程跳表

上面示例是极简单值跳表 ,仅用于演示结构;但Redis 源码中的跳表节点是自定义结构体,并非只存储单个数值 ,而跳表同时存 score 和 member。

Redis 真实跳表节点结构(源码精简版):

cpp 复制代码
typedef struct zskiplistNode {
    double score;      // 排序依据:分值,对应可视化中的数字
    char *ele;         // 业务数据:member元素,如用户ID、商品ID
    struct zskiplistLevel *level; // 多层索引指针,跳表核心结构
} zskiplistNode;
  • score:用于全局排序、构建多层索引;
  • ele(member):绑定的业务值,与 score 一一对应;
  • 每一层索引,均携带完整的 score+member 节点 跳跃,而非仅存储数字。

2.2 ZSet 底层双结构设计:哈希表 + 跳表

Redis 有序集合 ZSet,采用双结构冗余存储 ,各司其职,实现高性能有序操作:

  1. 哈希表(dict) :维护 member → score 的映射关系,实现 O(1) 单点极速查询;
  2. 跳表(skiplist) :节点存储 score + member,以 score 为排序依据,构建多层有序索引,支撑排序、分页、范围查询。
2.2.1 双结构数据存储示例

以直播间用户积分排行榜为例:

  • 哈希表存储

user_001 → 950
user_002 → 880
user_003 → 920

  • 跳表底层有序链表(Level 0)

(score:880, ele:user_002) → (score:920, ele:user_003) → (score:950, ele:user_001)

2.3 ZSet 核心 API 执行时序与原理

通过 3 个高频 API,直观展示哈希表与跳表的协同工作逻辑:

① 插入元素: ZADD live_rank 950 user_001

执行时序

  1. 哈希表新增映射:user_001 → 950;
  2. 随机生成节点层级,在跳表中插入 (score:950, ele:user_001);
  3. 逐层更新上层索引节点。
② 单点查分: ZSCORE live_rank user_001

执行时序 :直接查询哈希表,O(1) 返回分数 950,完全不操作跳表

③ 分页查排行榜: ZRANGE live_rank 0 2 WITHSCORES

执行时序 :直接遍历跳表有序链表,按序返回前 3 名用户及分数,完全不操作哈希表

④ 范围查询: ZRANGEBYSCORE live_rank 800 1000 WITHSCORES

执行时序 :跳表通过多层索引快速定位 800~1000 分值区间,返回对应有序数据。

核心设计逻辑:哈希表解决 "快速单点查",跳表解决 "有序批量查",牺牲少量内存冗余,换取极致性能

2.4 为什么 ZSet 放弃红黑树?

  1. 业务高频场景为排行榜、延时队列、时间线分页 ,均为大范围有序遍历,跳表链表结构遍历效率远高于红黑树;
  2. 跳表实现简单,无树旋转逻辑,内存占用可控;
  3. 并发场景下,跳表仅修改局部指针,锁粒度更小,性能更优。

三、落地二:Elasticsearch 底层跳表

3.1 我之前的 ES 深度分页痛点回顾

我写过《Elasticsearch 深度分页:时间分桶动态定位算法实战》,解决了from+size深度分页 OOM、性能极差的问题。当时侧重业务算法层面,现在从跳表底层解释:时间分桶算法本质是复用跳表分层索引思想

3.2 ES 中跳表的核心应用场景

1.DocValues 有序结构

ES 排序、聚合的底层,大量数值 / 时间字段用跳表结构存储有序数据(字段值 + 文档 ID) ,实现海量数据快速定位,和 Redis ZSet 的「score+member」设计逻辑完全一致;

2. 深度分页慢的根源

from+size分页需要扫描前 from 条数据,跳表虽然有序,但偏移量越大,扫描成本越高

3.时间分桶算法与跳表的协同

我的时间分桶动态定位算法:

  • 按时间拆分多个有序区间(桶),对应跳表的高层级稀疏索引
  • 先通过索引快速定位目标桶,跳过前面海量无效数据;
  • 仅在目标桶内做分页,规避全局深度扫描。

本质:手动实现了跳表的分层索引逻辑

四、选型:跳表、红黑树、B + 树、哈希表

4.1 四大数据结构核心原理对比表

|-----------|---------------|-----------------------------|--------------------|-------------------------------|
| 数据结构 | 底层结构 | 核心优势 | 核心短板 | 典型适用场景 |
| 跳表 | 多层有序链表 + 概率索引 | 有序范围查询强、分页友好、并发友好、实现简单 | 最坏时间复杂度 O (n),概率极低 | Redis ZSet、ES 有序字段、海量有序分页、排行榜 |
| 红黑树 | 平衡二叉搜索树 | 严格平衡,增删查稳定 O (logn),单点查询性能强 | 范围遍历弱、实现极复杂、并发锁粒度大 | Java TreeMap、少量内存有序精准查询 |
| B + 树 | 多路平衡树(磁盘页结构) | 磁盘 IO 适配好、持久化能力强、事务支持完善 | 内存场景性能一般 | MySQL 索引、磁盘存储数据库 |
| 哈希表 | 键值对哈希映射 | 单点查询 O (1) 极速、读写高效 | 无序、不支持范围 / 分页查询 | Redis String、缓存、快速成员存在性判断 |

4 . 2

  1. 内存场景,需要有序、分页、大范围遍历 → 优先跳表 (Redis、ES 有序检索)
  2. 内存场景,只做少量有序精准查询、无大范围遍历 → 红黑树 (Java TreeMap)
  3. 磁盘持久化、数据库存储、事务场景 → B + 树 (MySQL)
  4. 仅做精准查询、无序缓存 → 哈希表 (Redis String)

五、日常开发实战:跳表思想的深度复用

很多人认为跳表是底层数据结构,业务开发用不上,实际海量有序场景,都可以复用跳表分层索引思想,甚至直接手写简易跳表

5.1 核心复用思想

跳表思想本质:对有序数据,建立多层级、稀疏的索引,缩小查询范围,避免全量遍历。

业务中不需要实现随机层级,直接手动分桶、分层索引,就是简化版跳表。

5.2 场景 1:海量订单 / 日志深度分页

  • 痛点:千万级日志、订单,按创建时间分页,limit offset,size深度分页越往后越慢;
  • 跳表思想复用:
    • 底层:全量数据按时间有序存储(对应跳表 Level0);
    • 上层索引:按年→月→日分桶,构建分层索引(对应跳表高层索引);
    • 查询:先通过索引定位到具体日期桶,再在桶内分页,跳过前面海量数据;
  • 示例 SQL 伪代码:
sql 复制代码
-- 1. 索引层定位目标月份桶
SELECT min(id),max(id) FROM order_info WHERE create_time BETWEEN '2026-04-01' AND '2026-04-30';
-- 2. 桶内分页查询
SELECT * FROM order_info WHERE id BETWEEN min_id AND max_id LIMIT offset,size;
  • 本质:手动构建分层索引,和跳表原理完全一致

5.3 场景 2:本地高性能排行榜

  • 痛点:Java 内存内实现百万级用户积分排行榜,用 List 遍历排序效率极低;
  • 方案:直接手写极简跳表 ,仅实现有序插入、范围查询,无需复杂随机层级;
  • 实现逻辑:
    • 底层链表存储全量有序积分(用户 ID + 积分);
    • 上层建立稀疏索引(每 1000 个节点建一个索引);
    • 查询 TopN、分页、区间用户,直接走索引;
  • 优势:比 TreeSet(红黑树)遍历快 10 倍以上,适合本地缓存排行榜。

5.4 场景 3:滑动窗口限流、有序区间检索

  • 滑动窗口限流:存储时间戳有序列表,建立分层索引,快速统计窗口内请求量;
  • 商品价格区间查询:价格有序存储,分层建立价格段索引,快速筛选区间商品。

5.5 总结

中间件的底层设计,完全可以下沉到业务开发,跳表不是底层专利,而是通用性能优化方法论。

六、核心复习要点

6.1 跳表原理

  1. 结构:多层有序链表,底层存全量数据,上层建稀疏索引
  2. 机制:概率平衡替代红黑树旋转,支持有序范围查询
  3. 复杂度:增删查平均 O (logn),用于内存有序场景

6.2 中间件落地

  1. Redis ZSet:哈希表做单点查询,跳表做有序遍历,节点存储 score 与 member
  2. ES 分页:时间分桶算法复用跳表分层索引逻辑

6.3 架构选型

内存有序选跳表,磁盘持久化选 B + 树,单点查询选哈希表

6.4 业务落地

  1. 复用分层索引,实现海量有序数据分页、范围查询
  2. 本地高性能场景,自定义简易跳表

📚 我的技术博客导航:[点击进入一站式查看所有干货]


相关推荐
南境十里·墨染春水1 小时前
linux学习进展 Redis详解
linux·redis·学习
爱喝热水的呀哈喽2 小时前
agent4hypermesh计划
大数据·elasticsearch·搜索引擎
Mahir0810 小时前
Redis 与 MySQL 数据同步:一致性保证的完整解决方案
数据库·redis·mysql·缓存·面试·数据一致性
多加点辣也没关系12 小时前
Redis 的安装(详细教程)
数据库·redis·缓存
数据库小学妹13 小时前
数据库连接池避坑指南:告别“连接超时”与“资源耗尽”,让系统跑得更快!
数据库·redis·sql·mysql·缓存·dba
難釋懷14 小时前
Redis网络模型-IO多路复用模型-poll模式
网络·数据库·redis
Elastic 中国社区官方博客16 小时前
在 Elasticsearch 中使用利润率与流行度加权来优化电商搜索
大数据·数据库·elasticsearch·搜索引擎·全文检索
环流_18 小时前
Redis中string类型的应用场景
数据库·redis·缓存
环流_19 小时前
redis中list类型
数据库·redis·list