3. 排行榜基础实现

Redis 的有序集合(Sorted Set,简称 ZSet)是实现排行榜最自然的数据结构。它同时具备“集合去重”和“按分值排序”两个特性:每个成员(member)唯一,且绑定一个浮点分值(score),Redis 内部按分值升序维护成员顺序。排行榜需要的“添加成员、更新分数、取 Top N、查个人排名”四类操作,ZSet 都提供了对应的原生命令,且复杂度大多为 O(log N),非常适合高频读写场景。

一、数据结构设计

假设我们做一个游戏积分排行榜,业务上需要:玩家完成一局后上报积分,排行榜展示前 10 名,玩家也能查询自己的名次。我们用一个 ZSet 承载一个榜单,Key 命名建议带上业务与周期维度,例如:

rank:game:score:2024w28   // 第28周游戏积分榜
rank:game:score:all       // 总榜

成员 member 使用玩家唯一标识,如 uid:10086;score 使用积分值。这里有一个关键约定:ZSet 默认按 score 升序排列,而排行榜通常要“分数越高越靠前”。因此有两种处理方式:一是存真实积分,查询时用 ZREVRANGE 反向取;二是存“负积分”,查询时用 ZRANGE 正序取。推荐第一种,语义更直观,也便于后续做分数区间统计。

二、成员添加与分数更新

添加成员和更新分数在 ZSet 里是同一个命令:ZADD。如果成员不存在则新增,存在则更新其 score。例如玩家 10086 完成一局得 3200 分:

ZADD rank:game:score:2024w28 3200 uid:10086
ZADD rank:game:score:2024w28 2850 uid:10087
ZADD rank:game:score:2024w28 4100 uid:10088

如果积分是“累加型”(每局得分累加到总分),应使用 ZINCRBY,它返回更新后的分值,天然避免“先读后写”的并发覆盖问题:

ZINCRBY rank:game:score:2024w28 150 uid:10086
// 返回 "3350",表示累加后总分

需要注意:ZADD 是覆盖式更新,ZINCRBY 是增量式更新。业务上要明确“本局得分”是替换还是累加,否则会出现分数被覆盖或重复累加的 bug。若同一玩家短时间内多次上报,建议在业务层用“局次 ID”做幂等,避免重复加分。

三、Top N 查询

取前 N 名用 ZREVRANGE(按分值从高到低)。要同时拿到成员和分值,加 WITHSCORES:

ZREVRANGE rank:game:score:2024w28 0 9 WITHSCORES

返回结果成对出现,形如:

1) "uid:10088"
2) "4100"
3) "uid:10086"
4) "3350"
5) "uid:10087"
6) "2850"

索引从 0 开始,0 9 即前 10 名。若榜单很大,ZREVRANGE 只取区间,不会全量扫描,性能稳定。展示时通常还要把 uid 映射成昵称、头像,这部分建议在应用层用缓存或批量查询完成,不要塞进 ZSet 的 member 里,否则 member 过长会浪费内存且不利于更新。

四、个人排名查询

查某个玩家的排名用 ZREVRANK(从高到低的名次,0 表示第一名):

ZREVRANK rank:game:score:2024w28 uid:10086
// 返回 1,表示第 2 名

如果成员不存在,返回 nil,业务上要区分“未上榜”和“第 0 名”。同时可用 ZSCORE 单独取该玩家分数:

ZSCORE rank:game:score:2024w28 uid:10086
// 返回 "3350"

把 ZREVRANK + 1 作为展示名次,ZSCORE 作为展示分数,即可拼出“我的排名卡片”。若还要展示“距离上一名差多少分”,可用 ZREVRANGE 取该名次前一位的分数做差,或直接用 ZREVRANGE ... WITHSCORES 取相邻区间。

五、完整流程与关键判断

把上述命令串起来,一次典型的“上报—查询”闭环是:玩家完成对局 → 业务校验局次幂等 → ZINCRBY 累加积分 → 返回最新总分 → 前端请求 Top 10 用 ZREVRANGE 0 9 WITHSCORES → 玩家查看自己名次用 ZREVRANK 与 ZSCORE。判断结果是否正确的依据是:Top N 顺序与分值单调递减一致;个人名次与 Top N 中同一 uid 的位置一致;分数更新后名次应同步变化。

ZSet 的 score 是 double 类型,积分建议用整数,避免浮点精度导致排名抖动。若出现同分,Redis 会按 member 的字典序排列,业务上可接受或额外用“先达到该分数者优先”的规则在应用层处理。
ZSet 游戏积分排行榜:命令与数据流 写入链路 玩家完成对局上报积分 ZINCRBY 累加 / ZADD 覆盖 返回更新后总分 ZSet 内部有序结构 key: rank:game:score:2024w28 #1 uid:10088 4100 score 最高 #2 uid:10086 3350 ZINCRBY +150 后 #3 uid:10087 2850 #4 uid:10089 2600 按 score 升序维护,查询时反向取 同分按 member 字典序 O(log N) 插入 / 更新 / 排名 读取链路 ZREVRANGE 0 9 WITHSCORES ZREVRANK / ZSCORE Top N 列表 + 我的名次 写入 读取 判断依据:Top N 分值单调递减 · 个人名次与 Top N 中同一 uid 位置一致 · 更新后名次同步变化

六、可直接参考的命令清单

操作命令说明
添加/覆盖分数ZADD key score member成员不存在则新增,存在则覆盖 score
累加分数ZINCRBY key increment member返回更新后分值,适合每局累加
Top NZREVRANGE key 0 N-1 WITHSCORES从高到低取前 N 名
个人名次ZREVRANK key member0 表示第一名,nil 表示未上榜
个人分数ZSCORE key member返回字符串分值,不存在返回 nil
榜单规模ZCARD key返回成员总数,用于分页判断
生产环境要注意:ZSet 会随成员增长占用内存,长期榜单应设置过期时间(EXPIRE)或按周期分 key;ZREVRANGE 大区间查询会返回大量数据,分页时用 ZREVRANGE key start stop 控制区间,避免一次拉取全量。

至此,一个最小可用的游戏积分排行榜已经闭环:写入用 ZADD/ZINCRBY,读取用 ZREVRANGE/ZREVRANK/ZSCORE,配合幂等与周期 key 设计,即可支撑中小规模游戏的实时榜单需求。