redis zset采用dict+skiplist双结构,根本原因是单靠一种结构无法兼顾o(1)单点查询(zscore)与o(log n)范围操作(zrange/zrank);二者共享内存、各司其职,小数据时还用ziplist优化。

Redis 有序集合(ZSet)同时使用字典(dict)和跳表(skiplist)双结构,根本原因是:**单靠一种数据结构无法兼顾“快速单点查询”和“高效范围操作”这两类核心需求**。这是工程实践中典型的“用空间换时间、以结构换能力”的设计权衡。
单点查询需要 O(1) 响应,跳表做不到
如果只用跳表,查找某个 member 对应的 score(如执行 ZSCORE key member)需从头开始逐层比对,平均时间复杂度为 O(log N)。而业务中频繁出现按用户 ID 查分数、查排名等场景,O(log N) 在高并发下仍显冗余。字典以 member 为键、score 为值,哈希寻址天然支持 O(1) 查询,直接解决这个问题。
范围操作依赖有序性,字典无法胜任
字典本质是无序哈希表,不维护元素顺序。像 ZRANGE key 0 9 WITHSCORES(取前 10 名)、ZRANK key member(查某人排名)、ZCOUNT key min max(统计分数段人数)这类操作,必须基于 score 排序后的线性结构才能高效完成。跳表通过多层索引链表,在保持插入/删除 O(log N) 的同时,支持从任意位置向后遍历,完美支撑范围查询与排名计算。
两者共享数据,避免内存浪费
字典的 dictEntry 和跳表的 zskiplistnode 并不各自拷贝 member 和 score,而是通过指针指向同一份 SDS 字符串和 double 分数。这意味着:
- 更新一个 member 的 score 时,两个结构同步修改,保证一致性
- 内存中只存一份实际数据,没有冗余存储
- 结构切换(如小集合转大集合)时,数据可平滑迁移
小数据量还用压缩列表,进一步优化内存
当元素数 ≤128 且每个 member + score 总长 ≤64 字节时,Redis 会启用更紧凑的 ziplist 编码。它把 member-score 成对紧邻存放,升序排列,节省指针开销。但一旦超出阈值,就自动升级为 dict+skiplist 组合——既守住小场景的内存效率,又保障大场景的功能与性能底线。
不复杂但容易忽略:这个双索引设计不是叠加累加,而是各司其职、协同工作,让 ZSet 在排行榜、延迟队列、实时统计等真实场景中稳准快地落地。











