核心概括
Redis 的 Zset 同时具备两个核心特性:
- 有序性:元素按分值(score) 从小到大排列。
- 唯一性:集合中的成员(member) 是唯一的,但分值可以相同(分值相同时,按成员字典序排列)。
为了实现这种高效的、兼具'集合'和'有序'特性的数据结构,Redis 采用了两种底层数据结构相结合的方案:
- ziplist(压缩列表)或 listpack(紧凑列表):用于元素数量少、元素体积小的场景,以节省内存。
- skiplist(跳跃表) + dict(哈希表):用于通用场景,以提供高效的查询和范围操作。
这种根据条件动态切换底层结构的设计,体现了 Redis 在性能与内存之间做出的精妙权衡。
一、两种编码方式
在 Redis 内部,Zset 有两种编码方式,通过配置项 zset-max-ziplist-entries 和 zset-max-ziplist-value 来控制。
1. ziplist / listpack 编码
在 Redis 早期版本使用 ziplist,新版本(7.0+)逐渐用 listpack 替代 ziplist。我们以 listpack 为例讲解。
适用条件(同时满足):
- 有序集合保存的 元素数量 小于
zset-max-ziplist-entries(默认 128)。 - 每个 成员(member)的字符串长度 小于
zset-max-ziplist-value(默认 64 字节)。
内存布局:
listpack 是一个紧凑的、连续内存块,它按 [member1, score1, member2, score2, ...] 的顺序,成对存储成员和分值。
- 按分值排序:所有元素在 listpack 内部就是严格按照分值升序排列的。
- 查找方式:由于是紧凑数组,查找需要 线性遍历。但因为元素少,且内存连续,缓存友好,效率仍然可以接受。
- 插入/删除:需要移动后续元素,时间复杂度 O(N)。同样因为元素少,成本可控。
目的:在元素少且小的场景下,这种结构避免了额外的指针开销,极大地节约了内存。
2. skiplist 编码
当不满足上述任一条件时,Zset 会自动转换为 skiplist 编码。这才是 Zset 的'完全体'和核心实现。
它实际上是一个 复合结构,包含一个 跳跃表(skiplist) 和一个 字典(dict)。
typedef struct zset {
dict *dict; // 哈希字典
zskiplist *zsl; // 跳跃表
} zset;
为什么需要两种结构?
- 跳跃表(zskiplist):核心作用在于维护元素的有序性,支持高效的范围查询(如 ZRANGE, ZRANK)和插入/删除操作(平均 O(logN))。


