Redis 场景题:1000万人在线,如何设计实时排行榜?(面试复盘)
前言
ZSet肯定不行,但我当时也不知道怎么改,然后就被问穿了。现在整理了一下这道题的解题思路,想看看大家觉得这个方案有没有漏洞。
场景分析
- 数据规模:1000 万用户(DAU)。
- 并发量:假设活动高潮期,1000 万人中有 1% 的人同时操作,写 QPS 可能高达 10 万。
- 核心需求:实时更新用户积分,并展示前 100 名。
Redis 单机极限:
| 类型 | 最高QPS |
|---|---|
| 纯内存读(GET) | 10万 ~ 15万 QPS |
| 简单写(SET/INCR) | 8万 ~ 12万 QPS |
| Lua 脚本 | 6万 ~ 10万 QPS |
| ZSet 写(ZADD/ZINCRBY) | 5万 ~ 8万 QPS |
阶段一:全量 ZSet
Bash
# 用户送礼
ZINCRBY rank:global 100 "user_id"
# 查榜单
ZREVRANGE rank:global 0 99 WITHSCORES
- 性能瓶颈:Redis 是单线程的。
ZINCRBY的时间复杂度是 O(\log N)。当 N=1000万时,虽然 \log N 不算大,但面对 10万 QPS 的写入,单机 CPU 肯定被打满(通常 Redis 单机写极限在 8w 左右,复杂指令更低)。 - 大 Key 问题:一个 Key 存 1000 万个元素,即使只有 ID 和 Score,也是一个巨大的 Big Key。在集群扩容、数据迁移或持久化(RDB/AOF)时,会阻塞主线程,风险极高。
阶段二:Hash + Top100 ZSet
设计思路:
- 全量数据:用 Redis Hash 存储,
userId -> score。Hash 的操作是 O(1) - 排序数据:ZSet 里永远只留 100 个人。
每次送礼流程:
HINCRBY更新 Hash 中积分 —— O(1)- 判断:
- 是否 ≥ 当前第100名的score
- 如果大于:
ZREM删除第100名ZADD插入当前用户
复杂度近似:M × log(100)
自我反思:
不过这种单机是扛不住的,单机最多10万写,但是即使百分之一算,那也得10万QPS了
所以还是得redis集群,将ZSet分片,此时由于网络延迟,上面这个方案查top100最低分就不可接受了.
复杂度 M ×( log(100) + 网络延迟)
阶段三:集群分片写 + 内存缓冲 + 异步聚合
1. 写入分片
不要把 1000 万人塞进一个 Key。
- 定义 100 个桶(Bucket),比如
rank:bucket:1到rank:bucket:100。 - 用户送礼时,根据
hash(userId) % 100决定去哪个桶。 - 这样就把 10 万 QPS 分散到了 Redis 集群的多个节点上,每个节点只承载 1000 左右的 QPS,完全没压力。
2. 服务端缓冲—— 减少网络 IO
即使分片了,请求量还在。可以在**应用服务器(后端代码)**做一层缓冲:
- 用户送礼先写到本地内存
Map<UserId, Score>。 - 每隔 500ms,或者攒够 100 个请求,再打包通过 Pipeline 批量发给 Redis。
- 收益:把 Redis 的写 QPS 直接降低一个数量级。
3. 异步读 —— 解决读压力
- 搞一个定时任务(比如每秒一次)。
- 从 100 个分桶里,分别取出各自的 Top 100。
- 在服务内存里把这 100 \times 100 条数据汇总,再算一次总的 Top 100。
- 把结果塞入一个单独的 Key:
rank:final:top100。 - 前端只查这个 Key。