文章目录第一章Redis 有序集合Zset深度解析与实战1. 核心概念与底层结构剖析1.1 为什么选择 Zset1.2 底层数据结构的巧妙设计2. 基础操作元素的增删改查2.1 添加或更新元素ZADD2.2 获取元素数量与分数ZCARD 与 ZSCORE2.3 分数增减与删除操作ZINCRBY 与 ZREM3. 范围检索与排行计算按序获取元素3.1 按索引/排名查询ZRANGE 与 ZREVRANGE3.2 获取排名ZRANK 与 ZREVRANK3.3 按分数查询ZCOUNT 与 ZRANGEBYSCORE3.4 按照范围删除ZREMRANGEBYRANK 与 ZREMRANGEBYSCORE4. 优先级队列实战弹出最值元素4.1 删除并返回最值ZPOPMAX 与 ZPOPMIN4.2 阻塞式弹出BZPOPMAX 与 BZPOPMIN5. 高级聚合操作集合间的交集与并集5.1 交集操作ZINTERSTORE5.2 并集操作ZUNIONSTORE第二章Zset 内部编码机制与底层结构解析补充跳表a. 基础形态普通有序链表b. 进化形态增加一级索引c. 完全形态多级索引真正的跳表1. 内存与性能的博弈双编码机制1.1 压缩列表ziplist极致的内存节省1.2 跳表skiplist高效的数据操作2. 编码切换的临界点与参数配置2.1 核心配置参数2.2 触发切换的具体条件3. 编码转换实战演示3.1 初始状态与元素较少场景3.2 突破元素数量阈值3.3 突破元素大小阈值3.4 不可逆的转换机制4. 深入跳表SkipList为什么是跳表跳表的本质复杂的链表第三章Zset 实战应用场景与架构思考3.1 基础排行榜以“每日点赞热榜”为例3.1.1 赞数的初始化与动态更新3.1.2 榜单的清理与展示3.1.3 联合 Hash 获取完整业务数据3.2 内存容量估算海量数据真的放得下吗3.3 复杂维度排行榜基于权重的热度计算3.4 拓展思考Zset 是唯一的选择吗第一章Redis 有序集合Zset深度解析与实战在 Redis 的五种基础数据结构中有序集合Sorted Set通常简称为 Zset是功能最强大、也最具特色的数据结构之一。它不仅保留了集合Set中元素不能重复的特性还巧妙地为每个元素引入了一个浮点数类型的分数score从而实现了自动排序。这种特性使得 Zset 成为解决排行榜、优先级队列、延时任务等实际业务场景的利器。本章将深入剖析 Zset 的底层机制、所有核心命令及其高阶用法。1. 核心概念与底层结构剖析1.1 为什么选择 Zset在探讨 Zset 之前我们先回顾一下 List、Set 和 Zset 三者的异同点数据结构是否允许重复元素是否有序有序依据典型应用场景列表 (List)是是索引下标时间轴、消息队列集合 (Set)否否无标签、社交关系有序集合 (Zset)否是分数 (score)排行榜系统、优先级队列Zset 中的每个元素member都是唯一的但与之绑定的分数score允许重复。在 Zset 中元素与分数不能简单地理解为传统的“键值对Key-Value”而应理解为一种pair结构类似于 C 中的std::pair。其中member 是元素的核心主体score 只是辅助排序的工具。例如在一场考试中每个考生member是唯一的但他们的分数score允许相同。Redis 的 ZSet有序集合 按照元素的score分数从小到大排序如果多个元素的score相同则按照元素的member成员进行字典序排列。这里的字典序本质上是按照成员字符串的字节进行比较而不是按照插入顺序排列。因此ZSet 的排序主要由score决定score相同时再由member决定与元素的插入顺序无关。1.2 底层数据结构的巧妙设计Zset 为什么能在数据量庞大的情况下依然保持高效的增删改查这得益于其底层采用了跳表SkipList与哈希表Hash Table的组合结构。哈希表负责保存元素到分数的映射。这使得像ZSCORE这样直接获取某个元素分数的操作时间复杂度仅为O(1)。跳表负责维护元素按分数排序的顺序。跳表通过多级索引的链表结构使得查找、插入、删除的平均时间复杂度都控制在O(log(N))。由于底层是跳表而非数组Zset 的添加元素操作的时间复杂度是O(logN)相比于 Hash、Set、List 的O(1)会略慢但为了获得强大的有序性这种微小的性能损耗是完全值得的。2. 基础操作元素的增删改查2.1 添加或更新元素ZADDZADD是 Zset 中最核心的命令。它不仅用于添加新元素也能更新已存在元素的分数。语法格式ZADD key [NX | XX] [GT | LT] [CH] [INCR] score member [score member ...]核心选项解析XX仅用于更新已经存在的元素不会添加新元素。NX仅用于添加新元素不会更新已经存在的元素。GT / LT用于控制更新条件。GT表示仅当新分数大于当前分数时才更新LT表示仅当新分数小于当前分数时才更新。CH默认情况下ZADD返回的是本次新增的元素个数。指定此选项后返回值将包含新增及更新的元素总数。INCR将命令转化为类似ZINCRBY的效果给指定元素的分数加上指定的值。注意使用此选项时只能操作一个元素。命令有效版本1.2.0 之后时间复杂度O(log(N))N 为当前有序集合的元素个数返回值默认是本次添加成功的元素个数。实战示例# 正常添加元素 redis ZADD myzset 1 one (integer) 1 redis ZADD myzset 1 uno (integer) 1 redis ZADD myzset 2 two 3 three (integer) 2 # 使用 CH 选项此时更新已有元素也会被计入返回值 (更新了三个) redis ZADD myzset CH 100 one 200 two 300 three (integer) 3 # 使用 NX 选项只添加不更新返回值为新增的个数2个新元素 redis ZADD myzset NX 100 one 200 two 300 three 400 four 500 five (integer) 2 # 使用 INCR 选项相当于 ZINCRBY直接返回运算后的新分数 redis ZADD myzset INCR 10 one 11 # 分数可以是正负无穷大 redis ZADD myzset -inf negative infinity inf positive infinity (integer) 22.2 获取元素数量与分数ZCARD 与 ZSCOREZCARD获取 Zset 的基数即元素个数。语法ZCARD key时间复杂度O(1)返回值zset 内的元素个数。ZSCORE返回指定元素的分数。语法ZSCORE key member时间复杂度O(1)得益于底层 Hash 结构返回值指定元素的分数字符串格式。实战示例redis ZADD myzset 1 one redis ZADD myzset 2 two redis ZCARD myzset (integer) 2 redis ZSCORE myzset one 1 # 查询不存在的元素返回 nil redis ZSCORE myzset unknown (nil)2.3 分数增减与删除操作ZINCRBY 与 ZREMZINCRBY为指定元素的关联分数加上指定的增量increment。如果元素不存在则会创建该元素。语法ZINCRBY key increment member时间复杂度O(log(N))返回值增加后元素的分数。ZREM删除指定的一个或多个元素。语法ZREM key member [member ...]时间复杂度O(M*log(N))M 为删除元素的个数N 为当前集合元素个数返回值本次操作删除的元素个数。实战示例redis ZADD myzset 1 one redis ZADD myzset 2 two redis ZINCRBY myzset 2 one 3 # 删除单个元素 redis ZREM myzset two (integer) 1 # 批量删除 redis ZADD myzset 3 three 4 four redis ZREM myzset three four (integer) 23. 范围检索与排行计算按序获取元素Zset 提供了多种维度的查询方式包括基于下标排名的查询和基于分数的查询。3.1 按索引/排名查询ZRANGE 与 ZREVRANGEZRANGE返回指定区间内的元素分数按升序排列。区间索引start和stop从 0 开始支持负数-1 表示最后一个。带上WITHSCORES可同时返回分数。语法ZRANGE key start stop [WITHSCORES]时间复杂度O(log(N)M)M 为返回的元素个数ZREVRANGE与ZRANGE类似但按照分数降序返回。注意此命令在 Redis 6.2.0 之后已被废弃功能合并到了ZRANGE的REV参数中。语法ZREVRANGE key start stop [WITHSCORES]时间复杂度O(log(N)M)返回值区间内的元素列表按分数从大到小排列。实战示例redis ZADD myzset 1 one redis ZADD myzset 2 two redis ZADD myzset 3 three # 获取全部元素升序并带分数 redis ZRANGE myzset 0 -1 WITHSCORES 1) one 2) 1 3) two 4) 2 5) three 6) 3 # 获取全部元素降序并带分数 redis ZREVRANGE myzset 0 -1 WITHSCORES 1) three 2) 3 3) two 4) 2 5) one 6) 1 # 获取排名最后两个元素降序 redis ZREVRANGE myzset -2 -1 1) three 2) two3.2 获取排名ZRANK 与 ZREVRANKZRANK返回指定元素的升序排名从小到大从 0 开始。语法ZRANK key member时间复杂度O(log(N))返回值排名不存在则返回 nil。ZREVRANK返回指定元素的降序排名从大到小。语法ZREVRANK key member时间复杂度O(log(N))返回值排名不存在则返回 nil。实战示例redis ZADD myzset 1 one redis ZADD myzset 2 two redis ZADD myzset 3 three # three 的升序排名是 2 (从0开始算one是0two是1) redis ZRANK myzset three (integer) 2 # one 的降序排名是 2 (从大到小three是0two是1) redis ZREVRANK myzset one (integer) 2 # 查询不存在的元素 redis ZREVRANK myzset four (nil)3.3 按分数查询ZCOUNT 与 ZRANGEBYSCOREZCOUNT统计分数在min和max之间的元素个数。语法ZCOUNT key min max边界控制默认是包含边界闭区间。可以通过在值前加上(来排除边界如(1表示大于 1。特殊值支持inf正无穷和-inf负无穷注意无穷大本身不是有效数字但可作为边界参数。时间复杂度O(log(N))。Zset 内部记录了每个节点的跨度用于计算排名因此只需找到 min 和 max 对应的节点利用它们的排名相减即可得出结果无需遍历元素。ZRANGEBYSCORE返回分数在min和max之间的元素。语法ZRANGEBYSCORE key min max [WITHSCORES]时间复杂度O(log(N)M)。注意此命令在 6.2.0 之后废弃被整合进新的ZRANGE。实战示例# 1. 创建有序集合 myzset指定三个元素及其分数 redis ZADD myzset 1 one 2 two 3 three (integer) 3 # 2. 统计所有元素的数量-inf 和 inf 分别表示负无穷和正无穷 redis ZCOUNT myzset -inf inf (integer) 3 # 3. 统计分数在 (1, 3] 范围内的元素数量即大于 1 且小于等于 3 redis ZCOUNT myzset (1 3 (integer) 2 # 4. 统计分数在 (1, 3) 范围内的元素数量即大于 1 且小于 3 redis ZCOUNT myzset (1 (3 (integer) 1 # 5. 获取分数在 [1, 2] 范围内的元素默认按照分数从小到大返回 redis ZRANGEBYSCORE myzset 1 2 1) one 2) two # 6. 获取分数在 (1, 3] 范围内的元素 redis ZRANGEBYSCORE myzset (1 3 1) two 2) three # 7. 获取所有元素及其分数 redis ZRANGEBYSCORE myzset -inf inf WITHSCORES 1) one 2) 1 3) two 4) 2 5) three 6) 33.4 按照范围删除ZREMRANGEBYRANK 与 ZREMRANGEBYSCOREZREMRANGEBYRANK按照排序升序删除指定排名区间左闭右闭的元素。语法ZREMRANGEBYRANK key start stop时间复杂度O(log(N)M)返回值本次操作删除的元素个数。ZREMRANGEBYSCORE按照分数区间左闭右闭删除元素。语法ZREMRANGEBYSCORE key min max时间复杂度O(log(N)M)返回值本次操作删除的元素个数。实战示例redis ZADD myzset 1 one redis ZADD myzset 2 two redis ZADD myzset 3 three # 删除排名在 [0, 1] 的元素即 one 和 two redis ZREMRANGEBYRANK myzset 0 1 (integer) 2 # 查看剩余元素 redis ZRANGE myzset 0 -1 WITHSCORES 1) three 2) 3 # 重新填充数据 redis ZADD myzset 1 one 2 two # 按分数删除删除 (1, 3] 之间的元素即 three redis ZREMRANGEBYSCORE myzset (1 3 (integer) 1 # 查看剩余元素 redis ZRANGE myzset 0 -1 WITHSCORES 1) one 2) 14. 优先级队列实战弹出最值元素Zset 天然具有根据优先级分数排序的特性因此非常适合用作优先级队列。4.1 删除并返回最值ZPOPMAX 与 ZPOPMINZPOPMAX删除并返回分数最高的count个元素。如果不指定count默认弹出 1 个。语法ZPOPMAX key [count]时间复杂度O(log(N) * M)M 为弹出的个数返回值元素和分数的列表。注意其返回结构体类似 C 语言中的struct包含 member 和 score内存中的处理涉及到字符串和内存拷贝。ZPOPMIN删除并返回分数最低的count个元素。语法ZPOPMIN key [count]时间复杂度O(log(N) * M)返回值元素和分数的列表。排序细节当集合中存在多个分数相同的元素且同时为最高/最低分时Redis 会根据 member 的字典序strcmp函数比较来决定弹出顺序。分数是主要因素分数相同时则按字典序。为何不优化到 O(1)理论上Zset 的底层是跳表如果我们记录下头节点和尾节点的位置删除它们似乎是O(1)的。但 Redis 并没有这样做而是采用了通用的删除逻辑先查找再删除因此复杂度是O(logN)。作者认为在这种场景下logN的速度并不慢且通用逻辑能保持代码的简洁和统一为了避免极端的代码分支放弃了这种微小的优化。实战示例redis ZADD myzset 1 one redis ZADD myzset 2 two redis ZADD myzset 3 three # 弹出分数最高的 1 个元素 redis ZPOPMAX myzset 1) three 2) 3 # 重新填充数据 redis ZADD myzset 4 four # 弹出分数最低的 2 个元素 redis ZPOPMIN myzset 2 1) one 2) 1 3) two 4) 24.2 阻塞式弹出BZPOPMAX 与 BZPOPMIN这是ZPOPMAX和ZPOPMIN的阻塞版本。如果指定的集合中没有元素客户端会被阻塞直到超时超时时间设置为 0 表示永久阻塞或等到有其他客户端插入了新元素。BZPOPMAX语法BZPOPMAX key [key ...] timeout时间复杂度O(log(N))返回值包含 key, member, score 的数组。BZPOPMIN语法BZPOPMIN key [key ...] timeout时间复杂度O(log(N))返回值包含 key, member, score 的数组。实战示例redis DEL zset1 zset2 (integer) 0 redis ZADD zset1 0 a 1 b 2 c # 立即弹出返回包含 key, member, score 的数组 redis BZPOPMAX zset1 zset2 0 1) zset1 2) c 3) 2 # BZPOPMIN 示例 redis DEL zset1 zset2 (integer) 0 redis ZADD zset1 0 a 1 b 2 c redis BZPOPMIN zset1 zset2 0 1) zset1 2) a 3) 05. 高级聚合操作集合间的交集与并集Zset 支持对多个集合进行数学运算交、并、差并在合并过程中对分数进行特殊的聚合计算。5.1 交集操作ZINTERSTORE求出给定有序集合中元素的交集并保存进目标有序集合中。语法格式ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM | MIN | MAX]numkeys用于指定参与计算的 key 的个数Redis 需要这个参数来明确参数的边界。WEIGHTS权重。为每个输入集合指定一个权重系数元素的分数在参与聚合前会先乘以对应的权重。AGGREGATE/ˈæɡrɪɡət/聚合方式。默认是SUM相加也可以是MIN取最小或MAX取最大。时间复杂度O(N*K) O(M*log(M))。其中 N 是最小集合的元素个数K 是输入集合的个数M 是结果集的元素个数。面试视角这属于典型的算法复杂度分析。如果 K 很小通常为 2~3可以近似认为O(M) O(M*logM)化简后近似为O(M*logM)。面试中更看重的是分析问题的能力而不是死记硬背公式。实战示例redis ZADD zset1 1 one redis ZADD zset1 2 two redis ZADD zset2 1 one redis ZADD zset2 2 two redis ZADD zset2 3 three # 计算交集zset1 的分数乘 2zset2 的分数乘 3分数取和 (SUM) redis ZINTERSTORE out 2 zset1 zset2 WEIGHTS 2 3 (integer) 2 redis ZRANGE out 0 -1 WITHSCORES 1) one 2) 5 # (1*2 1*3) 3) two 4) 10 # (2*2 2*3)5.2 并集操作ZUNIONSTORE求出给定有序集合中元素的并集并保存进目标有序集合中。用法与ZINTERSTORE完全一致仅计算逻辑并集不同。语法格式ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM | MIN | MAX]时间复杂度O(N) O(M*log(M))。这里的 N 是所有输入集合的总元素个数因为并集需要遍历所有集合。实战示例redis ZADD zset1 1 one redis ZADD zset1 2 two redis ZADD zset2 1 one redis ZADD zset2 2 two redis ZADD zset2 3 three # 计算并集权重分别为 2 和 3 redis ZUNIONSTORE out 2 zset1 zset2 WEIGHTS 2 3 (integer) 3 redis ZRANGE out 0 -1 WITHSCORES 1) one 2) 5 3) three 4) 9 # (0*2 3*3) 5) two 6) 10第二章Zset 内部编码机制与底层结构解析在 Redis 中为了在内存占用与操作效率之间取得最佳平衡有序集合Zset并没有单一地依赖于某一种底层数据结构而是采用了双编码机制ziplist压缩列表和skiplist跳表。Redis 会在底层根据当前集合的实际状态在这两种编码之间进行动态切换这是 Redis 能够在有限内存中支撑海量数据的关键设计。补充跳表用图片图解来直观展示跳表Skiplist的结构和查找过程是最清晰的。你可以把跳表想象成“带有多级目录的有序链表”。假设我们有一组有序数据1, 3, 5, 7, 9, 11, 13, 15。a. 基础形态普通有序链表如果只有最底层的一层链表查找11需要从头顺着指针一个一个找需要遍历 6 次时间复杂度是O(N)。[Head] - [1] - [3] - [5] - [7] - [9] - [11] - [13] - [15]b. 进化形态增加一级索引为了提高查找速度我们每隔一个节点提取一个节点作为**“索引”**。查找11时先在索引层跳跃从3跳到7再跳到11。然后顺着指针下降到原始链表找到11。查找步数减少。一级索引: [3] ------- [7] ------- [11] ------- [15] | | | | 原始链表: [Head] - [1] - [3] - [5] - [7] - [9] - [11] - [13] - [15]c. 完全形态多级索引真正的跳表为了达到O(logN)的效率我们继续往上提取索引形成多级结构。此时查找11的路径变得极短二级索引: [7] --------------------- [15] | | 一级索引: [3] ------- [7] ------- [11] ------- [15] | | | | 原始链表: [Head] - [1] - [3] - [5] - [7] - [9] - [11] - [13] - [15]查找11的路线图从最高层二级索引开始比较7比目标小向右走到15。发现15比11大说明目标在7和15之间于是向下降到一级索引。在一级索引中从7向右走到11找到了如果没找到具体值就继续向下走到原始链表。1. 内存与性能的博弈双编码机制对于有序集合来说底层编码的选择主要取决于“数据量”与“单元素体积”。Redis 默认的策略是在数据量较小时优先追求极致的内存节省在数据量变大时则优先保证操作的高效性。1.1 压缩列表ziplist极致的内存节省当有序集合中元素个数较少或者单个元素体积较小时Redis 会使用 ziplist来存储。什么是 ziplist它是一块连续的内存空间将所有元素紧挨着存放在一起。它不需要像链表那样为每个节点维护前后指针prev/next从而极大地减少了指针带来的内存开销和内存碎片。优势压缩列表的最大意义在于节省内存空间。在元素数量不多的情况下连续内存的遍历速度也非常快。1.2 跳表skiplist高效的数据操作如果当前元素个数比较多或者单个元素体积非常大继续使用ziplist就会导致严重的性能问题因为每次插入、删除都可能引发大块内存的重新分配和数据的整体移动。此时Redis 会将底层实现转换为skiplist。优势跳表能够提供O(logN)的查询、插入和删除效率且非常适合范围查询完美契合了数据量较大时的性能需求。2. 编码切换的临界点与参数配置Redis 并非随意切换编码而是通过redis.conf配置文件中的两个关键参数来进行严格把控2.1 核心配置参数zset-max-ziplist-entries控制有序集合中允许使用ziplist编码的最大元素个数默认值为128。zset-max-ziplist-value控制有序集合中允许使用ziplist编码的单个元素最大字节数默认值为64 字节。2.2 触发切换的具体条件只有同时满足以下两个条件时Zset 的内部编码才会是ziplist元素个数 zset-max-ziplist-entries默认 128 个。每个元素的值长度 zset-max-ziplist-value默认 64 字节。只要上述条件中任意一个不满足Redis 就会立即将内部编码转换为skiplist3. 编码转换实战演示我们可以使用OBJECT ENCODING命令来实时观察 Zset 的内部编码状态。3.1 初始状态与元素较少场景当元素个数少且体积小时编码为ziplist127.0.0.1:6379 zadd zsetkey 50 e1 60 e2 30 e3 (integer) 3 127.0.0.1:6379 object encoding zsetkey ziplist3.2 突破元素数量阈值当元素个数超过 128 个时内部编码会被强制切换为skiplist127.0.0.1:6379 zadd zsetkey 50 e1 60 e2 30 e3 ... 省略 ... 82 e129 (integer) 129 127.0.0.1:6379 object encoding zsetkey skiplist3.3 突破元素大小阈值即使元素个数很少只要插入了一个超过 64 字节的超大字符串元素编码同样会被切换为skiplist127.0.0.1:6379 zadd zsetkey 50 one string bigger than 64 bytes ... 省略 ... (integer) 1 127.0.0.1:6379 object encoding zsetkey skiplist3.4 不可逆的转换机制注意Zset 的底层编码转换是单向且不可逆的。一旦因为元素数量过多或体积过大触发了skiplist编码即使后续通过ZREM删除元素使得元素个数重新小于 128底层编码也不会再退回到ziplist。4. 深入跳表SkipList为什么是跳表既然数据量大了之后必须使用skiplist我们需要深入了解这个数据结构以及 Redis 为什么选择它而不是传统的树形结构。跳表的本质复杂的链表简单来说跳表是一个“复杂链表”。它在普通有序链表的基础上增加了多级索引层。通过维护这些索引查找元素时可以像“跳跃”一样跳过部分节点从而将查询元素的时间复杂度降低到了O(logN)。正是由于跳表在O(logN)查找与天然的链表范围遍历之间取得了完美的平衡它成为了 Redis 中有序集合最核心的底层基石。第三章Zset 实战应用场景与架构思考“有人的地方就有江湖有江湖的地方就得排个座次。” 在互联网应用中只要涉及到多维度的比较与筛选排行榜系统就是不可或缺的模块。无论是微博的热搜榜单、游戏里的天梯排行还是电商的销量打榜其核心诉求都是高效地处理动态变化的数据并随时提供按规则排序的视图。Redis 的有序集合Zset正是为此而生的利器。本章将从最基础的点赞热榜出发深入探讨复杂维度的权重计算并剖析海量数据下的内存容量估算问题。3.1 基础排行榜以“每日点赞热榜”为例假设我们需要维护一个网站每天的文章点赞热榜用户可以对文章进行点赞或取消点赞系统需要实时展示点赞数最多的前 10 篇文章。借助 Zset这个功能的实现非常简单且高效。在这个场景中Zset 的member存储文章的 ID或用户名score存储当前的点赞数。3.1.1 赞数的初始化与动态更新当用户 james 发布了一篇文章并获得 3 个赞时我们可以使用ZADD命令将其添加到当天的热榜集合中# 初始化文章分数为 3 ZADD user:ranking:2022-03-15 3 james如果后续这篇文章再次获得点赞我们不应该重新计算总分再ZADD而是使用ZINCRBY命令进行原子性的增量更新# 给 james 的文章增加 1 个赞 ZINCRBY user:ranking:2022-03-15 1 jamesZINCRBY的优势在于它是原子操作即使在高并发场景下多个用户同时点赞分数也能准确累加且底层会自动调整该元素在跳表中的位置时间复杂度仅为O(logN)。3.1.2 榜单的清理与展示由于各种原因例如用户注销、作弊被惩罚或者单纯是取消了点赞我们需要将某篇文章从榜单中移除。此时使用ZREM命令# 删除成员 tom 的文章 ZREM user:ranking:2022-03-15 tom当需要展示获取赞数最多的前 10 篇文章时Zset 天然就是按分数升序排列的我们要取最高分只需使用降序查询命令。在旧版本中可以使用ZREVRANGE在新版本中推荐使用带有REV参数的ZRANGE# 获取点赞数最高的前 10 名 (索引 0 到 9) ZREVRANGE user:ranking:2022-03-15 0 9 WITHSCORES # 或 Redis 6.2.0 新语法 ZRANGE user:ranking:2022-03-15 0 9 REV WITHSCORES3.1.3 联合 Hash 获取完整业务数据在真实的业务中Zset 中保存的往往只是用户的 ID 或文章的 ID而排行榜上还需要展示文章标题、作者头像、发布时间等详细信息。此时Zset 只负责排序详细数据应存储在 Hash 结构中。例如我们将用户 tom 的详细信息保存在 Hash 中HGETALL user:info:tom当需要展示某个特定用户的分数和排名时可以使用ZSCORE和ZRANK# 获取 mike 的分数 ZSCORE user:ranking:2022-03-15 mike # 获取 mike 的升序排名注意如果是热榜通常需要降序排名 ZREVRANK ZRANK user:ranking:2022-03-15 mike通过这种Zset (排序) Hash (详情)的组合我们能够以极高的性能支撑起一个完整的排行榜业务。3.2 内存容量估算海量数据真的放得下吗面对排行榜系统很多人会有疑问如果是游戏天梯排行或者微博热搜用户/数据量动辄千万甚至上亿全放在 Redis 的内存里服务器能扛得住吗我们不妨做个简单的估算。假设有一款游戏有1 亿玩家userid假设使用 4 个字节来表示一个玩家 ID。score假设使用 8 个字节来表示分数双精度浮点数。每个玩家的基础数据占用约12 字节。那么 1 亿个玩家的基础数据总量为1,000,000,000 * 12 字节 ≈ 12 亿字节 ≈ 1.2 GB。当然底层的跳表和哈希表还需要额外的指针开销、内存分配器的碎片等实际占用肯定不止 1.2 GB可能是几 GB 甚至十 GB。但在现代服务器动辄上百 GB 内存的今天把全量玩家数据放在 Redis 中是完全可行的。很多时候我们对“数据量”缺乏直观的认识觉得百万、千万就已经很大了实际上计算机的内存远比我们想象的要强大得多。3.3 复杂维度排行榜基于权重的热度计算像游戏天梯排名胜负分数是确定的。但在微博热搜、B站热门等场景中排序的规则非常复杂。一篇文章的热度可能由浏览量、点赞量、转发量、评论量等多个维度共同决定。假设我们认为热度 浏览量 × 0.2 点赞量 × 0.3 转发量 × 0.5。在数据写入时我们可以为每个维度分别维护一个 Zsetzset:viewmember 是微博 IDscore 是浏览量。zset:likemember 是微博 IDscore 是点赞量。zset:forwardmember 是微博 IDscore 是转发量。到了需要计算综合榜单时直接利用 Zset 的集合间运算命令ZUNIONSTORE或ZINTERSTORE。通过指定WEIGHTS参数Redis 会自动在内存中完成成百上千万次乘法和加法运算并生成最终的热度排行榜# 计算综合热度榜单并按权重加权求和 ZUNIONSTORE zset:hot 3 zset:view zset:like zset:forward WEIGHTS 0.2 0.3 0.5生成zset:hot后直接使用ZREVRANGE就能立刻拉取热度最高的内容。具体各维度的权重怎么分配通常由公司专门的数据或算法团队通过机器学习等方式计算得出而 Redis Zset 负责提供极具性能优势的工程落地支撑。3.4 拓展思考Zset 是唯一的选择吗虽然 Redis 的 Zset 是实现排行榜的绝佳选择但它也并非银弹。Zset 本质上是一种在内存中维护的有序集合。如果说数据量极其庞大例如百亿级别的真实数据且对实时性要求没那么高或者需要做非常复杂的聚合查询那么将数据交给 ElasticsearchES或关系型数据库如 MySQL可能更为合适。但是只要是对实时性要求高、数据量在内存可控范围内、且具备明确的数值排序维度的场景Redis Zset 凭借其极简的命令、高效的O(logN)复杂度和极低的内存开销绝对是架构设计时的首选数据结构。
阅读完成 · 觉得有帮助?