Redis 有序集合命令详解

有序集合(sorted set,简称 zset)是 Redis 的"明星类型"——它兼具 set 的"不重复"和 list 的"有序",而且自动按分数排序。zset 是做排行榜、延迟队列、分页的最佳数据结构,几乎每个用 Redis 的项目都会用到它。

1. 增删改:ZADD / ZREM / ZSCORE

# sorted set (zset):成员不重复,每个带 score,自动按 score 排序
ZADD leaderboard 1500 "玩家A" 2300 "玩家B" 1800 "玩家C"
# 返回新增成员数:3

# 重复添加同名成员会更新 score(不会重复)
ZADD leaderboard 2000 "玩家A"      # A 的分数从 1500 改成 2000

# ZADD 选项(Redis 3.0.2+)
# NX  : 只新增,不更新已有成员
# XX  : 只更新,不新增
# GT  : 仅当新 score 大于当前时才更新(Greater Than)
# LT  : 仅当新 score 小于当前时才更新(Less Than)
# CH  : 返回值改为"受影响的成员数"(包括更新),默认只返回新增
ZADD leaderboard XX 9999 "玩家A"   # 仅在已存在时更新
ZADD leaderboard NX 100 "新玩家"   # 仅在不存在时新增

# 查看成员数
ZCARD leaderboard                  # 3

# 查看某成员的分数
ZSCORE leaderboard "玩家B"          # "2300"

zset 的成员是不重复的,但每个成员带一个 score(分数,浮点数)。重复添加同名成员会更新 score(而不是新增)。Redis 6.2 引入了 NXXXGTLT 选项,让 ZADD 行为更精细——比如做"只刷新高分"的排行榜时用 GT,新分数小于已有分数的写入会被忽略。

zset 的底层是"跳表(skiplist)+ 字典"的双重结构:字典用于 O(1) 查单成员的 score,跳表用于 O(log N) 的范围查询和排名。这种组合让 zset 既支持单点查又支持范围查,复杂度都很优秀。

2. 范围查询:ZRANGE 家族

zset 的查询分两类:按排名按分数。两组命令各有升序降序版本:

# 按排名(从 0 开始)取
ZRANGE leaderboard 0 -1                # 升序全部(默认不带 score)
ZRANGE leaderboard 0 -1 WITHSCORES    # 升序全部,带 score
ZRANGE leaderboard 0 2 WITHSCORES     # 升序前 3 名(分数最低的 3 个)

ZREVRANGE leaderboard 0 2 WITHSCORES  # 降序前 3 名(分数最高的 3 个,排行榜常用)

# 按分数范围取
ZRANGEBYSCORE leaderboard 1500 2000              # 分数 1500~2000 之间
ZRANGEBYSCORE leaderboard "(1500" "+inf"         # 分数 > 1500 到正无穷 ( 表示开区间
ZREVRANGEBYSCORE leaderboard 2000 1500           # 高分到低分范围(注意顺序)

# 查某成员的排名(从 0 开始)
ZRANK leaderboard "玩家B"      # 升序排名(分数最低是 0)
ZREVRANK leaderboard "玩家B"   # 降序排名(分数最高是 0,排行榜常用)

# Redis 6.2+ 统一用 ZRANGE 加 BYSCORE/REV 参数:
ZRANGE leaderboard 1500 2000 BYSCORE       # 等价 ZRANGEBYSCORE
ZRANGE leaderboard 2000 1500 BYSCORE REV   # 等价 ZREVRANGEBYSCORE

几个易混点要记牢:ZRANGE 默认是按排名升序;排行榜场景(分数高在前)用 ZREVRANGE;查"前 10 名"就是 ZREVRANGE key 0 9。Redis 6.2 之后官方推荐统一用 ZRANGEBYSCORE / REV 参数,旧命令仍可用但建议新代码用统一语法。

