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 的位置一致;分数更新后名次应同步变化。
六、可直接参考的命令清单
| 操作 | 命令 | 说明 |
|---|---|---|
| 添加/覆盖分数 | ZADD key score member | 成员不存在则新增,存在则覆盖 score |
| 累加分数 | ZINCRBY key increment member | 返回更新后分值,适合每局累加 |
| Top N | ZREVRANGE key 0 N-1 WITHSCORES | 从高到低取前 N 名 |
| 个人名次 | ZREVRANK key member | 0 表示第一名,nil 表示未上榜 |
| 个人分数 | ZSCORE key member | 返回字符串分值,不存在返回 nil |
| 榜单规模 | ZCARD key | 返回成员总数,用于分页判断 |
EXPIRE)或按周期分 key;ZREVRANGE 大区间查询会返回大量数据,分页时用 ZREVRANGE key start stop 控制区间,避免一次拉取全量。至此,一个最小可用的游戏积分排行榜已经闭环:写入用 ZADD/ZINCRBY,读取用 ZREVRANGE/ZREVRANK/ZSCORE,配合幂等与周期 key 设计,即可支撑中小规模游戏的实时榜单需求。