ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Redis Sorted Sets 详解:排行榜与延迟队列的底层利器

Redis Sorted Sets 详解:排行榜与延迟队列的底层利器 做后端的人大概率都遇到过这种需求给用户按某个字段排序展示、做一个实时排行榜、实现一个延迟队列。刚入行的时候我第一反应都是查出来以后 order by 一下不就行了数据量小的时候确实没问题一旦到了每秒几万次更新、还要实时排名、还要支持按分数区间查询这种量级数据库的 order by 就成了性能黑洞。这时候 Redis 的 Sorted Sets有序集合就该登场了。Sorted Sets 是 Redis 五种基础数据类型里最“聪明”的一个它把 Set 的去重能力和一个可排序的 score分数揉在一起所有成员天然按 score 从小到大排列底层用跳跃表加哈希表两个结构共同支撑。你问它“分数在 80 到 90 之间的前 10 名是谁”它毫秒级就能算出来不需要全表扫描不需要临时表。这篇文章就围绕 Sorted Sets 本身把它所有常用命令一个一个拆开讲讲清楚每个命令的语义、边界情况、时间复杂度和真实业务里你会踩的坑。适合刚学完 Redis 基础数据类型想深入理解的人也适合要在项目里落地排行榜、延迟队列的开发者面试前想系统过一遍 Sorted Sets 的人同样可以照着这篇顺一遍。1. 先弄明白 Sorted Sets 到底解决了什么问题1.1 一个排行榜需求逼出来的数据结构在没有 Sorted Sets 之前如果你想在 Redis 里维护一个“谁得分最高”的列表你能用什么用 List插入顺序固定想按分数排序得自己取出来再排数据多了根本排不动。用 Hashuser - score 存起来方便但你想拿“分数大于 100 的所有人”时就傻了只能 HSCAN 全量扫一遍再逐条判断。用 Set去重很爽但 Set 本身无序要排序还是得拉到客户端做。这三个方案共同的痛点排序操作在 Redis 之外完成数据要全量传输排序要消耗应用服务器 CPU数据量一大全完蛋。Sorted Sets 的核心思路是把“排序”这件事直接压进 Redis 内部——每个成员带一个 double 类型的分数Redis 负责在写入时就把数据维护成有序状态查询的时候直接按顺序吐出来。这个设计看似简单但它把“按分数排序”“按分数区间取数据”“算排名”这几个高频需求全部变成了 O(logN) 级别的操作这才是它能扛住高并发实时排行的根本原因。1.2 score member有序集合的“二元世界观”Sorted Sets 里每个元素由两部分组成member成员和 score分数。member 是唯一的同一个 key 里不允许出现两个相同的 member这一点和 Set 完全一致。score 是 double 类型即 IEEE 754 的 64 位浮点数可以重复多个 member 对应同一个 score 是完全合法的。所有 member 按照 score 从小到大排列如果 score 相同再按 member 的字典序lexicographical order排列。这里有个新手最容易忽略的点score 相同的情况下“谁排在前面”由 member 字符串本身的字节顺序决定而不是插入顺序。也就是说你先后插入了 b 和 a但它俩分数一样的话永远是 a 排在 b 前面。这个规则在实现并列排名的功能时特别关键后面实战部分我会再展开。SDS 字符串作为 memberscore 用 double 存储这就决定了 Sorted Sets 的适用边界它擅长存“实体 id 数值指标”这种二元结构比如 userId 积分、订单号 过期时间戳、商品 id 销量。如果你需要存的不是一个数值而是一段复杂对象要么序列化后当 member要么换别的数据结构别硬塞。2. 核心命令拆解写入、查询、删除2.1 ZADD一个命令搞定插入和更新ZADD 是 Sorted Sets 最基础的命令语法如下ZADD key [NX | XX] [GT | LT] [CH] [INCR] score member [score member ...]最常用的基本形式就是ZADD key score member127.0.0.1:6379 ZADD leaderboard 100 user:1 (integer) 1 127.0.0.1:6379 ZADD leaderboard 95 user:2 87 user:3 (integer) 2返回的整数值表示“本次新增了多少个 member”。如果 member 已经存在ZADD 就变成了更新 score返回 0127.0.0.1:6379 ZADD leaderboard 120 user:1 (integer) 0 # 因为 user:1 已存在只是分数从 100 改成了 120这时候不少人会踩一个坑到底更新没更新答案是更新了返回值只是单纯代表新增数量想确认有没有发生更新需要加 CH 选项。CH 是 “changed” 的意思加了这个参数后返回的是“新增数量 被修改分数的 member 数量”127.0.0.1:6379 ZADD leaderboard CH 130 user:1 (integer) 1 # CH 模式下才能看出 user:1 被改了还有几个选项值得单独说NX只在 member 不存在时插入等价于“新增语义”已存在的 member 不会更新 score。XX只在 member 已存在时更新 score等价于“更新语义”不存在的 member 不会被插入。GT/LT只有新分数比当前分数更大 / 更小的时候才更新Redis 6.2 之后才有做“分数只升不降”这种逻辑特别方便。INCR把 ZADD 变成原子自增等价于 ZINCRBY后面专门讲。建议凡是写“排行榜”这类高频更新场景优先想清楚你要的是“插入”还是“更新”还是“有条件更新”然后选对应的选项组合避免每回先 ZSCORE 判断再 ZADD 的两步操作那不仅多一次网络往返还引入了原子性问题。2.2 ZRANGE 与 ZREVRANGE正着拿、反着拿查询是 Sorted Sets 使用频率最高的操作。ZRANGE 按分数从小到大的顺序返回指定排名区间注意是排名区间不是分数区间的元素127.0.0.1:6379 ZRANGE leaderboard 0 -1 1) user:3 2) user:2 3) user:10 -1表示从第 0 个一直取到最后一个。ZRANGE leaderboard 0 1就是取前两名。ZREVRANGE 则是反过来的顺序从分数高到低127.0.0.1:6379 ZREVRANGE leaderboard 0 -1 WITHSCORES 1) user:1 2) 130 3) user:2 4) 95加了WITHSCORES会把 score 一起返回注意返回格式是 member-score 交替出现的扁平列表解析的时候要按两个一组处理。Redis 6.2 开始 ZRANGE 强化了很多可以直接按分数区间查、按字典序查甚至支持 REV 反向所以如果你用的是 6.2 以上版本ZREVRANGE 这种老命令可以慢慢被新语法替代127.0.0.1:6379 ZRANGE leaderboard 0 1 REV WITHSCORES 1) user:1 2) 130 3) user:2 4) 95新版本的 ZRANGE 相当于把 ZREVRANGE、ZRANGEBYSCORE、ZRANGEBYLEX 的功能全收敛到一个命令里了参数变多但语义更统一。老项目里见到 ZREVRANGE 也不要慌功能是等价的只是维护成本高一些。2.3 ZSCORE、ZCARD查分数和查数量这两个属于“看一眼”类命令简单但天天用127.0.0.1:6379 ZSCORE leaderboard user:1 130 127.0.0.1:6379 ZCARD leaderboard (integer) 3ZSCORE 返回 member 的分数member 不存在时返回 nil。ZCARD 返回这个有序集合的元素总个数等价于 Set 的 SCARD。还有一个 Redis 6.2 才加入的 ZMSCORE可以一次查多个 member 的分数127.0.0.1:6379 ZMSCORE leaderboard user:1 user:2 user:100 1) 130 2) 95 3) (nil)一次 RTT 拿到多个分数批量场景里能省不少网络开销。做后台管理页的时候我一般会用 ZMSCORE 把一批 userId 的分数批量捞出来然后拼成一个表格不用循环调用 ZSCORE。3. 按分数区间和字典序查询的进阶玩法3.1 ZRANGEBYSCORE闭区间、开区间与无穷大排行榜业务里最核心的查询是“给我分数在某个区间内的人”。ZRANGEBYSCORE 就是干这个的127.0.0.1:6379 ZRANGEBYSCORE leaderboard 90 130 1) user:2 2) user:1这个命令的区间符号有几个细节特别容易错( 表示开区间不含边界值。比如ZRANGEBYSCORE leaderboard (90 130 会排除分数正好是 90 的成员。-inf和inf分别表示负无穷和正无穷你要查“分数大于等于 90 的所有人”就是ZRANGEBYSCORE leaderboard 90 inf。分数不带括号时是闭区间含边界。我见过不止一个同事写ZRANGEBYSCORE key 90 130结果发现 90 分和 130 分的人都被包含进来这才想起闭区间默认是包含的。如果业务上不想含边界一定记得加(127.0.0.1:6379 ZRANGEBYSCORE leaderboard (90 (130还有一点老版本里 ZRANGEBYSCORE 不支持 LIMIT 之外的倒序限制要拿区间内分数最高的人得配合 ZREVRANGEBYSCORE127.0.0.1:6379 ZREVRANGEBYSCORE leaderboard 130 90 LIMIT 0 10注意 ZREVRANGEBYSCORE 的参数顺序是“从大到小”所以写的是130 90不是90 130这个顺序写反了会直接拿不到数据。Redis 6.2 之后用ZRANGE key min max BYSCORE REV LIMIT 0 10也能达到同样效果语义更清晰。3.2 ZRANGEBYLEX按字典序干活当所有成员的 score 都相同的时候Sorted Sets 实际上退化成按 member 字典序排列的集合这时候 ZRANGEBYLEX 就有用了。它按 member 的字典序范围查询语法里用-表示负无穷表示正无穷127.0.0.1:6379 ZADD autocomplete 0 apple 0 app 0 apricot 0 banana 127.0.0.1:6379 ZRANGEBYLEX autocomplete [app (b 1) app 2) apple 3) apricot[表示闭区间(表示开区间。这个命令最经典的应用是做输入框自动补全把候选词全部塞进同一个 keyscore 统一为 0然后根据用户输入前缀去 ZRANGEBYLEX 取范围。比如用户输入了 “app”你就查[app到[app\xff用\xff表示前缀的结束边界就能把所有以 app 开头的词都捞出来127.0.0.1:6379 ZRANGEBYLEX autocomplete [app [app\xff 1) app 2) apple这套方案比关系库的 LIKE 快得多比 Redis 自身的 KEYS 也安全得多不会阻塞。但注意它要求所有分数完全一致如果分数不同排序就按 score 走了字典序查询会得到意想不到的结果。3.3 ZCOUNT区间计数不取数据只取数有些场景你不需要数据本身只需要“这个区间有多少人”。ZCOUNT 就是干这个的127.0.0.1:6379 ZCOUNT leaderboard 90 inf (integer) 2这个命令返回分数在指定闭区间内的成员数量计算过程和 ZRANGEBYSCORE 一样走跳跃表索引复杂度是 O(logN)不是全量扫描。做各种“高于某个阈值的用户数量”统计时特别好用比如实时监控在线用户中“活跃度超过 80 的用户有多少”一条命令搞定。配套的还有 ZLEXCOUNT统计字典序区间内有多少成员用法和 ZRANGEBYLEX 一样也是要求 score 都相同。4. 删改、自增与排名计算4.1 删除操作ZREM 与批量删除删除单个或多个 member 用 ZREM127.0.0.1:6379 ZREM leaderboard user:2 (integer) 1 127.0.0.1:6379 ZREM leaderboard user:1 user:3 (integer) 2返回删除成功的数量。如果要按排名区间批量删用 ZREMRANGEBYRANK127.0.0.1:6379 ZREMRANGEBYRANK leaderboard 0 9这段代码会把排名第 0 到第 9 的成员全部删掉也就是“删除分数最低的前 10 个”。类似的还有 ZREMRANGEBYSCORE按分数区间删和 ZREMRANGEBYLEX按字典序区间删。做排行榜定期清理尾部数据时ZREMRANGEBYRANK 是最常用的只保留 Top N其他全清掉一行命令完成不需要遍历删除。4.2 ZINCRBY排行榜更新的核心排行榜最典型的操作不是“直接改分数”而是“每次行为发生时给分数加一定数值”。比如用户完成一个任务积分 10使用 ZADD 就得先 ZSCORE 读出旧值再加完写回去两步操作之间有并发窗口数据会丢。ZINCRBY 把“读-改-写”合并成一步原子操作127.0.0.1:6379 ZINCRBY leaderboard 10 user:1 140返回的是更新后的新分数。这个命令的时间复杂度是 O(logN)Redis 内部会直接定位到 member 然后更新分数并调整跳跃表位置不需要额外加锁。所有“积分累加”“播放量增加”“投票数加一”这类需求都应该用 ZINCRBY不要自己先读再写。4.3 ZRANK 与 ZREVRANK排名怎么算有了分数自然要问“某个人排第几”。ZRANK 返回 member 按分数从小到大排的排名从 0 开始ZREVRANK 返回从大到小排的排名127.0.0.1:6379 ZRANK leaderboard user:2 (integer) 1 127.0.0.1:6379 ZREVRANK leaderboard user:2 (integer) 2排名从 0 开始这个点很多人会忘前端展示的时候记得 1。如果 member 不存在返回 nil。ZREVRANK 0 就是第一名这在排行榜场景里是天然契合的ZREVRANK的返回值直接就是“第几名减一”。这里要提醒一个“并列排名”的坑。很多业务的排行榜要求“相同分数并列排名”但 ZRANK 给的是物理排名它按“分数 字典序”排完再算位置。如果三个用户分数都是 100ZRANK 会给出 0、1、2 三个不同的排名而不是并列第 1。要实现并列排名常规做法是分数相同的人按某种规则二次比较或者干脆用 ZSCORE 拿到分数后在应用层做分段处理。这个没有现成命令能一步到位必须自己设计设计时别把 ZRANK 当成并列排名直接用。5. 集合运算与弹出操作5.1 ZUNIONSTORE 与 ZINTERSTORE多集合合并ZUNIONSTORE 可以把多个 Sorted Sets 按分数合并成一个新的 Sorted Sets每个 key 还可以设置权重127.0.0.1:6379 ZADD week1 100 user:1 50 user:2 127.0.0.1:6379 ZADD week2 80 user:1 90 user:2 127.0.0.1:6379 ZUNIONSTORE total 2 week1 week2 (integer) 2 127.0.0.1:6379 ZRANGE total 0 -1 WITHSCORES 1) user:2 2) 140 3) user:1 4) 180ZUNIONSTORE dest 2 week1 week2里的2表示后面有两个 key。默认是简单的分数相加也可以用 WEIGHTS 给不同 key 的分数乘系数用 AGGREGATE 指定 SUM、MIN、MAX 三种聚合方式127.0.0.1:6379 ZUNIONSTORE total 2 week1 week2 WEIGHTS 1 2 AGGREGATE SUM这段代码表示 week1 的分数乘以 1week2 的分数乘以 2再加在一起。ZINTERSTORE 则是取交集只有同时出现在多个集合里的 member 才会进入结果集分数按 AGGREGATE 指定的方式聚合。这两个命令特别适合做“多期排行榜汇总”或“多重条件筛选后的综合分”。要注意的是ZUNIONSTORE 的时间复杂度是 O(N M) 加结果的排序开销如果参与运算的集合很大这段操作会阻塞 Redis 主线程。Redis 6.2 之后有了不带存储的 ZUNION / ZINTER它们直接返回结果不写入新 key127.0.0.1:6379 ZUNION 2 week1 week2 WITHSCORES不带 STORE 的命令适合结果集要马上被客户端消费的场景省掉了写 key 再删 key 的额外操作。但同样地大集合上的运算还是要谨慎评估耗时。Redis 7.0 还补了 ZDIFFSTORE 和 ZDIFF可以做差集运算。5.2 ZPOPMAX / ZPOPMIN 与阻塞版本ZPOPMAX 弹出分数最高的几个元素ZPOPMIN 弹出分数最低的弹出即从集合中移除127.0.0.1:6379 ZPOPMAX leaderboard 2 1) user:1 2) 140 3) user:2 4) 95一个典型用法是“消费队列”把任务按优先级分数存入 Sorted Setsworker 用 ZPOPMAX 取最高优先级的任务处理处理完再从集合里消失。BZPOPMAX 和 BZPOPMIN 是阻塞版本如果集合里没有元素就阻塞等待直到超时或有新元素127.0.0.1:6379 BZPOPMAX leaderboard 30这个命令的使用方式和 BLPOP 完全一致30是阻塞超时秒数0 表示永远阻塞。用 Sorted Sets 做延迟队列时BZPOPMIN 是从队列头部取“到期时间最早”的任务天然匹配。小心不要让多个消费者同时阻塞在同一个 key 上Redis 是单线程的阻塞的连接会占用连接资源连接池要留出余量。6. 底层原理为什么是跳跃表6.1 哈希表 跳跃表的双引擎结构Sorted Sets 的性能优势不是凭空来的它内部同时维护了两个结构。哈希表负责按 member 精确查找ZSCORE、ZADD判断 member 是否存在走的就是哈希表O(1) 复杂度。跳跃表负责按顺序维护元素ZRANGE、ZRANK、ZRANGEBYSCORE这类跟顺序有关的操作走的是跳跃表平均 O(logN) 复杂度。这两个结构指向同一批元素通过指针连接所以内存上有重叠但不重复存储 member 和 score。当集合里的元素数量很少时默认小于 128 个且每个元素字节数小于 64Redis 会使用压缩列表ziplist编码来省内存超过阈值才转换成真正的跳跃表 哈希表结构。这个细节在 Redis 7.0 之后被 listpack 取代了但思路一样小数据用紧凑编码大数据用高效结构。6.2 为什么选跳表而不是红黑树能维护有序序列的数据结构不少红黑树、B 树、跳表各有拥趸Redis 选择跳跃表有现实考量。实现简单跳表的插入、删除、查找逻辑就是“从高层往下走每层找合适位置”代码量比红黑树小一个量级而且不容易写错。范围查询友好跳表本质上是有序链表加上多级索引按范围遍历时只要找到起点然后顺着链表往右走就行。红黑树想遍历一个范围要先中序遍历代码复杂得多。并发调节容易跳表的层数是随机生成的不需要像红黑树那样在插入后做复杂的旋转和变色调整。Redis 是单线程模型虽然不太需要关注并发但随机化带来的实现简洁性依然有优势。B 树更适合磁盘持久化的大规模索引但 Redis 的数据整体在内存里内存随机访问本身就很快跳表的额外指针开销换来的是极简的实现和维护成本这是非常务实的取舍。6.3 时间复杂度速查表很多面试和日常排障都涉及复杂度判断我把 Sorted Sets 常用命令的时间复杂度整理成一张表命令时间复杂度说明ZADDO(logN)N 为集合元素数量ZSCOREO(1)走哈希表ZCARDO(1)内部维护了长度计数ZRANGE / ZREVRANGEO(logN M)M 为返回的元素数量ZRANGEBYSCOREO(logN M)ZRANK / ZREVRANKO(logN)ZINCRBYO(logN)ZREMO(logN)删除单个 memberZREMRANGEBYSCOREO(logN M)ZUNIONSTOREO(N M) 排序开销N、M 为参与集合的大小ZPOPMAX / ZPOPMINO(logN)这张表最大的启示单元素操作基本都是 O(logN)这是 Sorted Sets 能支撑千万级排行榜的根本原因但批量操作尤其 ZUNIONSTORE的耗时可能和集合规模线性相关用之前必须评估数据量。7. 实战案例排行榜与延迟队列7.1 实时排行榜完整方案排行榜是最经典的 Sorted Sets 应用。假设我们的业务是“用户签到积分榜”需求是每次签到积分 10实时展示 Top 100查看任意用户当前排名写逻辑用 ZINCRBY127.0.0.1:6379 ZINCRBY daily_score:20240601 10 user:123用日期作为 key 后缀天然支持“每日榜”“周榜”周榜可以用 ZUNIONSTORE 把 7 天的 key 合并。查 Top 100 用 ZREVRANGE127.0.0.1:6379 ZREVRANGE daily_score:20240601 0 99 WITHSCORES查用户排名用 ZREVRANK 再 1127.0.0.1:6379 ZREVRANK daily_score:20240601 user:123 (integer) 15整个方案没有任何应用层排序全部由 Redis 内部完成。设计时要注意 key 的过期策略每日榜如果只保留近 30 天就给 key 设置 30 天 TTLRedis 会自动清理过期 key不用写定时任务。7.2 延迟队列用时间戳当 score延迟队列的需求很常见订单超时未支付要关闭、消息发送失败要重试、定时任务要按点触发。用 Sorted Sets 做延迟队列的思路很巧妙——score 存“任务应该被执行的时间戳”member 存任务内容或任务 ID。生产者ZADD delay_queue current_timestamp delay_seconds task_id消费者用ZRANGEBYSCORE delay_queue 0 current_timestamp LIMIT 0 100取出所有已到期的任务逐条处理处理成功后用 ZREM 移除。如果没到期可以BZPOPMIN循环等待或者轮询时算一下最近一个任务的到期时间选择合适 sleep 时长。伪代码思路while True: now int(time.time() * 1000) tasks redis.zrangebyscore(delay_queue, 0, now, start0, num100) for task in tasks: if process(task): redis.zrem(delay_queue, task) time.sleep(1)这个队列方案最大的优势是实现简单、支持按时间精准排序而且天然支持多消费者并发抢任务因为 ZREM 是原子的每个任务只会被一个消费者删除。劣势是没有原生的 ACK 机制任务处理到一半消费者挂了任务就丢了。所以这种方案更适合“允许少量丢失”或“配合数据库记录状态可对账”的场景。如果你是第一次实现延迟队列从这套方案入手绝对比直接上 RocketMQ 要轻得多。8. 踩坑实录与排查技巧8.1 分数用浮点数的精度坑score 是 double 类型这不是 Redis 的缺陷是所有浮点数的通病。典型场景你的业务分数精确到小数点后两位比如 99.99存进去再读出来可能变成不能精确表示的二进制浮点数。更危险的是多个浮点数累加时误差会逐步放大。规避方案很简单分数用“分为单位”的整数存储99.99 元就存 9999展示时再除以 100。如果业务分数本身就是整数直接用整数别套一层浮点运算。我实际见过线上系统因为分数是99.99 0.01这种计算导致排行榜出现 100.0000000001 这种诡异数字页面直接把榜单样式干崩了。处理方案是把分数全部换算成整数分值入库从此再无精度问题。8.2 相同 score 时不要依赖插入顺序前面提过score 相同时按 member 字典序排。这意味着如果你拿“时间顺序”作为先后依据比如同时刻多人得分得分相同的人越晚插入反而可能排得越靠前或靠后完全取决于 member 的字符串内容。这个坑在实现“按时间排序的榜单”时特别容易踩。规避办法需要“分数相同时按时间先后排”的业务把时间因素编码进分数里。常见手法是使用“大整数 时间戳”的组合分数。比如主分数是积分次分数是时间逆序值可以把分数存成积分 * 10^12 (MAX_TIMESTAMP - timestamp)这种组合数既保证主分数优先又保证同分时时间早的排前面。这个数列可能超出 double 的精确表示范围所以实际工程里一般用整数拼接或者拆成多个 Sorted Sets 做二次排序需要你结合数据量权衡复杂度。8.3 大 key 与阻塞操作Sorted Sets 的单 key 可以容纳几百万元素性能依然不差但有几个操作要警惕ZRANGE key 0 -1不带 LIMIT 的全量查询一次返回几百万条网络传输直接打满客户端内存也可能扛不住一定要分页。ZREMRANGEBYRANK key 0 -1全量清空虽然很快但结果是一次性删除巨量元素会造成主线程耗时和内存回收压力。大 key 删除建议用 UNLINK 替换的异步释放机制或者分批删除。ZUNIONSTORE合并超大集合时要估算耗时会阻塞主线程阻塞期间所有其他命令都会排队。排查方法很简单。用DEBUG OBJECT key看编码方式和元素个数用MEMORY USAGE key看内存占用用SLOWLOG GET看历史慢命令。如果某个 key 的元素规模超过百万最好在监控里针对这个 key 单独加告警别等它拖垮整个 Redis 实例。8.4 内存优化压缩编码与命名字典Sorted Sets 占用的内存比想象中大因为跳跃表每个节点都要维护多层指针。优化手段有几个小集合会自动用紧凑编码ziplist / listpack存几百个元素时内存优势巨大尽量不要让有序集合无意义地塞入大量小元素。member 尽量用短字符串比如用户 ID 用数字字符串而不是长 UUID。member 每多一个字节排序和哈希都要多占一份内存。多个业务共用一个 Sorted Sets 还是拆成多个 key要根据读写模式权衡。同一个 key 能承载的写入并发远大于多个 key但单个超大 key 又会有阻塞风险。一般来说按业务维度拆分 key并让单个 key 的元素控制在几十万以内是工程上比较稳的平衡点。9. 我的个人经验总结做了几年 Redis 相关的东西Sorted Sets 是我在项目里用得最频繁的“重型武器”功能密度远超其他四种基本类型。根据我个人的体会有几个经验值得单独分享给你。第一个是“能用整数就别用浮点”。所有跟金钱、分数、排名相关的字段在设计阶段就定成整数存储这是成本最低但收益最大的决定能帮你省掉后面一整类精度 bug。第二个是“优先用 6.2 之后的命令风格”。ZRANGE 统一了反向、按分数、按字典序的查询语法新代码里写 ZRANGE 比混杂 ZREVRANGE、ZRANGEBYSCORE、ZRANGEBYLEX 三套老命令可读性强得多排查问题时心智负担也小很多。第三个是“排行榜这类需求一定要想清楚并列规则再动手”。ZRANK 只是物理排名业务上的“并列排名”需要自己在分数设计上做文章。在需求评审阶段就确认好这个规则能避免上线后又返工。最后一个技巧是“延迟队列别一上来就引入消息中间件”。如果你的延迟任务量在百万级以内、对消息可靠性要求不是极端苛刻一个 Sorted Sets 加一个轮询进程就够了开发和运维成本低好几个量级。等业务量真上去了再去迁移 RocketMQ 或专门的消息队列那时候你对“到底需要消息队列的哪些能力”才有清晰的认知。照着这篇把命令敲一遍、把场景想一遍再回你正在做的项目里去审视哪些地方能用到 Sorted Sets这个数据类型的价值你才算真正吃透了。
返回列表