如何设计一个实时更新的游戏积分排行榜(Top 100)?
设计一个实时更新的游戏积分排行榜(Top 100),核心挑战在于高并发写入(大量玩家同时更新分数)与低延迟读取(玩家需要即时看到排名变化)之间的平衡。
在业界,Redis 的有序集合(Sorted Set / ZSET) 是解决此类问题的“银弹”。
以下是详细的系统设计方案,从基础架构到进阶优化。
1. 核心技术选型:Redis ZSET
传统的关系型数据库(如 MySQL)在处理排行榜时,需要使用 ORDER BY score DESC LIMIT 100。当数据量达到百万级且写入频繁时,这种查询会造成全表/索引扫描,导致数据库锁死或响应极慢。
Redis ZSET 的优势:
- 数据结构: 内部使用跳表(Skip List)和哈希表实现。
- 时间复杂度: 插入/更新分数是 ,获取 Top 100 是 。即使有几百万用户,操作也能在毫秒级完成。
- 天然有序: 数据插入即排序,无需额外的排序操作。
2. 基础架构设计
2.1 数据模型
在 Redis 中,我们定义一个 Key,例如 game:leaderboard:global。
- Score (分数): 浮点型(double)。
- Member (成员): 玩家 ID(User ID)。
2.2 核心操作流程
更新分数 (Write):
当玩家游戏结束结算分数时:bash# 将玩家 1001 的分数设为 5000 ZADD game:leaderboard:global 5000 "user_1001"注:
ZADD既可以用于新增,也可以用于更新。获取 Top 100 (Read):
客户端轮询或服务器推送:bash# 获取分数最高的前 100 名(带分数返回) ZREVRANGE game:leaderboard:global 0 99 WITHSCORES获取特定玩家排名:
bash# 获取玩家 1001 的排名(从 0 开始,所以通常需要 +1) ZREVRANK game:leaderboard:global "user_1001"
3. 关键问题解决方案
3.1 同分处理(Tie-breaking)
Redis ZSET 在分数相同时,默认按照 Member (User ID) 的字典序排列。但这通常不符合游戏规则(通常要求先达到该分数的排在前面)。
解决方案:带时间戳的组合分数
我们将分数设计为小数:整数部分为游戏分,小数部分为时间戳的逆运算。
- 公式:
最终Score = 游戏分 + (1 - 当前时间戳 / 未来某个极大时间戳) - 原理:时间戳越小(越早达到),小数部分越大,总分越高。
- 展示时:取整即可得到原始游戏分。
3.2 数据持久化与一致性
Redis 是内存数据库,如果宕机数据会丢失(尽管有 RDB/AOF,但仍有风险)。
架构方案:Write-Behind(异步写入)
- 写入路径: 游戏服务器 -> Redis (主) -> 更新成功立即返回给客户端。
- 持久化路径: 游戏服务器 -> 发送消息到 MQ (Kafka/RabbitMQ) -> 消费者服务 -> MySQL/Postgres。
- 恢复: 如果 Redis 挂了,从 MySQL 中读取数据重建 Redis ZSET。
3.3 巨量数据优化(百万/千万级用户)
虽然 Redis 能存几千万数据,但 ZSET 过大会导致内存占用高,且重构慢。
优化策略:仅缓存头部数据
如果只需要 Top 100,真的需要把 1000 万人都放在 Redis 里吗?
- 策略: Redis 中只保留分数最高的 1000 人(作为缓冲区)。
- 逻辑:
- 新分数产生时,先对比 Redis 中第 1000 名的分数。
- 如果
新分数 > 第 1000 名分数,则ZADD写入 Redis,并移除最后一名(ZREMRANGEBYRANK)。 - 如果
新分数 < 第 1000 名分数,直接写入 MySQL,不更新 Redis。
- 注意:这种方法无法快速查询 Top 1000 以外用户的具体排名,如果需求包含“显示我的排名(即使我是第 100 万名)”,则仍需全量 ZSET 或使用分段算法。
4. 实时推送架构(前端如何更新)
让所有客户端每秒轮询(Polling)服务器获取 Top 100 会造成巨大的带宽压力。
优化方案:
- 定时快照 (Snapshot):
服务器端每隔固定时间(如 1 秒或 5 秒)从 Redis 拉取一次 Top 100。 - 差异对比 (Diff) 或 直接广播:
将这 100 条数据通过 WebSocket 广播给在线玩家。 - CDN 缓存 (适合非强实时):
如果允许 5-10 秒延迟,服务器生成 JSON 文件上传至 OSS/CDN,客户端请求 CDN 地址。
5. 周期性排行榜(周榜/月榜)
游戏通常有赛季或周期重置。
设计:
使用动态 Key:
- 总榜:
game:leaderboard:all - 周榜:
game:leaderboard:2023W42(2023年第42周) - 月榜:
game:leaderboard:2023M10
利用 Redis 的 EXPIRE 功能,设置过期时间,让旧排行榜自动清理。
6. 完整架构图示
[客户端 App/Web]
|
| (WebSocket 订阅 Top 100 / HTTP POST 上传分数)
v
[ 游戏网关 / API Server ]
|
+-----> 1. 同步写入 (ZADD) -----> [ Redis Cluster (ZSET) ]
| ^
+-----> 2. 异步消息 (Kafka) | (周期性读取 Top 100)
| |
v [ 排行榜广播服务 ]
[ 数据库写入 Worker ] |
| | (Push)
v v
[ MySQL / DB ] <----------------------- [ 客户端 ]
(永久存储)
7. 总结清单
- 数据结构: 使用 Redis
ZSET。 - 命令:
ZADD(写),ZREVRANGE(读 Top N),ZREVRANK(读个人排名)。 - 同分策略: 使用
分数 + 时间戳倒数的浮点数。 - 持久化: Redis 做缓存,MySQL 做底表,通过 MQ 异步落库。
- 推送: 不要客户端轮询 Redis,使用后端定时聚合后通过 WebSocket 广播或 CDN 静态化。
- 大规模: 如果用户量过亿,考虑按积分段分桶(Bucket)存储,或者仅在 Redis 存储 Top K 用户。