为什么 Redis 用跳表而不是二叉树:从简单到复杂的探索之旅
Redis 作为一个高性能的内存数据库,它的很多设计都让人眼前一亮,比如它在有序集合(Sorted Set)里选择了跳表(Skip List)而不是更常见的二叉树。虽然这俩的时间复杂度都是 log 级别,但 Redis 为啥偏偏挑了跳表呢?今天咱们就从最朴素的想法出发,一步步聊聊这个选择背后的逻辑,顺便看看简单的方案有哪些坑,又是怎么优化到今天这步的。
从最简单的想法开始:顺序查找
假如咱们要实现一个有序集合,最直白的方法是什么?当然是扔一个数组或者链表,把数据按顺序存进去。每次加个元素,就插到合适的位置;找元素呢,就从头扫到尾。这种方式简单得不能再简单,但问题也很明显:查找和插入的时间复杂度都是 O(n)。假设有 1000 个元素,平均得扫 500 次才能找到目标。数据量再大点,比如 10 万,那不得扫 5 万次?性能直接拉胯,用户体验也得跟着崩。所以,这条路显然走不通,得想点聪明的办法。
进化一步:试试二叉树
既然顺序查找太慢,咱们自然会想到用数据结构来提速。二叉搜索树(BST)是不是听起来挺靠谱?它每次都能把查找范围砍一半,时间复杂度直接降到 O(log n)。比如有 1024 个元素,最多 10 步就能找到目标,比起扫几百次强太多了。插入和删除也差不多是这个量级,听着挺美,对吧?
但等等,二叉树也不是没毛病。假设咱们的数据是按顺序加进来的,比如 1、2、3、4、5......这树会咋样?它会直接退化成一条链表!因为每次新元素都比前一个大,树就只能一直往右长,高度变成 n,查找又回到 O(n) 了。这叫"偏斜"(skewed tree),是个大坑。举个例子,假如你有 100 个有序数字插进去,最后这棵树深度就是 100,找最后一个元素得走 99 步,跟顺序扫也没啥区别了。
修补二叉树的坑:平衡树
发现这个问题后,咱们得修啊。咋修?自然想到平衡二叉树,比如 AVL 树或者红黑树。这种树通过旋转操作,保证树的高度始终控制在 O(log n)。比如红黑树,它用颜色标记和一些规则,确保最长路径不会超过最短路径的两倍。插 100 个元素,树高最多也就 7 左右,查找效率稳得一批。Redis 如果用这个,好像也没啥问题吧?
但这里又冒出新麻烦。平衡树的维护成本不低。每次插入或删除,都有可能打破平衡,得靠旋转来调整。举个例子,插一个数可能引发好几次旋转,每旋转一次都得调整指针,计算量不算小。假设你频繁操作,比如一秒内插删几千次,这额外开销就有点扎眼了。而且内存数据库对性能敏感,旋转这种操作还可能让缓存命中率变差,毕竟指针跳来跳去,CPU 不一定爱这套。
换个思路:能不能简单点?
平衡树虽然解决问题了,但总感觉有点"重"。有没有更轻量、灵活的方案呢?这时候跳表就悄悄登场了。跳表是个啥?简单说,它是个"分层链表"。底层是个普通有序链表,往上每层都随机挑一些节点做索引。找东西时,先从顶层走,快速定位大致范围,再一层层往下钻,最后到最底层找到目标。
举个例子,假设有 16 个数:1、3、5、7、9、11、13、15、17、19、21、23、25、27、29、31。底层是全集,往上一层随机挑一半,比如 1、5、9、13、17、21、25、29,再上一层再挑一半,比如 1、9、17、25。找 23 咋找?从顶层 25 开始,发现大了,退回 17,走一步到 21,再下去一层到 23。总共也就 4、5 步。平均下来,跳表的查找复杂度也是 O(log n),跟二叉树一个级别。
跳表比二叉树强在哪?
跳表听着挺妙,但为啥 Redis 选它不选二叉树呢?咱们来挖挖细节。
-
插入和删除更轻松
二叉树(哪怕是平衡树)调整结构时得旋转,跳表就不用这么麻烦。它插入时随机决定新节点出现在哪些层,靠的是概率,没复杂变换。删除也一样,找到节点直接摘掉就行。假设你插 1000 个数,跳表平均每层节点数是对数级别,操作开销低且稳定。红黑树呢?可能得旋转好几次,运气不好还得多算几步。
-
实现简单,调试方便
跳表本质是个链表加索引,代码写起来直白,逻辑也好懂。红黑树那套颜色规则和旋转,写错了调试起来能把人逼疯。Redis 追求高性能的同时,也得考虑开发效率,跳表在这块明显占优。
-
内存友好,适合并发
跳表每层节点数是逐渐减少的,内存分配相对连续,缓存利用率高。红黑树节点散落各处,指针跳转多,缓存不一定友好。而且跳表还能轻松支持并发,比如加个锁保护某层,操作起来比树结构灵活。
从朴素到跳表:优化的方向
回过头看,从顺序查找到二叉树,再到平衡树,最后到跳表,这一路其实就是在解决"效率"和"稳定性"的矛盾。顺序查找太慢,二叉树不稳,平衡树复杂,跳表就成了个折中又高效的选择。优化方向无非是:
- 减少不必要的计算(跳表避免旋转);
- 提升内存效率(分层结构更紧凑);
- 简化实现(概率替代严格平衡)。
这些正好跟现代高性能系统的主流思路吻合,像 Redis 这种内存数据库,跳表确实是个聪明解。
结尾彩蛋
所以,Redis 选跳表不是拍脑袋决定的,而是从简单方案一步步试错、优化的结果。跳表不一定在所有场景都秒杀二叉树,但在这儿,它确实更香。你说呢,下次写代码要不要也试试跳表这招?