📖 版块 A:知识讲解版 (温故知新)
-
跳表(SkipList)是什么:跳表是一种基于并联链表的随机化数据结构。它在单向链表的基础上,通过在部分节点上增加多级“索引”(前进指针),实现了快速跨越式查找。它的查找、插入和删除的平均时间复杂度都是 O(log N),最坏情况是 O(N)。
-
跳表在 Redis 中的应用:跳表是 Redis 有序集合(ZSet)的核心底层结构之一(当元素数量较少或较小时,ZSet 会使用压缩列表/listpack)。
-
为什么 ZSet 用跳表而不用红黑树或 B+树?
- 实现复杂度和可读性:红黑树的插入和删除操作为了维护树的平衡,需要进行复杂的左旋、右旋和变色,代码非常复杂且难以调试。而跳表的实现只需维护多级链表,算法更简单,可读性和可维护性更高。
- 范围查找的效率:ZSet 经常需要执行基于分数的范围查询(如
ZRANGEBYSCORE)。红黑树进行范围查询时,找到起点后还需要进行复杂的中序遍历;跳表在找到起点后,底层就是个有序链表,直接沿着第一层链表往后遍历即可,非常高效。 - 并发操作的便利性(虽然 Redis 核心是单线程,但从通用数据结构角度看):更新红黑树可能引发树的结构调整,这在多线程环境下需要锁住较大范围的节点;跳表的更新只会影响局部节点的指针,更适合并发改造。
- 为什么不用 B+ 树:B+ 树是为磁盘 IO 设计的。它的矮胖结构(高扇出)是为了最大限度减少磁盘读写次数(寻道时间长)。但 Redis 是纯内存数据库,不存在磁盘寻道的性能瓶颈,因此没必要使用维护成本更高的 B+ 树。
🎤 版块 B:面试实战版 (高手逐字稿)
Q:能详细说说Redis的跳表吗?有序集合(ZSet)为什么不用红黑树或者B+树来实现?
💬 高手逐字稿:
Redis 中的跳表主要用在有序集合(ZSet)的底层实现上。跳表的本质是一种“空间换时间”的随机化数据结构,它在普通的有序链表上面,人为地增加了多级索引。当我们查找数据时,可以先从最顶层的稀疏索引开始跳跃式查找,逐渐降级,最终在底层链表定位。这样就把链表 O(N) 的查询复杂度降到了平均 O(log N)。
至于 Redis 为什么选择跳表而不是红黑树或 B+ 树,这其实是一个非常经典的工程权衡问题,我认为主要有以下三个维度的考量:
第一点是范围查询的性能。ZSet 有个非常核心的操作就是基于分数的范围查询,比如获取排行榜的前 100 名。如果是红黑树,找到起始节点后,还需要用中序遍历去顺藤摸瓜找后续节点,操作比较繁琐;而跳表的底层第一层天然就是一个有序的双向链表(Redis 中跳表的底层节点有后退指针),找到起点后,直接 next 遍历过去就行了,范围提取极其高效。
第二点是实现的复杂度和内存开销。红黑树在插入和删除时,为了维护绝对的平衡,动不动就要进行左旋、右旋和变色,代码极其复杂且容易出错。而跳表的插入仅仅是通过一个随机函数决定要不要建立多级索引,只涉及简单的指针修改,不仅代码好写好维护,而且在内存利用率上,跳表还可以通过调节随机生成的层数概率来灵活控制内存开销,比红黑树更灵活。
第三点是与 B+ 树的场景差异。B+ 树的特点是“矮胖”,它是专门为磁盘 IO 设计的,目的是为了减少磁盘寻道次数,每一次加载一个磁盘页。但 Redis 是纯内存数据库,内存的随机访问速度极快,完全没有磁盘寻道的问题。如果在内存里去维护 B+ 树复杂的页分裂和页合并机制,反而是一种得不偿失的性能浪费。
所以,跳表凭借着 O(logN) 的性能、极其优雅的范围查询机制,以及极低的代码维护成本,成了 Redis 有序集合的最佳选择。