开闭区间:ZRANGEBYSCORE key (1500 2000 中的 ( 表示开区间(分数大于 1500,不含 1500);没有 ( 是闭区间(含两端)。无限大用 +inf / -inf

3. 自增分数:ZINCRBY

# 增减某成员的分数(原子)
ZINCRBY leaderboard 200 "玩家A"      # A 加 200 分,返回新分数
ZINCRBY leaderboard -100 "玩家B"     # B 减 100 分

# 删除成员
ZREM leaderboard "玩家C"

# 按排名范围删除
ZREMRANGEBYRANK leaderboard 0 9      # 删掉排名 0~9(最低的 10 个)
ZREMRANGEBYSCORE leaderboard 0 1000  # 删掉分数 0~1000 的成员

# 计算分数范围内的成员数
ZCOUNT leaderboard 1500 2000         # 分数在 1500~2000 的成员数

ZINCRBY原子的——玩家每得一分调用一次,Redis 保证所有并发自增都不会丢失。这是排行榜实时更新的核心:每次玩家得分,一句 ZINCRBY leaderboard 100 "玩家A",排名立即重排,完全无需应用层维护。

4. 多个 zset 的运算

和 set 一样,zset 也支持交集、并集,但多了"如何聚合分数"的选项:

# 集合运算:并集、交集(结果存到新 zset)
ZADD math 90 "Alice" 80 "Bob" 70 "Carol"
ZADD english 85 "Alice" 95 "Bob" 70 "Dave"

# ZINTERSTORE:交集
# 参数:目标 key, 几个集合参与, 集合1, 集合2, 聚合方式
ZINTERSTORE total 2 math english AGGREGATE SUM
# total 里:Alice=175, Bob=175, Carol 没有(不在 english 里)

# 用 SUM / MIN / MAX 决定怎么合并分数
ZINTERSTORE total_min 2 math english AGGREGATE MIN

# 给集合每个成员指定权重(乘到 score 上)
ZINTERSTORE weighted 2 math english WEIGHTS 1.0 1.0 AGGREGATE SUM

# ZUNIONSTORE:并集(成员不必都在所有集合里)
ZUNIONSTORE all 2 math english AGGREGATE SUM
# 四个人都会出现在 all 里

典型用法:多科成绩汇总(数学 + 英语分数相加得总分)、多维评分综合排名(销量、好评、复购按权重加权)、合并多个时段排行榜(周榜 = 7 个日榜并集求和)。注意大 zset 做并集运算会慢,谨慎用在数据量大的场景。

5. 实战一:游戏排行榜

这是 zset 最经典的应用,直接看完整流程:

# 经典实例:游戏实时排行榜
# 1. 玩家得分时实时更新
ZADD leaderboard 0 "player1" 0 "player2" 0 "player3"
ZINCRBY leaderboard 1500 "player1"
ZINCRBY leaderboard 2300 "player2"
ZINCRBY leaderboard 1800 "player3"

# 2. 取前 10 名(分数从高到低)
ZREVRANGE leaderboard 0 9 WITHSCORES

# 3. 查某玩家的全服排名
ZREVRANK leaderboard "player2"      # 返回 0 表示第一名
# 显示给用户时要 +1(用户看到的是 1-based)

# 4. 查玩家附近的对手(用于"看看我和我前后名次的人")
ZREVRANK leaderboard "player1"      # 假设返回 5(即第 6 名)
ZREVRANGE leaderboard 3 7 WITHSCORES  # 取第 4~8 名

# 5. 分页(每页 10 条,第 2 页)
ZREVRANGE leaderboard 10 19 WITHSCORES

这套方案用 MySQL 做会很痛苦:玩家每得一分要 UPDATE 一行,查前 10 名要 ORDER BY score DESC LIMIT 10 全表排序——百万玩家时数据库根本扛不住。而 zset 的范围查询是 O(log N + M)(N 是总人数,M 是返回数),无论总人数多少,取前 10 都是毫秒级。这是"把热点数据从慢数据库搬到 Redis"的标志性例子

6. 实战二:延迟队列

zset 的另一个高阶用法是延迟队列——把"应该执行的时间戳"作为 score,后台轮询取出 score 小于当前时间的任务。

# 延迟队列(score 存"应该执行的时间戳")
import time
ts_now = int(time.time())
ts_run = ts_now + 60          # 60 秒后执行
# 用 redis-py:
client.zadd("delay_queue", {"task_001": ts_run})

# 后台轮询消费者:
while True:
    ts_now = int(time.time())
    # 取出 score <= 当前时间的任务
    tasks = client.zrangebyscore("delay_queue", 0, ts_now, start=0, num=1)
    if not tasks:
        time.sleep(0.5)
        continue
    task = tasks[0]
    # 用 Lua 脚本原子地"删除并判断",防止多消费者抢任务
    removed = client.zrem("delay_queue", task)
    if removed:
        process(task)        # 真正处理任务

# 适用场景:30 分钟未支付自动取消订单、定时邮件、
# 短信验证码 5 分钟过期等"X 秒后做某事"的需求

"30 分钟未支付自动取消订单""短信验证码 5 分钟后过期""订阅消息定时推送"——这类"X 秒后做某事"的需求,用 zset 实现比单独跑定时任务更精确、更省资源。注意取出后必须用 ZREM 删除并判断返回值,防止多个消费者抢同一个任务。这是 Lua 脚本"取+删原子化"的标准应用

7. 大 zset 的安全遍历

# 大 zset 的安全遍历(类似 HSCAN)
ZSCAN leaderboard 0 COUNT 10
# 1) "0"                          # 下次游标
# 2) 1) "player1"
#    2) "1500"
#    3) "player2"
#    4) "2300"
#    ...

# 弹出分数最高的(取出并删除,Redis 5.0+)
ZPOPMAX leaderboard             # 弹分数最高
ZPOPMIN leaderboard             # 弹分数最低
BZPOPMAX leaderboard 30         # 阻塞版

# 取随机成员(Redis 6.2+)
ZRANDMEMBER leaderboard 3

和 hash、set、list 一样,zset 字段非常多时不要用 ZRANGE 0 -1 一次拉出所有成员——会阻塞 Redis。用 ZSCAN 增量遍历,或用 ZRANGE key 0 99 分批取。ZPOPMAX / BZPOPMAX 是"取出并删除分数最高成员"的原子操作,适合"取最高优先级任务"的场景。

小结

zset 是 Redis 最有特色的类型,核心命令:ZADD/ZREM(增删)、ZSCORE/ZRANK/ZREVRANK(单点查)、ZRANGE/ZREVRANGE(按排名范围)、ZRANGEBYSCORE(按分数范围)、ZINCRBY(自增分数)、ZINTERSTORE/ZUNIONSTORE(集合运算)。掌握 zset,你就能搞定排行榜、延迟队列、分页、加权评分等高价值场景。

← 上一篇 Redis 列表与集合命令

下一篇 Redis 持久化:RDB 与 AOF

✈️💬