实现一个 “实时刷新、按分数从高到低排序、且要能快速查询某个玩家名次”​ 的排行榜,你会使用什么数据结构,又怎么设计 Key 和核心的 Redis 命令?

需求分析

在游戏里设计排行榜,首先要明确几个核心需求:一是需要按分数实时排序;二是要能快速查询单个玩家的排名和分数;三是通常要能查询Top N的玩家列表。
基于这三点,我会优先选择 Redis 的 Sorted Set(有序集合)。因为它底层是跳表实现,插入、删除、按分数范围查询的复杂度都是 O(log N),非常适合实时排序场景。它能直接满足我们‘按分数排序’和‘查询Top N’的核心需求。

关键命令和基础实现

为了支持不同排行榜,故我的key设计会用rank:{活动ID}这样的格式,按活动区分。或者rank:{类型},按玩家uuid、门派等分类。

命令

  • 更新/插入玩家分数:ZADD leaderboard:1 分数 玩家ID
  • 查询玩家排名(从高到低):ZREVRANK leaderboard:1 玩家ID(这里返回的是从0开始的排名,如果要显示‘第1名’,需要+1)
  • 查询玩家分数:ZSCORE leaderboard:1 玩家ID
    *查询Top 10:ZREVRANGE leaderboard:1 0 9 WITHSCORES

进阶需求

当多名玩家分数相同时,需要按达到该分数的时间先后进行排名,先达到的排前面

我的解决方案是采用‘复合分数’:

  1. 设计思路:构造一个新的分数,让它同时编码‘原始分数’和‘时间’两个信息。公式是:

    最终分数 = 原始分数 * 基数 + (最大时间 - 时间戳)

  2. 如何取值:

    • 基数必须是一个远大于时间戳的数字(比如 10^10),确保原始分数的差异主导排序。

    • (最大时间 - 时间戳)是为了让更早的时间得到一个更大的数字,从而实现同分时,先到者排名更高。(最大时间

    举个例子:如果玩家A在时间戳 100 时获得 1000 分,玩家B在时间戳 200 时获得 1000 分。基数取 10^5,最大时间取 10^3。那么:

     玩家A的最终分数 = 1000 * 100000 + (1000 - 100) = 100,000,900
    
     玩家B的最终分数 = 1000 * 100000 + (1000 - 200) = 100,000,800
    
     玩家A的分数更高,所以排名更靠前,符合“同分先到者胜”的规则。
    

方案的问题

引入了一个新问题:业务代码从 Redis 里读出的分数,是一个被编码过的‘最终分数’,无法直接展示给玩家。

解决方案

为了兼顾排序效率和业务清晰度,我会采用一个读写分离的架构:

  1. 写入时(双写):

    • 用 ZADD更新复合分数到 Sorted Set(用于排序)。

    • 同时,用 HSET将玩家的原始分数存入一个 Hash 结构(如 leaderboard:detail:{活动ID})。

    • 为了保证这两步的原子性,我会用 Redis 的 MULTI/EXEC事务或 Lua 脚本​ 来执行,避免数据不一致。

  2. 读取时:

    • 查排行榜:先用 ZREVRANGE从 Sorted Set 拿到排名和玩家ID列表,再根据玩家ID列表去 Hash 里用 HMGET批量查询原始分数,然后组装数据返回。

    • 查个人:直接去 Hash 里 HGET原始分数,简单清晰。

  3. 架构价值:

    • 职责清晰:Sorted Set 只负责‘排’,Hash 只负责‘存’,代码可读性、可维护性极佳。

    • 空间换时间与清晰度:虽然多了一份存储,但对于读多写少的排行榜场景,用这点空间成本换来业务逻辑的极度简化,是完全值得的。”

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