广告位 · 页头(后台-模块管理-新建模块后替换此区域)

Redis Zrange命令怎么用?参数怎么选择?有哪些避坑建议?

ZRANGE 命令在 Redis 有序集合里承担范围读取的任务。不少项目里只记住了 ZRANGE key 0 -1 这种最简用法,遇到按分数筛选、倒序排列、分页拉取时仍然习惯去查旧命令,甚至搞混 REV 和 LIMIT 的作用范围。实际上从 Redis 6.2 开始,ZRANGE 已经把按索引、按分数、按字典序三种模式统一到一起,理解它的参数组合,可以少记好几个命令。下面先把语法拆开,再结合实际场景说清楚怎么选、怎么避坑。

Redis Zrange命令怎么用?参数怎么选择?有哪些避坑建议?

一、ZRANGE 语法与参数拆解

ZRANGE 的完整语法如下:ZRANGE key start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count] [WITHSCORES]。先看最简单的索引模式,start 和 stop 是闭区间索引,0 表示第一个成员,-1 表示最后一个成员。比如 ZRANGE rank 0 2 返回分数最低的前三个成员。如果 start 大于 stop,比如 ZRANGE rank 5 2,Redis 不会报错,只会返回空数组。负数索引用起来很方便,-3 表示倒数第三个元素,-1 表示最后一个元素,但如果负数超出了实际元素数量,Redis 会把它按边界处理,不会直接返回错误。

默认情况下,ZRANGE 按照分值从低到高排列。如果多个成员分数相同,Redis 内部会按照成员的字典序进行升序排列,这一点对之后的 BYLEX 模式也有关键影响。WITHSCORES 参数会让 Redis 在返回成员名称的同时返回对应分数,返回结果是成员和分数交替出现的数组,而不是一个成员对应一个分数的对象结构。很多客户端库会把这个交替数组封装成更方便的结构,但直接用 redis-cli 查看时,需要留意每一对值的先后顺序。

127.0.0.1:6379> ZADD rank 100 user1 200 user2 150 user3
(integer) 3
127.0.0.1:6379> ZRANGE rank 0 -1 WITHSCORES
1) "user1"
2) "100"
3) "user3"
4) "150"
5) "user2"
6) "200"

从 Redis 6.2 开始,ZRANGE 还可以使用 BYSCORE 和 BYLEX 来改变范围判断方式。BYSCORE 会把 start 和 stop 解释为分值范围,而不是索引范围;BYLEX 则把 start 和 stop 解释为字典序区间。REV 参数可以反转排列顺序,搭配 BYSCORE 使用时,相当于旧命令 ZREVRANGEBYSCORE 的能力。LIMIT offset count 只能和 BYSCORE 或 BYLEX 一起出现,用在索引模式中会报语法错误。这个限制经常被忽略,导致线上命令执行失败。

二、不同业务场景下的参数选择

如果业务只是需要排行榜前几名,直接用索引模式最快。比如 ZRANGE rank 0 9 WITHSCORES 取分数最低的前十个成员。这里要注意,如果排行榜希望分数高的排在前面,不能只靠 ZRANGE rank 0 9,因为默认是升序。可以在命令中加上 REV,写成 ZRANGE rank 0 9 REV WITHSCORES,它等价于旧命令 ZREVRANGE rank 0 9 WITHSCORES。这样就能从高分到低分取前十个成员,对游戏排行榜、销量榜单这类场景非常直观。

