Redis ZSet 有序集合实现原理
ZSet =跳表(skiplist) + 哈希表(dict),两套结构同时保存一份数据。
数据特点:成员member唯一不重复,每个member绑定一个分数score,按照score排序。
1. 两个底层结构各自职责
- skiplist 跳表
- 按照
score做排序,负责:范围查询、排行榜、倒序、区间分页(zrange、zrevrange、zrangebyscore) - Redis的跳表是双向跳表,节点带有back指针,可以高效反向遍历。
- 平均增删改查 O(log n),最大层数固定32层,节点层数随机生成。
- dict 哈希表
- key:member成员,value:对应的score分数
- 作用:O(1)时间快速获取某个member的score,判断成员是否存在,保证member唯一性。
插入的时候dict先判断member是否已经存在,实现去重。
⚠️两份结构存的是同一份数据,内存会有少量额外开销,换取查询性能。
2. ziplist 压缩列表(小zset)
当满足两个条件,zset不使用跳表,改用ziplist压缩列表存储:
- 元素数量 <
zset‑max‑ziplist‑entries默认128 - 每个元素大小 <
zset‑max‑ziplist‑value默认64字节
ziplist是连续内存,节约内存;内部按score有序排列。
一旦超过阈值,自动转换为 skiplist + dict。
3. 核心命令底层怎么走
zadd key score member- dict判断member是否存在,存在则更新score;不存在新增
- 将数据插入跳表,按score维护有序
zscore key member:直接查dict哈希表,O(1)返回分数,不走跳表zrange key start end:直接在skiplist做范围遍历 O(log n + k),k是返回元素数量
4. 业务场景
- 排行榜、热搜、延时队列、带权重的有序列表
面试常见坑
- score可以相同:多个member允许分数一样;score相同会按member字典序排序。
- zset没有给member单独过期的能力,只能对整个zset key设置expire。
- 不要存超大zset,
zrange返回大量数据会阻塞Redis。
总结:
ZSet底层分两种情况,少量短元素用ziplist压缩列表;数据量大则采用跳表+哈希表组合。跳表负责排序和范围查询,哈希表实现快速取score和去重。
口述简短版:
Redis的zset,数据少的时候用压缩列表。数据量大是跳表加上哈希表一起实现。跳表负责按照score排序,用来做排行榜、范围查询;哈希表用来快速拿到成员的分数,保证成员不重复。注意成员唯一,分数可以重复。