前言
大家好,我是田螺。
分享一道网上很火的腾讯面试题:亿级用户排行榜怎么设计呢?换种说法,王者荣耀亿级排行榜,如何设计?
本文田螺哥从面试的角度,跟大家一起探讨一下,如何回答更好呢?
- 数据库的order by为什么不行?
- 为什么Redis是排行榜的“扛把子”?
- Redis扛亿级数据可能存在哪些问题以及对应解决方案
- 实现方案:分治
- 巨人的肩膀,前人踩过的坑
- 公众号:捡田螺的小男孩
- github地址,感谢每颗star:github
1. 数据库的order by
很多小伙伴,一提到排行榜,就想到数据库的order by。
比如微信运动的步数排行:
select * from user_info
order by step desc
这个实现没有问题的,如果表的数据量少的话,反而推荐这样实现。如果数据量多呢。则存在问题,尤其还涉及亿级的数据量时~
在亿级用户+高并发实时更新的场景下,会彻底崩盘。 原因一句话:磁盘扛不住,排序算不动,并发撑不起。
2.为什么Redis是排行榜的扛把子
当数据量较大且需要实时更新并频繁查询时,使用 Redis 的zset有序集合更为适合。
zset
是 Redis
提供的一种数据结构,它类似于集合(set),但每个成员都关联着一个分数(score
),Redis 使用这个分数来对集合中的成员进行排序。
不仅仅是redis的zset支持排序,API简单易用,还因为redis的排序快、可扩展性强、能轻松应对高并发。
2.1 redis排序快
Redis 的数据全放内存,避免磁盘读写,核心操作秒回:
- 更新分数(ZADD)→ 快如闪电
- 查排名(ZREVRANK)→ 毫秒响应
- 查Top 100(ZREVRANGE)→ 瞬间出结果
以下为 Redis vs MySQL 性能对比(亿级数据)
操作 | Redis (Sorted Set) | MySQL (ORDER BY) |
---|---|---|
更新单个用户分数 | 0.1ms | 5~50ms(索引更新代价高) |
查询用户排名 | 0.2ms | 100ms~5s(依赖索引和缓存) |
获取Top 100 | 1ms | 1s~10s(内存排序或临时文件) |
高并发支撑能力 | 10万+/秒 | 1000+/秒(需分库分表) |
2.2 可扩展性强
分片存储轻松应对亿级数据。
Redis通过分片存储将数据拆分到多个实例,如同把1亿用户分配到10个小数据库,每个只需处理1000万数据,轻松实现:
- 1️⃣ 线性扩展:加机器就能提升容量和性能
- 2️⃣ 压力分散:读写请求分摊到不同分片,避免单点瓶颈
- 3️⃣ 独立扩容:热点分片可单独升级配置,不干扰其他节点
类比理解:
把一仓库货物(数据)分装到10辆卡车(分片),每辆车只运1/10的货,装卸速度自然快10倍!
2.3 轻松应对高并发
Redis用内存操作+单线程+IO多路复用三把利剑,轻松切开高并发大山:
- 1️⃣ 内存闪电读写:数据全放内存,比磁盘快10万倍
- 2️⃣ 单线程无锁:避免多线程切换损耗,原子操作不怕并发冲突
- 3️⃣ IO多路复用:一个线程监听万个连接,像银行超级柜员同时处理多窗口业务
田螺哥打个比喻吧:
Redis就像一个超高效快餐窗口:
- 只卖预制菜(内存数据) → 出餐快
- 一个收银员专注打单(单线程) → 不手忙脚乱
- 智能叫号器管理排队(IO多路复用) → 千人排队也能快速响应
据有关测试证明,单机Redis可扛10万+ QPS,分片集群轻松突破百万级并发。
3.Redis扛亿级数据可能存在哪些问题以及对应解决方案
3.1 热Key问题
比如“全服TOP100”榜单,容易造就热点key问题。
全服玩家频繁查询 ZREVRANGE leaderboard 0 99(获取Top 100),导致所有请求集中访问 同一个Key(leaderboard)。容易导致单分片CPU和带宽被打满(假设数据分片不均匀)。极端情况下Redis实例崩溃,全服排行榜瘫痪
可以通过这些方式解决:
- 1. 多级缓存(Redis + jvm本地缓存)
- 请求优先读本地内存缓存
- 缓存未命中时读Redis集群
- Redis集群内部缓存Top 100(设置更短TTL)
- 读写分离 + 从库负载均衡
主库处理写请求(更新分数)。多个从库轮询处理读请求(查Top 100)
- 分片Key设计
操作:将排行榜按分数区间拆分成多个Key,例如:
- leaderboard:top1(前100名)
- leaderboard:top2(101~1000名)
- leaderboard:rest(其他用户)
查询逻辑:查Top 100时,只需访问 leaderboard:top1。
3.2 内存爆炸
存储1亿用户,若每个键占32字节(如 user:123),仅键就需约3.2GB,加上分数和指针,内存压力巨大。
优化方案:
- 缩短键名:将 user:123 转换为整数(如123),利用 Redis 的 int 编码优化内存。
- 分片存储:按用户ID哈希分片到多个 Redis 实例,分散压力。
3.3 数据持久化风险
Redis 宕机可能导致最新数据丢失(即使开启AOF,默认每秒同步一次)。
容灾方案:
-
异步双写:更新分数时,同步写入 Kafka,由消费者异步落库 MySQL,用于故障恢复。
-
混合持久化:开启 RDB + AOF,平衡恢复速度与数据完整性。
4. 实现方案:分治
比如我们要查询王者荣耀巅峰赛的前一百积分的玩家。(其实就是一个TOP N问题)
我们可以按照这种思路:
- 按区间拆分
- 动态路由
- 聚合查询
4.1. 按区间拆分:把排行榜切成小块蛋糕**
怎么拆?
- 高分玩家放「金盘子」:2500分以上 →
rank:2500_2600
- 中分玩家放「银盘子」:2400~2500分 →
rank:2400_2500
- 低分玩家丢「大锅」:0~2400分 →
rank:0_2400
为什么快?
- 查Top 100只需翻「金盘子」,不用搅动整个大锅!
- 盘子越小 → 翻找速度越快
4.2 动态路由:玩家换区自动导航
怎么动?
- 玩家积分变化时,自动检测该去哪:
# 伪代码:2503分该放哪个区间?
if 2500 <= new_score < 2600:
扔进 rank:2500_2600
elif 2400 <= new_score < 2500:
扔进 rank:2400_2500
4.3 聚合查询:拼图式合并结果
怎么拼?
- 从高到低翻盘子:
- 先查「金盘子」→ 拿到前50名
- 不够100?再查「银盘子」→ 补50名
- 全局排序:
- 把两个盘子的100人按分数重新排座次
5. 巨人的肩膀,前人踩过的坑
- 慎用ZREVRANGE类全量操作
直接使用ZREVRANGE获取Top N时,若数据量过大(如1亿用户),会触发O(N)复杂度遍历,导致Redis线程阻塞
- 警惕黑马用户冲击分片策略
突然出现的高分用户(如积分暴涨至前0.1%)可能打乱原有分片规则,导致数据集中在某个分片引发热点。
- 内存爆炸与性能抖动
亿级用户存储占用内存超限,且RDB/AOF持久化时fork子进程引发内存翻倍。
- 数据迁移阻塞服务
用户积分跨分片迁移时,若未原子操作可能导致数据丢失或重复。