ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

【Redis 初阶】ZSet 类型深度解析:跳表支撑的有序集合与排行榜实战

2026/9/4 8:46:05 拓冰建站 浏览量
【Redis 初阶】ZSet 类型深度解析:跳表支撑的有序集合与排行榜实战 草莓熊Lotso个人主页❄️个人专栏:《C知识分享》 《Linux 入门到实践零基础也能懂》✨生活是默默的坚持毅力是永久的享受 博主简介文章目录前言一. ZSet 类型基本介绍1.1 核心排序规则二. ZSet 核心命令全解2.1 zadd添加与更新元素2.2 zcard / zcount2.3 范围查询zrange /zrevrange/zrangebyscore2.4 弹出操作zpopmax /bzpopmax/zpopmin /bzpopmin2.5 排名与分数查询zrank /zrevrank/zscore2.6 删除操作zrem /zremrangebyrank/zremrangebyscore2.7 zincrby原子增减分数2.8 集合运算zinterstore /zunionstore2.9 命令小结三. ZSet 底层编码实现3.1 ziplist 压缩列表3.2 skiplist 跳表四. ZSet 典型应用场景4.1 实时排行榜系统4.2 多维度加权排行榜4.3 带优先级的阻塞队列结尾前言前面我们讲完了 Set 集合它的核心能力是去重但无法处理需要排序的场景。今天我们来看 Redis 最具特色的数据结构 ——ZSet有序集合。它在 Set 元素唯一的基础上为每个元素关联了一个分数 score实现了按分数自动排序既能高效单点查询又能灵活做范围排行是面试中跳表、排行榜场景的高频考点。很多人用 ZSet 只会写排行榜 Demo却对 zadd 的众多选项、分数区间的开闭规则、底层跳表的设计逻辑一知半解。本文从基础特性讲起逐个拆解核心命令与踩坑细节深入 ziplist 与跳表的底层编码再结合排行榜等经典场景讲透实战用法带你把 ZSet 从 “会用” 吃透到 “懂原理”。一. ZSet 类型基本介绍在正式讲命令之前先把三种集合类结构做个清晰的区分避免概念混淆数据结构元素是否可重复有序性排序依据典型场景List是位置有序插入顺序 / 下标时间轴、消息队列Set否无序无标签、去重、集合运算ZSet否排序有序分数 score排行榜、优先级队列1.1 核心排序规则ZSet 的 “有序” 和 List 的有序完全不同List 是插入位置有序顺序调换就是不同的列表ZSet 是按分数大小升序 / 降序排列和插入顺序无关。排序依据每个元素member对应一个浮点型分数 score按分数从小到大升序排列元素唯一分数可重复member 不允许重复但多个元素可以有相同的分数同分处理规则分数相同时按照 member 字符串的字典序排序内部默认升序ZSet 内部存储默认按升序排列降序查询只是反向遍历。注意不要把 member 和 score 理解成键值对。键值对是单向查找按键找值但 ZSet 既可以通过 member 查分数也可以通过分数范围查 member是双向可查的。二. ZSet 核心命令全解ZSet 的命令比较多我们按功能分组逐一讲解同时标注时间复杂度和版本差异。2.1 zadd添加与更新元素ZADD key[NX|XX][GT|LT][CH][INCR]score member[score member...]这是 ZSet 最核心的写入命令也是参数最多的命令之一。有个反直觉的点需要注意分数在前元素在后和我们习惯的 “键值对” 顺序相反。核心选项说明NX仅添加新元素不更新已存在的元素XX仅更新已存在的元素不添加新元素CH修改返回值计数规则。默认只统计新增元素个数加 CH 后统计所有发生变化的元素新增 分数更新INCR对元素的分数做增量操作效果等同于zincrby此时只能指定一个元素。GT / LT分别表示 “新分数大于当前值才更新” 和 “新分数小于当前值才更新”Redis 6.2 及以上版本支持5.x 版本不可用。默认情况下不加 NX/XX元素不存在就新增元素存在就更新分数自动调整位置保持有序。 时间复杂度每个元素 O (logN)N 为集合总元素数。# 基础添加127.0.0.1:6379zadd rank99吕布98赵云96典韦95关羽(integer)4# 查看结果带分数127.0.0.1:6379zrange rank0-1withscores1)关羽2)953)典韦4)965)赵云6)987)吕布8)992.2 zcard / zcountzcard获取元素总数ZCARD key返回有序集合的元素总个数时间复杂度O(1)底层有内置计数器直接读取。zcount统计分数区间元素数ZCOUNT key min max统计分数在 [min, max] 区间内的元素个数时间复杂度O(logN)。区间开闭规则默认是闭区间包含边界值如果想排除某个边界在值前面加(表示开区间。# 闭区间包含95和97127.0.0.1:6379zcount rank9597(integer)3# 排除95包含97127.0.0.1:6379zcount rank(9597(integer)2# 两边都排除127.0.0.1:6379zcount rank(95(97(integer)1这个(的写法确实不符合直觉但因为历史兼容性问题Redis 不会轻易修改这种广泛使用的语法 —— 一旦改了所有线上老代码都可能出问题成本极高。这也是成熟基础软件的共性宁可保留不完美的设计也要保证向后兼容。另外分数支持两个特殊值-inf负无穷、inf正无穷用来表示 “小于所有值” 和 “大于所有值”。# 统计所有元素等价于zcardzcount rank-infinf为什么 zcount 是 O (logN) 而不是 O (N) 因为跳表的每个节点记录了自己的排名跨度找到 min 对应元素拿到排名找到 max 对应元素拿到排名两个排名相减就得到数量全程不需要遍历区间元素。2.3 范围查询zrange /zrevrange/zrangebyscorezrange按下标范围查询升序ZRANGE key start stop[WITHSCORES]按下标区间返回元素左闭右闭支持负下标加WITHSCORES同时返回分数。 时间复杂度O (logN M)M 为返回的元素个数。# 查询前3名升序分数最低的3个zrange rank02withscoreszrevrange按下标范围查询降序反向按分数从高到低返回参数规则和 zrange 一致。版本说明Redis 6.2 之后zrevrange、zrangebyscore 等功能逐步合并到 zrange 命令中通过 BYSCORE、REV 等选项控制旧命令标记为弃用但仍然兼容。zrangebyscore按分数范围查询ZRANGEBYSCORE key min max[WITHSCORES]按分数区间返回元素规则和 zcount 一致支持(开区间和 -inf/inf。2.4 弹出操作zpopmax /bzpopmax/zpopmin /bzpopminzpopmax / zpopminZPOPMAX key[count]ZPOPMIN key[count]删除并返回分数最高 / 最低的元素可指定 count 一次弹出多个。 时间复杂度O (logN * M)M 为弹出个数。如果多个元素分数同为最大值会按 member 的字典序选择弹出哪一个。一个有意思的工程细节最大值就是跳表的尾节点理论上可以记录尾指针实现 O (1) 删除但 Redis 实际用了通用删除函数还是走了一遍查找流程。这并不是疏漏而是工程上的取舍logN 本身已经足够快为了极端场景单独做优化收益不高还会增加代码复杂度。优化要做在真正的性能瓶颈上。bzpopmax / bzpopmin阻塞版本的弹出命令集合为空时客户端会阻塞等待直到有新元素插入或者超时。BZPOPMAX key[key...]timeout支持同时监听多个 key哪个集合先有元素就返回哪个timeout 单位为秒支持小数设为 0 表示永久等待非常适合实现阻塞优先级队列。2.5 排名与分数查询zrank /zrevrank/zscorezrank / zrevrankZRANK key member ZREVRANK key member返回指定元素的排名下标zrank 按升序算zrevrank 按降序算。元素不存在返回 nil。 时间复杂度O (logN)。zscoreZSCORE key member查询指定元素对应的分数。 时间复杂度O(1)。按说在跳表里找元素应该是 O (logN)为什么这里是 O (1)因为 Redis 做了空间换时间的优化额外用一个哈希表dict保存了 member 到节点的映射查分数直接从哈希表里拿不需要遍历跳表。这也是典型的工程优化思路高频查询用额外空间换性能。2.6 删除操作zrem /zremrangebyrank/zremrangebyscorezrem按元素删除ZREM key member[member...]删除一个或多个指定元素返回成功删除的个数。 时间复杂度O (k * logN)k 为删除元素个数。zremrangebyrank按排名范围删除ZREMRANGEBYRANK key start stop删除排名在 [start, stop] 区间内的所有元素左闭右闭。 时间复杂度O (logN M)M 为删除元素数。zremrangebyscore按分数范围删除ZREMRANGEBYSCORE key min max删除分数在指定区间内的所有元素同样支持(开区间语法。 时间复杂度O (logN M)。2.7 zincrby原子增减分数ZINCRBY key increment member为指定元素的分数加上增量 increment负数即为减法。操作后自动调整元素位置保持有序。 时间复杂度O (logN)。 这是实现实时排行榜最核心的命令用户分数变化时原子更新不会有并发问题。2.8 集合运算zinterstore /zunionstoreSet 有交、并、差三种运算ZSet 同样支持集合运算且因为带分数运算规则更丰富。版本说明Redis 6.2 新增了 zinter、zunion 直接返回结果的命令5.x 版本只有 store 版本即运算结果存入新的 key。zinterstore交集运算并存储ZINTERSTORE destination numkeys key[key...][WEIGHTS weight...][AGGREGATE SUM|MIN|MAX]参数说明numkeys明确指定参与运算的 key 个数。这个设计和 HTTP 的 Content-Length 思路一致 —— 通过显式声明长度避免后续的参数和选项混淆解决 “粘包” 式的歧义问题。WEIGHTS权重系数每个集合的分数会乘以对应的权重再参与运算AGGREGATE分数聚合方式默认 SUM求和可选 MIN取最小、MAX取最大。示例# 两个集合求交集分数求和zinterstore result2set1 set2# 带权重的交集set1权重2set2权重3zinterstore result2set1 set2 weights23时间复杂度大致为 O (NK MlogM)实际开发中不需要死记硬背理解运算逻辑即可。zunionstore并集运算并存储参数规则和 zinterstore 完全一致计算的是多个有序集合的并集相同元素的分数按聚合规则合并。2.9 命令小结命令作用时间复杂度zadd key score member …添加 / 更新元素O (k*logN)k 为元素数zcard key获取元素总数O(1)zscore key member查询元素分数O(1)zrank / zrevrank key member查询元素排名O(logN)zrem key member …删除指定元素O(k*logN)zincrby key incr member原子增减分数O(logN)zrange / zrevrange按下标范围查询O(logN M)zrangebyscore按分数范围查询O(logN M)zcount key min max统计分数区间元素数O(logN)zremrangebyrank按排名范围删除O(logN M)zremrangebyscore按分数范围删除O(logN M)zpopmax / zpopmin弹出最值元素O(logN * M)bzpopmax / bzpopmin阻塞弹出最值O(logN)zinterstore交集运算并存储O(N*K MlogM)zunionstore并集运算并存储O(N MlogM)三. ZSet 底层编码实现和其他数据类型一样ZSet 也会根据数据量自动切换底层编码在空间和性能之间做权衡。一共有两种编码ziplist压缩列表和skiplist跳表。3.1 ziplist 压缩列表当同时满足两个条件时使用 ziplist 编码元素个数小于zset-max-ziplist-entries默认 128 个每个元素的长度小于zset-max-ziplist-value默认 64 字节。小数据量下用连续内存紧凑存储省去跳表的多层指针和哈希表开销内存利用率非常高。代价是读写需要遍历元素多了性能下降明显。3.2 skiplist 跳表只要不满足 ziplist 的任意一个条件就自动切换为跳表编码。 跳表是 ZSet 的核心实现通过多层有序链表实现 O (logN) 的查找效率同时天然支持范围查询。这也是面试中的高频考点后面源码视角会详细拆解。可以通过OBJECT encoding验证编码切换# 元素少使用ziplist127.0.0.1:6379zadd smallset10a20b30c(integer)3127.0.0.1:6379OBJECT encoding smallsetziplist# 加入长元素触发切换127.0.0.1:6379zadd smallset40aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(integer)1127.0.0.1:6379OBJECT encoding smallsetskiplist还是那句话记思想不记数字。阈值可配置、版本有差异理解 “小数据省空间、大数据保性能” 的策略才是关键。源码视角跳表的设计智慧站在数据结构的角度跳表是非常精巧的设计用非常简洁的思路实现了平衡树的性能还在范围查询上更有优势。跳表的基本结构普通单链表查找只能从头遍历时间复杂度 O (N)。跳表的思路是 “加索引”第 0 层原始全量链表包含所有元素第 1 层一级索引每隔几个元素选一个节点第 2 层二级索引在一级索引的基础上再抽样以此类推层数越高节点越稀疏。查找时从最高层开始向右走不动了就往下走一层一直降到第 0 层找到目标节点。整个过程类似二分查找时间复杂度 O (logN)。为什么用跳表不用红黑树这是非常经典的面试题核心原因有两个范围查询更高效跳表本质还是链表找到区间起点后直接沿着第 0 层向后遍历就能拿到所有范围元素红黑树是树结构范围遍历需要中序遍历实现更复杂缓存局部性也更差。实现与维护更简单插入删除不需要做复杂的旋转平衡操作只需要修改对应层的指针代码实现难度远低于红黑树出 bug 的概率更低。当然跳表也有缺点因为有多层索引内存占用比普通链表高。但对于 Redis 来说这点空间换性能的代价完全值得。两个关键工程优化跨度span记录每个节点不仅存了指针还记录了指针跨越的节点数。这样查询排名、计算区间元素数量时只需要累加沿途的跨度值就能得到排名不需要遍历元素这也是 zcount、zrank 能做到 O (logN) 的根本原因。哈希表辅助映射额外维护一个 dict 哈希表保存 member 到跳表节点的映射。这样按 member 查分数zscore直接 O (1) 命中不需要从跳表顶层往下找。用少量额外内存把最常用的查询优化到了极致。四. ZSet 典型应用场景4.1 实时排行榜系统这是 ZSet 最核心、最广泛的应用场景比如游戏天梯榜、微博热搜榜、商品销量榜、学生成绩排行等。实现思路以用户 ID / 内容 ID 作为 member对应的分数战力、热度、销量作为 score分数变化时用zincrby原子更新自动调整排名查 TopN 用zrevrange查用户排名用zrevrank查分数段用户用zrangebyscore。很多人会担心全服几百万玩家排行榜放 Redis 内存装得下吗我们做个简单估算 一个玩家记录member 按 4 字节用户 ID 算score 是 8 字节 double再加上跳表指针开销平均一个玩家几十字节。就算 1 亿玩家总内存也就一两 GB对于现代服务器来说完全是小意思。直觉上很大的数据对计算机而言其实微不足道。4.2 多维度加权排行榜很多排行榜不是单一维度比如微博热度 浏览量 点赞量 转发量 评论量每个维度权重不同。实现思路每个维度单独建一个 ZSetmember 都是微博 IDscore 是对应维度的数值用zunionstore设置不同的 WEIGHTS 权重加权求和得到综合热度分结果集合就是最终的热度排行榜。这种方案的优势是权重调整非常灵活不需要修改业务代码改权重参数重新计算即可。实际公司里会有专门的算法团队调优权重和热度公式Redis 的集合运算正好承接落地。4.3 带优先级的阻塞队列用bzpopmax可以实现阻塞优先级队列分数表示任务优先级高优先级任务先被消费。相比于普通 List 实现的消息队列多了优先级能力适合有轻重缓急的任务调度场景。核心考点总结核心特性元素唯一不重复按分数排序同分按字典序member 与 score 双向可查。核心命令zadd 的 NX/XX/CH/INCR 选项zcount 的开闭区间规则zscore 为 O (1) 的原因集合运算的 numkeys、权重、聚合参数。时间复杂度核心增删查改均为 O (logN)zcard、zscore 为 O (1)范围操作为 O (logN M)。底层编码ziplist 与 skiplist 的切换条件、各自的适用场景与优劣。跳表原理多层索引的设计思路、O (logN) 查找的原理、相比红黑树的优势、跨度与哈希表的优化细节。应用场景实时排行榜、多维度加权排行、优先级队列的实现思路。设计思想时空权衡、空间换时间优化高频查询、向后兼容的工程取舍。结尾 我是草莓熊 Lotso若这篇技术干货帮你打通了学习中的卡点 【关注】跟我一起深耕技术领域从基础到进阶见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好需要时直接查、随时用 【评论】分享你的经验或疑问比如曾踩过的技术坑一起交流避坑 ️ 【投票】用你的选择助力社区内容方向告诉大家哪个技术点最该重点拆解 技术之路难免有困惑但同行的人会让前进更有方向愿我们都能在自己专注的领域里一步步靠近心中的技术目标结语ZSet 可以说是 Redis 设计最精巧的数据结构之一跳表的经典结构、空间换时间的优化、丰富的排序与范围能力让它在排行榜、优先级调度等场景里几乎是最优解。理解它的底层原理不仅能帮你应对面试更能在业务中做出更合理的技术选型。到这里Redis 的五大基础数据类型我们就全部拆解完了。后续我们会继续深入 Redis 的高级特性与核心原理。✨把这些内容吃透超牛的放松下吧✨ʕ˘ᴥ˘ʔづきらど