详情Redis的Zset结构
一、什么是 ZSet?
ZSet(Sorted Set)是 Redis 提供的一种兼具Set和Hash特性的数据结构。
像 Set 一样:它内部的成员(
member
)是唯一的,不允许重复。这使得它天然支持去重。像 Hash 一样:每个成员都会关联一个分数(
score
),这是一个双精度的浮点数(double
)。核心特性:ZSet 中的所有成员会按照这个
score
进行从小到大的排序。正因为这个排序特性,它可以实现非常多的排行榜类业务场景。
简单来说,ZSet 就是一个有序的、去重的字符串集合,排序的依据是每个字符串对应的分数。
二、ZSet 的常见使用场景
正是因为score
+ 排序
的特性,ZSet 的应用场景非常广泛:
排行榜:这是最经典的应用。
游戏排行榜:玩家分数作为
score
,玩家ID作为member
。ZREVRANGE
命令可以轻松获取前N名。热搜榜:热搜词的热度作为
score
,关键词作为member
。每次点击事件执行ZINCRBY
命令增加热度。
带权重的队列:
将任务的执行时间戳作为
score
,任务内容作为member
。消费者使用ZRANGEBYSCORE
查询当前时间戳之前的所有任务来处理,实现延迟队列。
范围查找:
例如,统计年收入在 20W 到 50W 之间的用户。将收入作为
score
,用户ID作为member
,可以快速通过ZRANGEBYSCORE
命令获取。
优先级系统:
不同用户或任务有不同的优先级,用
score
表示,系统可以优先处理高优先级(score
小或大)的项目。
三、ZSet 的底层实现
这是理解 ZSet 性能的关键。Redis 为了在内存使用和性能之间取得平衡,为 ZSet 设计了两种内部编码(底层数据结构),会根据一定的规则自动切换:
ziplist(压缩列表)/ listpack (紧凑列表)
skiplist(跳跃表) + dict(字典)
3.1 ziplist和listpack有什么区别
ziplist
和 listpack
都是Redis设计的用于存储字符串元素的紧凑型数据结构,旨在节省内存。listpack
被设计用来取代 ziplist
,是其现代化的继承者。
特性 | ziplist (压缩列表) | listpack (紧凑列表) | 优势 |
---|---|---|---|
设计目标 | 节省内存的双向链表 | 节省内存、更安全的单向列表 | 更简单、更安全 |
遍历方式 | 双向(前向和后向) | 单向(仅前向) | 实现简化 |
关键缺陷 | 级联更新 | 无级联更新 | 性能稳定,无最坏情况 |
节点结构 | [prevlen][encoding][data] | [encoding][data][backlen] | 解耦,自包含 |
内存顺序 | 头部 -> 尾部 | 头部 -> 尾部 | 基本相同 |
现状 | 逐渐被废弃 | 新一代的默认实现 | 代表未来 |
3.2 为什么redis使用listpack替换ziplist
核心缺陷:级联更新 (Cascade Update)
ziplist的级联更新问题:
原因: ziplist的每个节点都包含一个
prevlen
字段,用于记录前一个节点的长度,以便实现反向遍历。问题触发: 当一个新的节点被插入或一个已有节点被更新,导致其长度发生变化时,它后面的节点的
prevlen
字段可能需要更新。连锁反应: 如果后一个节点因为
prevlen
变化(例如从1字节变为5字节)而导致自身长度也发生变化,那么再后面的节点又需要更新。在最坏情况下,会导致一连串的节点都需要重新分配空间和复制数据,性能从O(1)退化到O(N²)。虽然实际中发生概率不高,但一旦发生对性能影响很大。
listpack的解决之道:
消除根源: listpack完全移除了
prevlen
字段,因此一个节点的长度变化绝不会影响后续的节点。实现方式: 它将表示节点长度的字段
backlen
(称为“反向长度”或“条目长度”)放在了节点的末尾。并且这个backlen
记录的是自身节点的长度,而不是前一个节点的。结果: 任何插入、删除、修改操作都只影响操作点附近的有限节点,性能稳定在O(N),没有最坏情况下的性能灾难。
节点结构 (Entry Structure):
ziplist节点:
[prevlen][encoding][data]
prevlen
:前一个节点的长度。长度可变(1字节或5字节)。encoding
:编码方式,标识数据的类型和长度。data
:实际存储的数据。
listpack节点:
[encoding][data][backlen]
encoding
:编码方式,标识数据的类型和长度。data
:实际存储的数据。backlen
:自身节点(从encoding
开始到backlen
结束)的总长度。长度可变(1-5字节)。
listpack结构的优势在于“自包含”。要解析一个listpack,只需要从前往后或从后往前读取即可。要找到上一个节点,不需要知道前一个节点的任何信息,只需通过当前节点的
backlen
跳转到上一个节点的起始位置。这种设计使得节点之间完全解耦。
遍历方式:
ziplist: 由于有
prevlen
,它可以非常高效地进行双向遍历(从头到尾或从尾到头)。listpack: 由于没有指向前一个节点的直接指针,反向遍历相对麻烦一些。需要通过当前节点的
backlen
来计算前一个节点的起始位置,然后向前跳转。因此,它的反向遍历效率低于ziplist。Redis的作者认为,在绝大多数使用场景下,需要的是正向遍历或随机访问(通过索引),反向遍历的需求很少。用一点点反向遍历的性能损失,换来整个结构的稳定性和简单性,是非常值得的交易。
对比项:
对比项 ziplist listpack 设计哲学 功能优先(双向遍历),容忍缺陷 稳健优先(消除缺陷),牺牲次要功能 关键问题 存在级联更新风险 无级联更新,性能稳定 复杂度 实现复杂,难以维护 实现更简单,更健壮 适用性 已过时,逐渐被废弃 现代选择,是Redis当前和未来的默认配置
3.3 skiplist
跳跃表(SkipList) 是Redis中另一个核心数据结构,它与ziplist/listpack的设计目标和应用场景完全不同。如果说ziplist/listpack是为了节省内存而设计的紧凑型、线性结构,那么跳跃表就是为了实现高效范围查询而设计的索引型、多层结构。
核心概念:
跳跃表(SkipList)的本质是在普通有序链表的基础上,添加了多级索引来加速查找。它可以看作是一种“以空间换时间”的算法。
想象一下《新华字典》:
普通链表:一页一页翻着找,效率是O(N)。
跳跃表:
第一级索引:拼音首字母目录(比如A, B, C...章)
第二级索引:每个字母章内部的更细分类(比如A章又分为A, Ai, An...节)
第三级索引:...直到最后的详细页码(数据本身)
通过这种多级“目录”,你可以快速跳过大量不需要的页面,使查找效率逼近二分查找。
3.4 结构转换
当一个有序集合满足以下条件时,会使用跳跃表+字典(dict
)的组合来实现:
zset-max-listpack-entries 128
含义:当有序集合中的元素(成员)数量小于或等于128个时,会使用listpack来存储。
为什么:对于少量元素,listpack这种紧凑的、连续的内存结构非常节省内存,且效率足够高。
超过时:如果元素数量大于128,Redis会自动将底层实现从listpack转换为跳跃表(skiplist) + 字典(dict) 的组合。
zset-max-listpack-value 64
含义:当有序集合中任何一个成员(member)的字符串长度小于或等于64字节时,会使用listpack来存储。
为什么:较短的字符串可以高效地编码并存储在listpack中。
超过时:如果任何一个成员的字符串长度大于64字节(即使总元素数量很少),Redis也会自动将底层实现转换为跳跃表 + 字典。
两个条件是“或”的关系。只要违反其中任意一个,有序集合的底层编码就会升级为跳跃表。