zrangebyscore 是 redis zset 范围查询主力命令,基于跳跃表实现 o(log n + m) 高效查询;zrange 按排名而非分数,不可替代价格等区间筛选;支持闭区间(如 20 40)和开区间(如 (20 (40)。

zrangebyscore 是范围查询的主力命令
Redis ZSet 的范围查询不靠遍历,核心是 zrangebyscore。它直接利用底层跳跃表(skiplist)的有序结构,从 min 分数位置快速跳到起始节点,再顺序读取直到 max,时间复杂度接近 O(log N + M),其中 M 是结果数量。
常见错误是误用 zrange:它按排名索引(0 到 -1)取值,和分数无关。比如商品价格变了,排名就乱了,不能替代按价格区间筛选。
-
zrangebyscore products 20 40—— 包含端点,查分数 ≥20 且 ≤40 的所有元素 - 开区间要用括号:
zrangebyscore products (20 (40表示 >20 且 - 无穷大写法固定:
-inf和+inf,不能写成inf或null - 加
WITHSCORES才能同时拿到值和分值,否则只返回 member 字符串
分数设计必须匹配业务语义
范围查得快,前提是分数能准确表达你要“范围”的东西。比如查“最近一小时订单”,不能把时间戳当 score 直接塞进去——得用毫秒时间戳(如 int(time.time() * 1000)),否则精度不够、范围不准。
容易踩的坑:
- 用浮点数做 score(如
19.99)会导致精度丢失或比较异常,尽量转为整数(如价格 × 100) - 多个维度要复合查询?ZSet 不支持多字段索引。常见做法是拼接字符串再哈希,但会破坏数值可比性;更稳妥的是拆成多个 zset 或配合 RedisSearch
- 分数重复很常见(比如多人同分排行榜),ZSet 允许,但排序时同分元素的相对顺序不保证稳定
底层是跳跃表,不是 B+ 树或红黑树
很多人以为 ZSet 用的是传统数据库的索引结构,其实 Redis 选的是跳跃表(skiplist)+ 哈希表组合。跳跃表天生支持 O(log N) 查找起点、O(M) 向后遍历,特别适合范围扫描;哈希表则负责 O(1) 查 member 对应的 score,用于更新和单点查询。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
这意味着:
- 插入/更新
zadd有对数级开销,高频写入时要注意吞吐压力 - 小数据量(默认 ≤128 元素且每个 member ≤64 字节)会自动用 listpack 存储,省内存但范围查询变慢;一旦超限立刻切到跳跃表,行为突变,压测时得覆盖边界量级
- 没有“索引失效”概念,但 score 改动会触发节点重排,频繁改分可能引发短时延迟毛刺
分页和大数据量要加 LIMIT
直接 zrangebyscore key -inf +inf 在千万级 zset 上可能卡住连接,尤其客户端没设 timeout。生产环境必须配 LIMIT offset count。
注意两点:
-
LIMIT 0 10是第 1 页,LIMIT 10 10是第 2 页——offset 是从 0 开始计数的偏移量,不是页码 - 深度分页(比如
LIMIT 100000 10)仍需遍历前 10 万条,性能随 offset 线性下降;真要支持深分页,得用游标式方案(记录上一页最大 score,下一页查zrangebyscore key (last_score +inf LIMIT 0 10) - 如果需要总数,单独跑
zcount key min max,别依赖len(result),因为带LIMIT时它不等于总命中数
真正影响性能的从来不是命令本身,而是 score 是否可索引、数据规模是否触发底层结构切换、以及有没有为分页场景设计游标逻辑。这些细节不验证到线上流量,很容易在大促时突然暴露。










