🎮 前言:当 1000 万玩家同时冲榜
场景还原:
某款 MOBA 手游开启“全服天梯赛”,预计活跃玩家 1000 万。
策划要求:“积分变化要实时反映在排行榜上,我就要看到那个排名跳动的爽感!”
作为后端开发,你自信地掏出了 Redis 的ZSet:
ZADD rank_global1050"Player_A"结果活动开启第 10 分钟,Redis CPU 飙升 100%,网络带宽被打满,客户端请求全部超时。运维查监控发现,rank_global这个 Key 的大小已经超过了 500MB。
恭喜你,制造了一个标准的“BigKey”事故。
在千万级数据下,单纯依赖一个 ZSet 是死路一条。今天,我们就来聊聊如何设计一个打不死的实时排行榜系统。
💀 瓶颈分析:ZSet 为什么会挂?
Redis 的ZSet底层使用的是跳表 (SkipList)和哈希表。
虽然它的增删查复杂度是O(logN)O(\log N)O(logN),但在千万级数据下存在两大硬伤:
- BigKey 问题:一个 Key 包含 1000 万个元素,进行
ZRANGE或ZREVRANGE时,如果页大小设置不当,会造成大量的网络 IO。更有甚者,如果这个 Key 需要迁移(集群 Rebalance),会阻塞 Redis 很久。 - 写并发瓶颈:Redis 是单线程写。如果全服玩家同时拿分,TPS 达到 10 万+,单机 Redis 根本扛不住高频的
ZADD更新。
⚔️ 优化方案一:分治法 —— ZSet 分桶 (Sharding)
既然一个 ZSet 装不下,那我们就把它拆成 128 个,甚至 1024 个。
核心思路:
- 入榜:根据
UserID % N将玩家分散到rank_0到rank_127这 128 个小 ZSet 中。 - 查询 Top N:这是难点。如果我要查全服 Top 10,我必须从这 128 个 ZSet 中,分别取出各自的 Top 10,然后在应用内存中进行“归并排序”,选出最终的 Top 10。
架构图解:
- 优点:彻底解决了 BigKey 问题,写入性能线性扩展。
- 缺点:查询 Top N 变复杂了。但通常排行榜只看前 100 名,归并 128 * 100 个数据的开销完全可以接受。
🚀 优化方案二:降维打击 —— 头部实时,尾部离线
策划说要“实时”,但真的所有 1000 万人都关心自己是第 9,999,998 名吗?
显然不是。只有头部玩家(前 1000 名)在乎毫秒级排名,普通玩家只要知道自己大概在前 50% 就行了。
设计策略:
- Top K 实时榜:Redis 中只保留积分最高的Top 5000用户。
- 当玩家积分变化时,如果积分 > 第 5000 名的积分,则
ZADD进 Redis。 - 如果 Redis 元素超过 5000,则定期移除末尾用户。
- 当玩家积分变化时,如果积分 > 第 5000 名的积分,则
- 全量离线榜:所有用户的完整积分数据存入 MySQL 或 HBase。
- 普通用户的排名,通过定时任务(每 5 分钟)或者查询数据库统计得出。
- 或者直接显示“未上榜”。
代码逻辑示例 (Lua 脚本保障原子性):
-- ARGV[1]: 玩家 ID-- ARGV[2]: 新积分-- ARGV[3]: 排行榜容量 (如 5000)localkey=KEYS[1]localscore=tonumber(ARGV[2])-- 1. 更新分数redis.call('ZADD',key,score,ARGV[1])-- 2. 检查是否超出容量localcount=redis.call('ZCARD',key)ifcount>tonumber(ARGV[3])then-- 移除分数最低的那个人 (排名在 0 到 count-limit 之间的人)redis.call('ZREMRANGEBYRANK',key,0,count-tonumber(ARGV[3])-1)end🛡️ 优化方案三:写缓冲 (Write-Back)
在直播间刷礼物场景下,土豪手速极快,一秒钟送出 100 个“666”。
如果每次送礼都请求一次 Redis,网络开销太大。
策略:本地聚合
- 在应用服务器(Java/Go)内存中维护一个
ConcurrentHashMap<UserId, Score>。 - 收到加分请求时,只更新内存。
- 每隔1 秒,或者累积满 100 个操作,批量将变化刷入 Redis (
ZINCRBY)。
这样可以将 Redis 的写 QPS 降低一个数量级(10w -> 几千)。虽然有 1 秒的数据延迟,但在用户体验和系统稳定性之间,这是极佳的权衡。
📊 总结
设计千万级排行榜,不要试图用一个 ZSet 解决所有问题。
- 要分片:避免 BigKey 阻塞 Redis。
- 要截断:只存头部数据,尾部数据走数据库。
- 要缓冲:合并高频写入,保护 Redis 带宽。
没有完美的技术,只有最适合业务场景的架构。