
说到Redis前面几篇把String、List、Hash、Set都过了一遍这一篇终于轮到最让我“上头”的Sorted Sets有序集合。为什么这么说因为在我带过的项目里凡是涉及排名、榜单、实时分数这类需求的Redis其他类型要么做起来别扭要么性能顶不住只有Sorted Sets是真正贴着需求长出来的。这篇就把Sorted Sets类型和它的命令体系一次讲透既聊清楚每个命令怎么用也把背后的设计逻辑和我在生产环境里踩过的坑一并倒出来希望能帮正在学Redis或准备用Sorted Sets做业务的你省点时间。1. Sorted Sets到底是个什么结构先理解它为什么能“排序”很多初学者第一次看到Sorted Sets容易把它理解成“Set加了个分数字段”这个方向是对的但只猜对了一半。Sorted Sets确实Set家族的一员保留了成员唯一的特性也就是同一集合里不会出现重复成员但它真正厉害的地方在于每个成员都要绑定一个double类型的分数Redis会按照这个分数对成员做升序排列。换句话说这是一个天然有序、且顺序可以动态调整的集合结构。这种设计背后依赖的是两种底层数据结构跳跃表skip list和哈希表dict。简单说哈希表负责快速定位“某个成员的分数是多少”跳跃表负责维护“按分数排列的有序链表”两种结构通过指针共享同一批成员对象所以Sorted Sets在读写时才能在时间复杂度上做到比较均衡。跳跃表这个名词听起来吓人你可以把它理解成“加了索引的多层链表”查询时可以从最高层快速跳过一段节点再用少量回退找到目标。它比平衡二叉树实现简单但查询和写入的复杂度都是O(logN)级别足够应对绝大多数场景。那Sorted Sets适合解决什么问题最典型的就是排行榜。比如一个积分榜单用户积分一变排名就要跟着变用数据库做这种高频更新会很痛苦而ZADD一条命令就能在更新分数的同时完成排序读取时再按排名范围捞人都是微秒到毫秒级别的操作。除此之外Sorted Sets还常被用作延迟队列把任务执行时间戳作为分数用ZRANGEBYSCORE捞出到期的任务来处理还可以做滑动窗口限流用时间戳做分数、用时间窗口做分数范围统计窗口内的请求总数。这些场景都是“数据要排序排序还要频繁更新”的典型代表Sorted Sets几乎是为它们量身定制的。理解Sorted Sets时有个关键点要特别注意排序规则不是按插入顺序也不是按成员名称而是严格按score值升序。分数相同的情况下再按成员的字典序排列。这个“同分再排序”的细节很多新手会忽略实际做业务时才意识到同分不同名的成员在榜单上谁先谁后是有明确规则的不是你想象中的“按先来后到”。我后面讲ZADD和排名相关命令时还会再提到这个坑。2. 基础命令全解ZADD、ZRANGE、ZREM、ZCARD必须吃透2.1 ZADD不只是往集合里塞数据ZADD是Sorted Sets最核心的写入命令它的基本语法是ZADD key [NX|XX] [CH] [INCR] score member [score member ...]。最常见的用法很简单ZADD ranking 100 user1表示把user1写进ranking集合分数是100;如果要批量写可以一次塞入多组分数和成员ZADD ranking 100 user1 90 user2 80 user3一次命令写三个用户减少多次网络往返这个在批量初始化榜单时非常实用。ZADD有几个容易被忽略的参数。NX表示只有成员不存在时才执行写入相当于“仅新增”该参数下不会覆盖已有成员的分数XX则相反只更新已存在的成员不新增成员。CH是changed的缩写加上这个参数后命令返回值不再单纯返回“新增了多少个成员”而是返回“新增成员数被更新分数的成员数”方便你判断当前操作到底改变了多少数据。INCR参数让ZADD变身为“按增量修改分数”的命令效果等同ZINCRBY这一点我下一节细讲。实际使用中我最常用的组合是ZADD key CH score member因为默认情况下ZADD的返回值只表示新增成员数你向集合里连续写入同一批成员时返回值会是0这会让你误判“数据没写进去”。加上CH之后返回值更直观能明确反映本次操作是否引发了实际变化。这个细节在写自动化脚本做数据同步时特别有用靠返回值就能判断是不是需要继续后续逻辑。2.2 ZRANGE和ZREVRANGE按排名捞数据ZRANGE按分数从低到高的顺序截取指定排名区间的成员语法是ZRANGE key start stop [WITHSCORES]。start和stop是排名索引从0开始也可以用负数表示从尾部倒数例如ZRANGE ranking 0 -1 WITHSCORES就是取出整个榜单所有成员和分数。这里的索引规则和List类型保持一致0是第一名分数最低的成员-1是最后一名0 -1就是全量数据这在做榜单全量导出时是我最常写的命令。ZREVRANGE则是反着来按分数从高到低排序后取区间做“Top N榜单”时通常用这条命令。比如ZREVRANGE ranking 0 9 WITHSCORES拿到的是分数最高的前10名直接就是“积分总榜Top10”的结果。很多人会搞混ZRANGE和ZREVRANGE其实只要记住REV是reverse前缀表示倒序不带REV是正序正序榜从0开始是分数最低的成员倒序榜从0开始是分数最高的成员。这组命令务必要记住“区间是按排名下标而不是按分数”这一点。如果你要按分数区间捞数据得用下一部分要讲的ZRANGEBYSCORE很多初学者在这里踩了坑拿ZRANGE传了两个分数进去结果发现返回的成员完全不是预期的那批其实就是没搞清楚ZRANGE的索引语义。2.3 ZREM、ZCARD、ZCOUNT删除与统计三件套ZREM用于移除集合中的指定成员语法是ZREM key member [member ...]可以批量删除。返回值是实际被移除的成员个数如果成员原本就不存在会被忽略不计。这个命令在清理过期用户、移除已处理任务时是高频操作。相比Set的SREMZREM的语义完全相同只是在内部除了要删除哈希表中的成员还要从跳跃表中摘除对应的节点所以整体耗时比SREM略高但在实际使用中差异几乎可以忽略毕竟都是O(logN)级别。ZCARD用来获取集合的基数也就是成员总数语法很简单ZCARD key。ZCOUNT则是统计分数在某个闭区间范围内的成员数量语法是ZCOUNT key min max注意min和max都是包含端点的。比如ZCOUNT ranking 80 100统计分数在80到100之间的成员个数这个在做“及格率”“高分段人数”之类的统计时非常方便一条命令就返回数字不需要把全量数据捞回客户端再做过滤既省流量又省计算。实际生产里这三条命令经常被组合使用先ZCARD看看总量再ZCOUNT看看某个分数段的数量算占比淘汰一批成员后再用ZCARD确认是否清干净了。整个过程全是O(logN)或O(1)操作即便集合里有几十万成员也扛得住。3. 分数与排名操作ZINCRBY、ZSCORE、ZRANK的精妙之处3.1 ZINCRBY积分增减的正确姿势ZINCRBY是Sorted Sets里我个人使用频率最高的命令之一语法是ZINCRBY key increment member作用是把指定成员的分数增加increment。如果成员不存在命令会先创建该成员再按increment作为初始分数写入如果increment是负数就相当于减分。这背后体现了一个很关键的设计思想Sorted Sets的分数更新永远都应该通过“相对增量”而不是“先读后写”来操作。为什么这么说以积分榜为例传统做法是先ZSCORE读出当前分数在客户端加上本次变动值再ZADD写回去。这个流程最大的隐患是并发问题两个请求同时读到同一个旧分数分别加上自己的增量后先后写回后写的那个会覆盖前一个的更新积分直接丢失。ZINCRBY把“读分数增加值写回”这三个步骤合并成Redis服务端的一个原子操作天然避免了竞态条件性能和正确性都更有保障。凡是涉及计分、扣减、投票数变更这类高频更新都应该优先选择ZINCRBY而不是ZADD。使用ZINCRBY时返回值是更新后的最新分数如果你需要在业务里使用新分数直接拿这个返回值就行不用再额外查一次ZSCORE。还有一个细节increment支持浮点数比如ZINCRBY ranking 1.5 user1Redis内部对double的精度处理遵循标准的IEEE 754规则极小的小数累加在某些极端情况下会有浮点精度问题我在后面的避坑章节会专门展开。3.2 ZSCORE与ZRANK查分数、查排名ZSCORE用于查询指定成员的分数语法是ZSCORE key member时间复杂度O(logN)。如果成员不存在返回空结果。这是在Redis客户端里快速确认某个用户“现在多少分”最直接的手段。需要注意的是ZSCORE返回的是字符串形式的数字比如100或98.5你在客户端拿到之后需要转换成数值类型再参与计算直接拿字符串做加法会出问题。ZRANK返回指定成员在正序排列中的排名索引排名从0开始也就是分数最低的成员排名为0。ZREVRANK则返回倒序排名分数最高的成员排名为0。这两条命令配合起来可以快速判断一个成员的“段位”。比如某个用户在正序排名是999倒序排名是0说明他是整个榜单的分数天花板。这种排名查询有一个O(logN)的定位过程实际表现非常快。3.3 同分成员的排名规则踩坑ZRANK和ZREVRANK的实现有一个必须强调的规则当多个成员分数相同时排名按照成员名称的字典序排列。这意味着如果你有两个成员分数都是100成员名是a和b那么a会比b排得更靠前。这看起来没什么但结合ZINCRBY使用时会出现一个让人摸不着头脑的现象某些成员分数明明和别人一样但ZRANK排名却有先后而且不是你插入顺序决定的。我在一个抽奖项目里就遇到过这种情况用户积分相同但每次榜单刷新时排名顺序不一样后来排查发现是成员名称的字典序在作祟。如果业务上对相同分数的成员有额外的排序需求比如按“最近一次更新时间”来分先后纯靠Sorted Sets的score字段是做不了的。常用的解法是把时间因素编码进分数里比如分数 业务分数 1 - 时间戳归一化值或者牺牲一部分精度把时间戳缩放到一个小数位上让同分场景自然按时间倒序。这个技巧我后面会用一个完整例子演示。4. 按分数范围操作ZRANGEBYSCORE、ZREMRANGEBYSCORE、ZRANGEBYLEX4.1 ZRANGEBYSCORE按分数泳道捞数据ZRANGEBYSCORE的语法是ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]它按分数区间获取成员min和max默认是包含端点的闭区间。关键技巧在于如何表达开区间在分数前加(符号表示不包含该值比如(10表示大于10(100表示小于等于100时排除掉100。如果要查“分数大于等于10且小于100”的区间应该写成ZRANGEBYSCORE ranking 10 (100。LIMIT参数是这里的高频用法它支持分页式地截取区间内的数据。但我要提醒一点LIMIT在Sorted Sets里的作用和MySQL的LIMIT不一样它是“从符合条件的成员中跳过offset个再取count个”不是按排名全局偏移。当你需要“全量分数在某段区间的Top N”时配合倒序命令更合适。比如查“分数不低于200的前20名”应该用ZREVRANGEBYSCORE ranking inf 200 LIMIT 0 20注意这条命令的min和max顺序是反的因为它是按分数从高到低遍历。很多初学者在这里很容易把参数顺序写错导致返回的数据完全不对。4.2 ZREMRANGEBYSCORE按范围批量删除ZREMRANGEBYSCORE是一条批量删除命令语法是ZREMRANGEBYSCORE key min max删除分数在区间内的所有成员返回值是被删除的数量。它和ZREM的区别在于ZREM是按成员名删除ZREMRANGEBYSCORE是按分数段删除。这在做延迟队列时特别有用任务处理完之后按时间戳范围批量清理已到期的任务一条命令就能清掉一整个窗口的数据。还有一个关联命令是ZREMRANGEBYRANK按排名区间删除成员比如ZREMRANGEBYRANK key 0 -1清空整个集合。这几个按范围操作的命令看名字都带RANGE很容易混淆我建议用一张表来记忆BYSCORE是按分数删、BYRANK是按排名删、BYLEX是按字典序删。后面这个BYLEX我们马上讲到。4.3 ZRANGEBYLEX同分场景下的字典序操作ZRANGEBYLEX是Sorted Sets里比较容易被忽略的命令它按成员名称的字典序返回区间语法是ZRANGEBYLEX key min max [LIMIT offset count]。这个命令有个使用前提集合里所有成员的分数必须相同否则结果不可预测。因为此时跳跃表排序的唯一依据就是成员名字分数如果不一致排序结果就混入了分数因素返回的数据自然不是纯字典序。那么这条命令有什么实际用途一个典型场景是做“全局唯一ID分配器”或“按名称前缀搜索”。比如你把一批商品ID写入一个Sorted Sets集合分数统一设为0那么ZRANGEBYLEX可以直接按字典序扫描ID列表用[前缀匹配指定的起始字符串例如ZRANGEBYLEX ids [sku_100 [sku_200可以取出sku_100到sku_200之间的所有ID这在做基于有序ID的分页扫描时效率极高。不过我实际用过之后的感受是能用ZRANGEBYLEX解决的业务场景相对有限如果你暂时用不上先知道有这么个东西等遇到“同分集合要按字典序区间遍历”的需求时再回来翻这篇就够了。5. 集合间运算ZUNIONSTORE、ZINTERSTORE如何玩出花样5.1 ZUNIONSTORE多榜单合并计算ZUNIONSTORE是Sorted Sets最重要的复合计算命令之一它的语法是ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight ...] [AGGREGATE SUM|MIN|MAX]。numkeys参数表示参与运算的集合数量这个参数必须显式给出Redis需要据此解析后续参数的边界。WEIGHTS为每个集合指定加权系数例如两个榜单按3:1的比例加权合并第一个集合权重3第二个权重1合并时每个成员的分数先乘以对应权重再进入聚合。AGGREGATE参数决定多个集合里同一个成员重合时怎么计算最终分数默认是SUM求和也可以选MIN取最小值或MAX取最大值。举个例子公司有“月度销售榜”和“月度服务分榜”现在要生成一个综合绩效榜销售榜权重0.7服务榜权重0.3综合分就是两个分数加权求和。一条ZUNIONSTORE命令就能算出结果并写入新key不用在客户端做任何合并计算。我需要提醒的是目标集合如果已经存在它的旧数据会被直接覆盖所以命名上要特别注意别把原榜单数据冲掉了。5.2 ZINTERSTORE交集的业务想象空间ZINTERSTORE的语法和ZUNIONSTORE完全一致区别在于它计算的是多个集合的交集只有同时出现在所有集合里的成员才会被保留并对它们的分数执行AGGREGATE聚合。它的经典应用是做“多重标签筛选”商品A集合里存了“电子产品”标签下的所有商品ID商品B集合里存了“打折商品”标签下的ID商品C集合存了“库存充足”标签下的ID用ZINTERSTORE计算三者交集就能快速得到“当前打折且有货的电子产品”列表。这个过程如果换成数据库SQL去JOIN在高并发查询下压力很大而Sorted Sets的多个集合可以先在离线任务里算好交集把结果写入预热的key线上查询直接读这个key性能非常理想。在实际项目中这种“预计算再落地”的思路比在SQL里临时JOIN要稳定得多也是我把Sorted Sets归为“可以撑起业务复杂计算”的类型的根本原因。5.3 权重与聚合的实际调参经验使用WEIGHTS时有一个容易忽略的语义权重参数的数量必须和numkeys声明的集合数量一致否则Redis会返回语法错误。权重可以为小数比如0.5也可以为0这相当于忽略某个集合的分数影响只把它当作筛选条件的“存在性标记”。比如ZINTERSTORE的多个集合里某个集合只负责标记黑白名单权重设为0即可它不影响最终分值的计算但能保证“名单里的成员才参与聚合”。这个用法很巧妙能少写大量业务代码。AGGREGATE的选型需要结合业务语义。做积分合并、票数汇总时用SUM做“多条件是取最高分还是最低分”这种场景时比如取多个榜单中的最高排名用MAX或MIN。说白了聚合方式就是对同一个成员在多集合中的分数做归约选错语义结果就差一截。我在实践里会把所有相关集合的score语义写进接口注释比如“score表示抽奖权重合并时SUM”避免三个月后自己都忘了当初为什么这么设。6. 从命令到实战排行榜、延迟队列、限流器的具体玩法6.1 排行榜ZINCRBY更新 ZREVRANGE读取完整链路排行榜是最能体现Sorted Sets价值的业务场景实现思路不复杂用户产生积分变动时用ZINCRBY更新分数查询榜单时用ZREVRANGE取出分数从高到低排名靠前的用户。举个例子一个直播间送礼物排行收到礼物的主播分数增加代码大概是这样# 某主播收到价值100的礼物 ZINCRBY live_ranking 100 anchor_1024 # 查询当前榜单Top10 ZREVRANGE live_ranking 0 9 WITHSCORES这里有个生产环境要注意的点榜单查询虽然是O(logN)O(M)的复杂度但如果你在活动期间每秒请求量很大每次都直接打Redis的ZREVRANGE仍然会带来不小的压力。更稳妥的做法是“读缓存写实时”ZINCRBY负责真实计算另起一个定时任务每隔几十秒把TopN结果写入一个String类型的缓存key线上接口优先读这个key。这样用户看到的榜单最多延迟几十秒但对Redis的压力从“每请求一次”降低到“每秒几十次”能扛住更大的并发量。排行榜还有一个刚需是查“我的排名”。这个也不复杂ZREVRANK key member返回排名加1之后就是用户视角的“第几名”。需要注意如果用户不在榜单里ZREVRANK返回空业务层要兜底处理比如提示“未上榜”而不是返回0名。我自己就见过线上代码没做空值判断用户不在榜上被写成了第一名非常尴尬。6.2 延迟队列score存时间戳用ZRANGEBYSCORE捞到期任务延迟队列是Sorted Sets除排行榜之外最典型的应用。设计思路很简单任务的计划执行时间戳作为score任务唯一标识作为member。提交延迟任务时执行ZADD delay_queue 未来时间戳 task_123然后由一个后台轮询任务每隔一段时间用ZRANGEBYSCORE取当前时间戳之前的所有任务执行任务内容最后用ZREM或ZREMRANGEBYSCORE删除已处理的任务。# 提交一个5分钟后执行的任务 ZADD delay_queue $(date -d 5 minutes %s) task_123 # 轮询取出所有到期任务 ZRANGEBYSCORE delay_queue -inf $(date %s) # 处理完成后删除 ZREM delay_queue task_123这个方案比定时器逐个扫描要高效得多因为跳表结构天然支持按分数范围快速定位取到期任务只需要O(logNM)的代价。而且整个队列是持久化的哪怕业务重启任务也不会丢。相比MQ的延迟消息Redis延迟队列的缺点是每个任务需要自己保证消费成功的确认机制没有内置的消息重试和死信队列。我用的补偿策略是任务处理失败时把score更新为当前时间戳重试间隔比如ZADD delay_queue $((new_time)) task_123实现简单的退避重试。6.3 更进阶同分排序、滑动窗口限流与二级索引同分排序问题是我特别想再强调一遍的“隐藏需求”。比如一场答题活动用户按正确题数排名很多人分数一样这时产品要求“谁先达到这个分数谁排前面”。单纯的score不够用我采用的编码方案是score 正确题数 * 10000000000 (某个最大基准 - 完成时间戳)。这样同分的用户之间完成时间越早附加数越大综合score越大在倒序榜上就更靠前。注意基准值要足够大能容纳时间戳的差值并且要控制在小数精度范围内避免double精度丢位。滑动窗口限流也可以借助Sorted Sets实现。用当前时间戳做score、用户标识做member每次请求时先ZREMRANGEBYSCORE清理窗口过期记录再ZCARD统计窗口内请求数。如果统计值超过阈值就拒绝请求否则ZADD写入本次请求。整个过程全部在Redis服务端完成原子性有保障实现代码非常简短。不过这种限流方案在超高并发下会累积大量member需要定期清理所以我更推荐把它用在中小流量的接口防护上。Sorted Sets还能充当二级索引比如缓存某类商品按价格排序的ID列表分数就是商品价格。数据库里做“按价格区间分页查询”很吃索引但Sorted Sets通过ZRANGEBYSCORE LIMIT直接支持这个查询模式。把价格变动的商品ID用ZADD写进去查询时按价格区间捞ID再用ID去数据库或缓存取详情这在高并发商品列表页非常常见确实能大幅减轻数据库压力。7. 常见问题与避坑指南这些坑我是真金白银踩出来的7.1 底层结构认知跳表为什么快但又不是万能理解Sorted Sets的底层结构对你的日常排障会有直接帮助。跳跃表是多层有序链表结构节点在插入时按概率随机决定层数。查询时从头节点最高层开始遍历每层向右移动时跳过若干节点直到看到目标值所在的区间再降一层继续查找最终定位到目标节点。这个设计与B树有本质区别它不需要维护复杂的平衡操作插入和删除都只影响到邻近节点因此并发写入的表现很稳定。不过跳跃表不是万能的。Sorted Sets所有命令的时间复杂度都建立在“按分数或排名快速定位”这一核心能力上但如果你在业务里高频使用“按成员名查排名”ZRANK它虽然也是O(logN)但两三次连续的O(logN)操作叠加后在超大集合和超高并发下仍会积累明显延迟。我的建议是能缓存的热数据比如TopN榜单就不要让线上请求反复穿透Redis的计算层。原则上Sorted Sets适合做“写多读多但每次读写都足够快”的数据结构不适合做复杂条件查询和全量扫描。7.2 分数精度与double的坑Redis Sorted Sets的score是double类型但Redis在接收命令时会把数字解析成字符串内部存储时以双精度浮点数方式保存。整数分数在2^53以内都是精确的一旦超过这个范围或者大量使用小数并频繁累加就会出现精度丢失。比如你用ZINCRBY不断加0.1累加十次后拿ZSCORE看到的可能不是1.0而是0.9999999999999999。这在金额、百分比等对精度敏感的业务里是致命的。解决方式无非三种一是干脆用整数把小数放大100倍或1000倍存成整数展示端再做除法二是在客户端对ZSCORE读出的浮点数做四舍五入后再参与运算三是在写入时用ZADD key score member直接覆盖分数而不是连续多次ZINCRBY累加减少误差累积。我个人最推荐第一种整数化方案把一切都收到整数纬度上避开double的精度深渊。7.3 元素数量巨大时的编码类型问题Sorted Sets在元素数量少且元素体积小的时候内部会采用ziplist压缩列表编码以节省内存而当元素数量超过配置阈值后会转换为skiplist编码。这个转换过程是透明的但转换前后的性能和内存特性差异明显。ziplist编码下查询复杂度是O(N)但N很小无所谓一旦转换成skiplist查询变快但内存占用增加。你可以通过OBJECT ENCODING key查看当前key的编码类型排查问题时这是一个很有用的诊断命令。官方配置里有两个参数控制转换阈值zset-max-ziplist-entries默认128zset-max-ziplist-value默认64字节。如果你明确知道自己的集合会长得很大可以在Redis配置里适度调高或调低这些阈值。但我的建议是尽量别动它保持默认值就好因为ziplist压缩内存的效果是显著的而它的O(N)查询在128个元素下根本无感。真正的坑在于一批小集合各自占用独立key几百个key都是ziplist编码看起来每个都很小总量却很惊人。这种场景使用的是hash结构那是另一篇文章的话题这里点到即止。7.4 常用命令速查与易混辨析把这一篇讲过的命令聚一下方便你贴到笔记里随时查阅命令作用关键点ZADD添加或更新成员分数NX/XX/CH/INCR参数ZRANGE按下标正序取成员下标从0开始支持负数ZREVRANGE按下标倒序取成员Top榜单常用ZRANGEBYSCORE按分数区间取成员(表示开区间LIMIT分页ZREVRANGEBYSCORE按分数区间倒序取成员max在前min在后ZREM按成员删除支持批量ZREMRANGEBYSCORE按分数区间删除批量清理场景ZREMRANGEBYRANK按排名区间删除清空可用0 -1ZCARD取成员总数O(1)ZCOUNT统计分数区间内数量闭区间ZSCORE取指定成员分数不存在返回空ZINCRBY成员分数增减原子操作ZRANK/ZREVRANK查询正/倒序排名同分按字典序ZRANGEBYLEX按字典序区间取成员所有分数必须相同ZUNIONSTORE多集合并集WEIGHTS权重、AGGREGATE聚合ZINTERSTORE多集合交集同样支持权重和聚合易混点我再最后唠叨一遍ZRANGE是下标区间ZRANGEBYSCORE是分数区间ZRANGEBYLEX是字典序区间三者名字看着像家族语义各有各的适用场景ZREVRANGE和ZREVRANGEBYSCORE一个是按下标倒序、一个是按分数倒序ZRANK是正序排名、ZREVRANK是倒序排名。把这些成对记清楚命令这块就稳了。回到开头说的Sorted Sets在我用过的所有Redis数据类型里是设计和现实贴合得最紧的一个。它不是那种你忘了它也能把业务糊过去的类型而是当你真正遇到排序、排名、时间窗这类问题时会庆幸Redis给了你这么趁手的工具。我写这篇也是希望把这些年的使用心得沉淀下来供自己做备忘也供遇到同样问题的朋友参考。你在动手实践时不妨从一个最简单的积分榜开始跑通ZADD、ZINCRBY、ZREVRANGE这条链路再去尝试延迟队列和交集计算基本就能把Sorted Sets的战斗力掌握七七八八了。