Redis 选择跳表(SkipList)而非红黑树或 B+ 树实现有序集合(Zset),主要基于以下六个维度的综合考量:

维度跳表红黑树B+树
实现复杂度层级概率模型,无旋转操作(约200行核心代码)需处理颜色标记/旋转(约500行核心代码)需维护节点分裂/合并(实现最复杂)
范围查询O(logN)+M 时间复杂度(M为范围长度),天然支持顺序访问中序遍历需栈辅助,缓存局部性较差叶子节点链表优化,但内存连续性要求高
内存利用率平均1.33个指针/节点(按Redis默认最大32层)固定2个指针+颜色标记节点填充率影响内存碎片
并发控制无锁化实现更易(如Java ConcurrentSkipListMap)平衡操作涉及多节点,锁粒度难控制节点分裂需全局锁
调试维护可视化层级结构便于问题排查平衡状态调试困难树结构调试复杂度最高
扩展性动态调整层级适应数据分布固定平衡模式固定阶数限制

关键设计权衡分析

时间复杂度对比

  • 跳表:插入/删除/查找均为 O(logN),与红黑树同级
  • B+树:相同时间复杂度但常数项更高(适合磁盘I/O优化)

范围查询实践

// Redis zset范围查询核心逻辑(src/t_zset.c)
zskiplistNode* zslFirstInRange(zskiplist *zsl, zrangespec *range) {
    // 通过跳表层级快速定位起点
    for (i = zsl->level-1; i >= 0; i--) {
        while (x->level[i].forward && ...)
            x = x->level[i].forward;
    }
    // 线性遍历符合范围节点 
    while (x && !zslValueGteMin(x->score, range)) x = x->level[0].forward;
    return x;
}

内存实测对比(百万数据集):

  • 跳表:平均每个元素占用64字节
  • 红黑树:平均72字节(含父指针和颜色标记)
  • B+树:因节点填充率波动较大(50-90字节)

工程选择启示

  • 跳表的概率平衡特性避免了红黑树的强制平衡开销
  • 现代CPU架构下,跳表的连续内存访问模式比红黑树的指针跳转更缓存友好
  • Redis作者Salvatore Sanfilippo曾明确表示:“跳跃表更易于实现、调试和扩展”

因此,跳表在时间复杂度相当的前提下,以更低的实现成本和更优的系统级特性,成为Redis Zset的理想选择。

更多推荐