Redis zset在大数据量时用跳表+dict双结构:跳表按score排序支持O(log n)范围查询,dict按member查score实现O(1)点查,二者共享内存且通过zset-max-listpack-entries等阈值自动切换编码。

跳表不是树,但查询路径接近 O(log n)
Redis 的 zset 在数据量较大时会自动从 listpack 切换到 skiplist 编码,核心目的就是把链表的 O(n) 查找压到平均 O(log n)。它不靠平衡调整,而是用随机层数模拟“多级索引”:每个节点在插入时以概率 ZSKIPLIST_P = 0.25 向上生成新层,越高层节点越稀疏。
查找时从最高层开始横向推进,遇到 score 大于目标值就下一层,否则继续向右——这个过程天然跳过大量无关节点。实际测试中,100 万个元素的 zset,ZRANK 或 ZRANGE 命令通常只做 10–15 次指针跳转就能定位。
- 最大层数硬限制为 32,防止极端情况下的内存爆炸
- 单次查找最坏仍是 O(n),但概率极低;工程上可视为稳定 O(log n)
- 和红黑树相比,跳表没有旋转开销,插入/删除更轻量,尤其适合频繁写入场景(比如实时排行榜)
为什么必须搭配 dict,单独跳表不行
跳表按 score 排序,但用户查的是 member(比如 ZSCORE key user_id)。如果只靠跳表,就得遍历所有层去找匹配的 member 字符串,退化成 O(n)。
所以 Redis 实际存储是 zskiplist + dict 二合一结构:
-
dict存member → score映射,ZSCORE瞬间返回 -
zskiplist存score → member有序序列,支撑ZRANGE、ZREVRANK等范围操作 - 两个结构共享同一份
member和score内存,无冗余
这解释了为什么 zset 内存占用略高于纯哈希表——它为顺序性付出了空间代价。
跳表性能受什么参数影响最直接
真正影响线上表现的不是算法本身,而是两个可配阈值触发的编码切换行为:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
zset-max-listpack-entries默认 128:超过此数强制升为skiplist -
zset-max-listpack-value默认 64:任一member长度超 64 字节,立即切跳表
这两个值太小会导致小数据集也走跳表,白白增加指针开销;太大则让 listpack 长时间承受线性扫描压力。实测中,若 member 是 UUID(36 字节),且数量常在 100 左右,建议把 zset-max-listpack-entries 调到 256 以上,避免频繁重编码。
注意:CONFIG SET 修改后只对新建 zset 生效,已有键需 MEMORY USAGE 检查当前编码,必要时用 DEL + 重建触发切换。
跳表节点的 span 字段容易被忽略但很关键
每个跳表节点的每一层都有个 span 字段,记录该层指针跨越的**底层节点数量**(不是 score 差值)。它支撑 ZRANK 这类“求排名”操作:从头节点出发,每跳一次就把经过的 span 累加,最终得到目标节点在有序序列中的位置。
这个设计让排名计算变成 O(log n),而不是遍历计数。但副作用是:每次插入/删除都要更新沿途所有层的 span 值,带来额外 CPU 开销。所以高并发写入场景下,zset 的 ZADD 吞吐会略低于纯 hash。
如果你只用 zset 做范围查询,几乎不调 ZRANK 或 ZREVRANK,那 span 就是纯成本——可惜 Redis 没提供关闭选项,这是底层不可绕过的权衡。


















