|

Aimee

Write the Code. Change the World.

排行榜的两类需求,难度差了一个数量级

· 分享镜

排行榜的两类需求,难度差了一个数量级

"做个排行榜"——产品说完这句话,你要先问清楚两件事:

一是展示前 N 名的列表,还是要显示用户自己的排名?

前 N 名列表很好做,自己的排名是另一个问题。

二是用户量级是多少?

几万和几千万,实现完全不同。


展示前 N 名:Redis ZSet 够了

// 写入:用户得分更新
redis.zadd("rank:game", score, String.valueOf(userId));

// 读取:前 100 名(降序)
Set<ZSetOperations.TypedTuple<String>> top100 =
    redis.zrevrangeWithScores("rank:game", 0, 99);

ZREVRANGE 返回的是已排序结果,不需要在应用层排序,O(log N + K)。

展示时要做的只有:用 userId 批量查用户名、头像等信息,再拼到排名列表里。

分页也简单:第 2 页取 ZREVRANGE rank:game 100 199,以此类推。


"我排第几名":ZREVRANK

这比前 N 名列表多了一个场景:用户想知道自己的排名。

千万不要这样查:

SELECT COUNT(*) + 1 FROM rank WHERE score > (SELECT score FROM rank WHERE user_id = ?)

数据量大时,这是全表扫描,慢且不一定准(并发更新时)。

Redis 的 ZREVRANK 专为这个设计:

// 用户在降序榜中的排名(0 表示第 1 名)
Long rank = redis.zrevrank("rank:game", String.valueOf(userId));
if (rank == null) {
    return -1; // 未上榜
}
return rank + 1; // 转成从 1 开始

时间复杂度 O(log N),1 亿用户也是毫秒级。


附近排名:"我的前后各 5 名"

这个需求很常见,实现也简单:

long myRank = redis.zrevrank("rank:game", String.valueOf(userId)); // 从 0 开始
long start = Math.max(0, myRank - 5);
long end = myRank + 5;

Set<ZSetOperations.TypedTuple<String>> nearby =
    redis.zrevrangeWithScores("rank:game", start, end);

不需要多次查询,一次 ZREVRANGE 就拿到附近的所有人。


超大规模:ZSet 还撑得住吗

ZSet 每个成员约 64-80 字节。1 亿用户大约占 6-8 GB 内存,单台 Redis 能 hold 住,但做备份、主从同步时会有压力。

实用的分层方案:

  • 全量 ZSet:保存所有用户得分,用来查自己的排名(ZREVRANK
  • 榜单缓存:定时(如每 5 分钟)从 ZSet 取前 1000 名写入独立缓存,页面展示时只读这份缓存

这样即使 ZSet 很大,页面展示的前 N 名都走小缓存,读取极快;排名查询走 ZREVRANK,O(log N) 不受影响。


榜单有效期和重置

周榜、月榜需要定时重置,别直接 DEL

// 重置周榜:重命名旧榜留存归档,新建空 key
String archiveKey = "rank:game:week:" + lastWeekId;
redis.rename("rank:game:weekly", archiveKey);
redis.expire(archiveKey, 30, TimeUnit.DAYS);
// 新的 weekly key 为空,自动从零开始

归档的榜单供"历史榜单"查询,不影响当前榜的性能。

评论0

登录后参与评论。

还没有评论,来抢沙发吧。

回到顶部