⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 捡田螺的小男孩 「捡田螺的小男孩」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

前言

分享一道网上很火的腾讯面试题:亿级用户排行榜怎么设计呢?换种说法,王者荣耀亿级排行榜,如何设计?

本文从面试的角度,跟大家一起探讨一下,如何回答更好呢?

  • 数据库的order by为什么不行?
  • 为什么Redis是排行榜的“扛把子”?
  • Redis扛亿级数据可能存在哪些问题以及对应解决方案
  • 实现方案:分治
  • 巨人的肩膀,前人踩过的坑

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实例崩溃,全服排行榜瘫痪

可以通过这些方式解决:

  • 多级缓存(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 聚合查询:拼图式合并结果

怎么拼?

  1. 从高到低翻盘子:
  • 先查「金盘子」→ 拿到前50名
  • 不够100?再查「银盘子」→ 补50名
  1. 全局排序:
  • 把两个盘子的100人按分数重新排座次

5. 巨人的肩膀,前人踩过的坑

  • 慎用ZREVRANGE类全量操作

直接使用ZREVRANGE获取Top N时,若数据量过大(如1亿用户),会触发O(N)复杂度遍历,导致Redis线程阻塞

  • 警惕黑马用户冲击分片策略

突然出现的高分用户(如积分暴涨至前0.1%)可能打乱原有分片规则,导致数据集中在某个分片引发热点。

  • 内存爆炸与性能抖动

亿级用户存储占用内存超限,且RDB/AOF持久化时fork子进程引发内存翻倍。

  • 数据迁移阻塞服务

用户积分跨分片迁移时,若未原子操作可能导致数据丢失或重复。

文章目录
  1. 1. 前言
  2. 2. 1. 数据库的order by
  3. 3. 2.为什么Redis是排行榜的扛把子
    1. 3.1. 2.1 redis排序快
    2. 3.2. 2.2 可扩展性强
    3. 3.3. 2.3 轻松应对高并发
  4. 4. 3.Redis扛亿级数据可能存在哪些问题以及对应解决方案
    1. 4.1. 3.1 热Key问题
    2. 4.2. 3.2 内存爆炸
    3. 4.3. 3.3 数据持久化风险
  5. 5. 4. 实现方案:分治
    1. 5.1. 4.1. 按区间拆分:把排行榜切成小块蛋糕
    2. 5.2. 4.2 动态路由:玩家换区自动导航
    3. 5.3. 4.3 聚合查询:拼图式合并结果
  6. 6. 5. 巨人的肩膀,前人踩过的坑