Redis ZSet 用跳表而非红黑树,根本原因是其在单线程下更优平衡范围查询效率、实现可维护性与调试成本;跳表范围遍历走底层双向链表,CPU预取友好,红黑树中序遍历Cache Miss高;插入删除逻辑更简,ZRANK等扩展易实现;代码清晰、调试直观,符合Redis务实工程哲学。

Redis ZSet 用跳表(zskiplist)不用红黑树,根本原因不是“性能碾压”,而是**在单线程模型下,跳表把范围查询效率、实现可维护性、调试成本三者平衡到了极致**——红黑树在这些维度上全面吃亏。
跳表的范围遍历为什么比红黑树快?
ZSet 高频操作如 ZRANGE、ZRANGEBYSCORE 本质是“从某点开始,线性取 M 个节点”。跳表直接走最底层双向链表(level[0].forward),指针连续跳转,CPU 预取友好;红黑树必须中序遍历,得靠栈或父指针回溯,节点内存地址离散,Cache Miss 高。
- 查 score ∈ [80, 100] 的 500 个用户:跳表只需定位起点 + 沿
level[0]走 500 步;红黑树要反复进出子树,逻辑分支多,实际耗时高 15%~30%(实测百万数据) - Redis 源码里
zslFirstInRange()函数就是纯指针推进,无递归、无栈、无状态保存 - 红黑树若想优化范围遍历,得额外加“后继指针”或改造成 threaded tree,但这就违背了它作为通用平衡树的设计初衷
插入/删除逻辑复杂度差一个数量级
跳表增删核心只做两件事:随机决定层数、逐层更新前驱节点的 forward 指针。红黑树则必须处理旋转 + 变色 + 多 case 失衡修复。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 跳表插入:平均修改约 2~3 个指针(
zslInsert()中的 for 循环即可完成) - 红黑树插入:最少 1 次变色,可能触发左旋+右旋组合,需判断 5 种以上失衡形态,C 实现通常超 200 行逻辑
- Redis 曾收到社区 patch,在跳表上加
span字段轻松支持ZRANK(O(log N)),而红黑树要支持 rank 得重写整套遍历逻辑
单线程下,并发优势反而是次要的
很多人提“跳表更适合并发”,但这对 Redis 不关键——它本身是单线程事件循环。真正受益的是**代码可读性与长期维护成本**。
- 跳表节点结构清晰:
zskiplistNode里只有score、obj和level[]数组,调试时 gdb 打印一层就看全路径 - 红黑树节点含
color、parent、left、right四字段,任意指针错位都会导致整个树不可用,且 bug 极难复现 - Redis 作者 @antirez 明确说过:“我宁愿接受概率性结构带来的常数项开销,也不要为 0.1% 的理论优势承担 5 倍的调试时间”
跳表不是“次优解”,它是 Redis 在真实工程约束下的最优解:不追求教科书级的最坏复杂度,而是让 99% 的日常运维、patch 提交、故障排查都变得直观可预期。当你在 src/t_zset.c 里看到 zslDeleteNode() 只有 20 行 C 代码时,就该明白这个选择有多务实。

















