Redis 排行榜:ZSET 进阶(更新、分页、删除与复合排序)

有序集合(ZSET)是 Redis 做排行榜的秘密武器。它以 O(log N) 的复杂度插入元素,O(log N + M) 的复杂度进行范围查询。本指南从基础排名到高级模式全面覆盖。

问题场景

用关系型数据库做排行榜意味着:

  • 每次请求都要 ORDER BY score DESC——大规模下很慢。
  • 计算排名需要 COUNT 行数——每次查询 O(N)
  • 实时更新需要重新排序。

Redis ZSET 用内置排序数据和 O(log N) 的排名查找解决了这三个问题。

基础排行榜

添加分数

ZADD leaderboard 1500 "alice"
ZADD leaderboard 2300 "bob"
ZADD leaderboard 1800 "charlie"
ZADD leaderboard 2100 "diana"
ZADD leaderboard 1950 "eve"

Top N 玩家

ZREVRANGE leaderboard 0 2 WITHSCORES

返回分数最高的前 3 名玩家。

获取玩家排名

ZREVRANK leaderboard "charlie"
ZSCORE leaderboard "charlie"

ZREVRANK 返回从 0 开始的排名(0 = 第一名)。一次获取 Charlie 的排名和分数。

分数更新

增加分数

当玩家获得积分时,使用 ZINCRBY

ZINCRBY leaderboard 500 "charlie"
ZSCORE leaderboard "charlie"
ZREVRANK leaderboard "charlie"

Charlie 的分数从 1800 变为 2300,排名自动更新。

替换分数

设置绝对分数(如重新计算后):

ZADD leaderboard 3000 "alice"
ZSCORE leaderboard "alice"
ZREVRANK leaderboard "alice"

如果成员已存在,ZADD 会更新分数。

分页

对于有数千名玩家的排行榜,你需要分页。

基于页码的分页

ZREVRANGE leaderboard 0 2 WITHSCORES

ZREVRANGE leaderboard 3 5 WITHSCORES

ZREVRANGE leaderboard 6 8 WITHSCORES

每页 3 条。第 1 页 = 索引 0–2,第 2 页 = 索引 3–5,以此类推。

获取总数

ZCARD leaderboard

用它计算总页数:ceil(ZCARD / page_size)

基于分数的分页(游标)

对于超大排行榜,基于分数的分页避免了偏移量开销:

ZREVRANGEBYSCORE leaderboard +inf -inf LIMIT 0 3 WITHSCORES

然后用最后一个分数作为下一页的游标。

删除玩家

删除单个玩家

ZREM leaderboard "eve"
ZREVRANGE leaderboard 0 -1 WITHSCORES

按排名范围删除

删除排名最低的 2 名玩家:

ZREMRANGEBYRANK leaderboard 0 1
ZREVRANGE leaderboard 0 -1 WITHSCORES

注意:ZREMRANGEBYRANK 使用升序(最低分 = rank 0)。

按分数范围删除

删除所有分数低于 1000 的玩家:

ZREMRANGEBYSCORE leaderboard 0 999

高级模式

时间衰减排行榜

用于"热门"排行榜,近期活动权重更高:

ZADD trending 1739600100 "post:1"
ZADD trending 1739600200 "post:2"
ZADD trending 1739600050 "post:3"
ZREVRANGE trending 0 -1 WITHSCORES

用时间戳作为分数。更新的帖子排名更高。定期清理旧条目:

ZREMRANGEBYSCORE trending 0 1739500000

多条件排名(复合分数)

Redis ZSET 分数是 64 位浮点数。你可以编码多个条件:

score = 主分数 * 1000000 + (MAX_TIMESTAMP - 时间戳)

这样先按主分数排序,相同分数时按时间排序(更早 = 更高)。

ZADD composite 2300999900 "alice"
ZADD composite 2300999800 "bob"
ZADD composite 1800999950 "charlie"
ZREVRANGE composite 0 -1 WITHSCORES

Alice 和 Bob 主分数相同(2300),但 Alice 排名更高,因为她更早达到。

周/月排行榜

用带时间周期的 Key 命名:

ZADD leaderboard:2026:w07 500 "alice"
ZADD leaderboard:2026:w07 300 "bob"
ZINCRBY leaderboard:2026:w07 200 "alice"

EXPIRE leaderboard:2026:w07 604800

设置 TTL 自动清理旧排行榜。

合并聚合排行榜

将周排行榜合并为月视图:

ZADD week1 100 "alice" 200 "bob"
ZADD week2 150 "alice" 100 "bob"
ZUNIONSTORE monthly 2 week1 week2 AGGREGATE SUM
ZREVRANGE monthly 0 -1 WITHSCORES

常见坑点

  1. 分数精度:ZSET 分数是 IEEE 754 双精度浮点数。2^53 以内的整数是精确的。超过这个范围会丢失精度。
  2. 大 ZSET:百万级成员的 ZSET 运行正常,但对它执行 ZRANGE 0 -1 会阻塞 Redis。务必分页。
  3. 并列处理:没有复合分数时,相同分数的玩家排序是未定义的。Redis 对相同分数按字典序排序,这可能不是你想要的。
  4. 原子更新:如果需要同时更新分数和元数据,使用事务或 Lua 脚本。分开的 ZADD + HSET 可能导致不一致状态。

在线编辑器试一试

前往 Redis 在线编辑器 构建排行榜:

ZADD leaderboard 1500 "alice" 2300 "bob" 1800 "charlie" 2100 "diana" 1950 "eve"
ZREVRANGE leaderboard 0 -1 WITHSCORES
ZREVRANK leaderboard "charlie"
ZINCRBY leaderboard 500 "charlie"
ZREVRANGE leaderboard 0 2 WITHSCORES
ZREM leaderboard "eve"
ZCARD leaderboard

观察 Charlie 在分数增加后是怎么爬升排名的。这就是零重排序开销的实时排名。