惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
月光博客
月光博客
aimingoo的专栏
aimingoo的专栏
Google DeepMind News
Google DeepMind News
Recent Announcements
Recent Announcements
MyScale Blog
MyScale Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
SegmentFault 最新的问题
Hugging Face - Blog
Hugging Face - Blog
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
F
Fortinet All Blogs
小众软件
小众软件
D
Docker
U
Unit 42
博客园 - 聂微东
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
爱范儿
爱范儿
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
IT之家
IT之家
云风的 BLOG
云风的 BLOG
博客园 - 司徒正美
有赞技术团队
有赞技术团队
腾讯CDC

Aimee's Blog

秒杀怎么防机器人:验证码之外的几道防线 防薅羊毛怎么做:号码风控、设备指纹与分层策略 接口限流怎么选:固定窗口、滑动窗口、令牌桶 批量打包下载怎么设计:流式 ZIP 与异步任务 图片处理怎么做:异步处理与 CDN 实时参数 大文件上传怎么设计:分片、断点续传、秒传 报表可重跑的设计:快照、水位线与幂等 UV 统计为什么有误差:精确计数与近似计数怎么选 大数据量导出怎么做:流式写入与异步任务 内容审核怎么选:先审后发,还是先发后审 实时热榜怎么设计:Redis ZSet 与热度分值 互关状态怎么保持同步:双向关系的维护与缓存失效 并发点赞的计数设计:原子操作与最终对账 删了父评论,子评论怎么处理——先想清楚用户看到的是什么 定时消息任务触发了两次 消息量大,写扩散还是读扩散——接到这个需求先问清楚规模 同一条消息推送了三次 活动结束了,用户还在收短信 接到注销需求,先问两个问题 微服务与服务拆分:何时拆、怎么拆 异步与事件驱动架构:把协作从「打电话」改成「发消息」 高可用设计:怎么让系统尽量不宕机 可扩展性设计:怎么让系统加机器就能扛更多 缓存架构:多级缓存怎么搭 高并发三板斧:限流、熔断、降级 架构设计到底在设计什么 —— 从单体到微服务的演进 服务成本账:一个服务一个月烧多少钱 可观测性:线上出问题怎么查 API 设计:好接口长什么样 消息队列:为什么要 MQ,以及丢失、重复、顺序怎么破
排行榜的两类需求,难度差了一个数量级
Aimee · 2026-08-10 · via Aimee's Blog

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

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

一是展示前 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 为空,自动从零开始

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