按分数区间查询时,选择 BYSCORE 会比先取全部数据再在业务代码里过滤更高效。比如要查询分数在 100 到 200 之间的成员,可以使用 ZRANGE rank 100 200 BYSCORE WITHSCORES。BYSCORE 模式下,start 和 stop 默认是包含边界值的。如果希望某一边不包含,可以在数值前加上左括号,例如 ZRANGE rank (100 200 BYSCORE WITHSCORES 表示不包含 100 分。这个写法在 redis-cli 里直接输入即可,不需要转义,但在一些客户端库中,参数需要作为字符串传入,避免被当成数字处理。

字典序模式 BYLEX 适合所有成员分数都一致的场景,例如用它来存储一个有序的字符串集合,并按照字典顺序做范围分页。语法上,start 和 stop 需要使用 [ 表示包含某个字符串,或 ( 表示排除某个字符串,- 和 + 分别代表负无穷大和正无穷大。例如 ZRANGE words [a [z BYLEX 可以查询以 a 到 z 范围开头的字符串。需要特别强调,BYLEX 只有在所有成员分数相同时才有明确语义,否则返回结果可能混乱,这也是这个模式不常用的原因之一。

127.0.0.1:6379> ZADD words 0 apple 0 banana 0 cherry 0 date
(integer) 4
127.0.0.1:6379> ZRANGE words [b [c BYLEX
1) "banana"
2) "cherry"

当需要分页拉取大数据量时,LIMIT 可以和 BYSCORE 或 BYLEX 配合使用。例如 ZRANGE rank -inf +inf BYSCORE LIMIT 20 10 WITHSCORES 表示从升序结果中跳过 20 条,取接下来的 10 条。offset 是从 0 开始计数,count 为负数时表示返回从 offset 开始的所有剩余成员。这个特性在做一个不依赖外部游标的分页接口时很好用,但当结果集发生增删时,偏移量可能重复或跳过数据,需要业务层权衡一致性。

三、ZRANGE 的注意事项与避坑建议

第一个容易踩的坑是索引越界不报错。ZRANGE 对 start 和 stop 的容错度很高,start 超过最后一个索引、stop 小于第一个索引,或者 start 大于 stop,都不会抛异常,而是直接返回空结果。这本身不是问题,问题在于当线上出现空结果时,很多排查路径会先怀疑 Redis 连接或数据写入,忽略了很可能是索引传错。建议在业务代码中对 start 和 stop 的来源做一次边界校验,至少在测试环境断言传入范围是否合理。

第二个坑是返回结果过大导致阻塞。Redis 是单线程执行命令,如果对一个包含几十万甚至上百万成员的有序集合执行 ZRANGE key 0 -1,Redis 会在一次命令中把所有成员和分数全部返回,既会长时间占用主线程,也会产生巨大的网络响应。对于需要全量拉取的场景,更推荐使用 ZSCAN 做游标遍历,或者把数据分批用 ZRANGE key cursor cursor+pageSize 拉取,但后者在数据变动时可能出现重复或遗漏,适合对一致性要求不高的后台任务。

第三个坑是分数相同导致顺序不符合业务预期。有序集合在分数相同时按成员字典序排列,这个顺序不是随机,也不是插入顺序。比如排行榜中出现并列分,Redis 可能因为成员名字的关系把某个玩家排在另一个前面,产品如果要求并列时按时间先后展示,就会认为 Redis 返回错了。解决办法通常是把时间因素编码进分数,比如使用 score = 主分值 + 时间戳倒数 的复合分数,或者把时间戳拼到成员名中,再用 BYLEX 辅助排序。

第四个坑是用 REV 之后误解 LIMIT 的偏移方向。REV 会先反转结果顺序,LIMIT 再基于反转后的序列进行偏移和截取。比如 ZRANGE rank -inf +inf BYSCORE REV LIMIT 0 10 返回的是按分数从高到低的前 10 个,而不是升序的前 10 个。如果业务想要倒序第二页,offset 也要按倒序后的位置来计算。这个和普通索引模式下先分页再加 REV 的结果可能完全不同,需要明确命令参数的执行顺序。

还有一个版本兼容问题需要留意。ZRANGE 的新参数形态在 Redis 6.2 才完整支持,如果客户端连接的是 6.0 或更早版本,ZRANGE key 0 -1 REV 会直接报语法错误。很多云服务提供的是新版本 Redis,但自建或嵌入式场景中仍可能遇到老版本。遇到这种情况,要么升级 Redis,要么继续使用 ZREVRANGE、ZRANGEBYSCORE 等旧命令,但不要在同一套代码里混用两套命令风格,维护成本会明显上升。

四、实际示例:排行榜接口从取出到分页

下面用一个简单的游戏排行榜示例把常用参数串起来。假设已有成员和分数如下,其中 player:2 和 player:5 分数相同,按字典序排列时 player:2 会出现在 player:5 前面。

127.0.0.1:6379> ZADD game:rank 1000 player:1 1500 player:2 1200 player:3 900 player:4 1500 player:5
(integer) 5
127.0.0.1:6379> ZRANGE game:rank 0 -1 WITHSCORES
1) "player:4"
2) "900"
3) "player:1"
4) "1000"
5) "player:3"
6) "1200"
7) "player:2"
8) "1500"
9) "player:5"
10) "1500"

取前三名,需要按分数从高到低,可以用 ZRANGE game:rank 0 2 REV WITHSCORES,结果会返回 player:5、player:2、player:3。由于 player:5 和 player:2 分数相同,倒序模式下字典序仍然保持升序,所以 player:2 会排在 player:5 后面。若产品要求同分情况下先上榜的玩家排前面,就要在前面提到的时间复合分数上做文章。

如果要分页拉取所有成员,可以按分数范围来做。比如每页 2 条,取第二页,可以执行 ZRANGE game:rank -inf +inf BYSCORE LIMIT 2 2 WITHSCORES,得到 player:3 和 player:2 这两条数据。这个方式比直接使用索引范围 ZRANGE game:rank 2 3 WITHSCORES 更稳定,因为索引分页在数据插入或删除后会导致同一元素出现在不同页。当然,分数分页也有自己的问题,如果分数大量重复,分页结果也会受到字典序排序变化的影响,但它通常比纯索引分页更接近业务的稳定排序语义。

最后再强调一次,ZRANGE 虽然看起来简单,但它的参数组合决定了查询语义。写代码时先确定三个问题:要按索引、按分数还是按字典序查;要不要倒序;需不需要分页。把这三个答案映射到 ZRANGE 的参数上,基本就能避开大多数使用错误。旧命令不是不能用,但新项目统一用 ZRANGE,代码可读性和维护成本都会更好。

广告位 · 文章底部(后台-模块管理替换此区域)