以可扩展的方式在排行榜中查找任意玩家的排名是数据库常见的难题。
有几个因素将推动您需要选择的解决方案,例如:
- 总人数
- 评价个别玩家的得分
- 添加新分数的比率(同时玩家 * 以上)
- 分数范围:有界或无界
- 分数分布(统一,或者是他们的“热门分数”)
简单化的方法
典型的简单方法是计算所有得分较高的玩家,例如SELECT count(id) FROM players WHERE score > {playerScore}
。
这种方法适用于小规模,但随着您的玩家群的增长,它很快变得既缓慢又耗费资源(在 MongoDB 和 Cloud Firestore 中)。
Cloud Firestore 本身不支持,count
因为它是不可扩展的操作。您需要在客户端通过简单地计算返回的文档来实现它。或者,您可以使用 Cloud Functions for Firebase 在服务器端进行聚合,以避免返回文档的额外带宽。
定期更新
与其给他们实时排名,不如将其更改为仅每隔一段时间(例如每小时)更新一次。例如,如果您查看 Stack Overflow 的排名,它们只会每天更新。
对于这种方法,您可以安排一个函数,或者如果运行时间超过 540 秒,则安排 App Engine。该函数将写出玩家列表,就像在一个ladder
集合中一样,并在一个新rank
字段中填充玩家排名。当玩家现在查看天梯时,您可以在 O(X) 时间内轻松获得顶部 X + 玩家自己的排名。
更好的是,您可以进一步优化并将顶部 X 显式写出作为单个文档,因此要检索梯子,您只需要阅读 2 个文档,top-X 和播放器,既节省资金又加快速度。
这种方法确实适用于任何数量的播放器和任何写入速率,因为它是在带外完成的。随着您的成长,您可能需要根据支付意愿调整频率。除非您进行了优化(例如,忽略所有 0 分玩家,因为您知道他们排在最后),否则每小时 30K 玩家将是每小时 0.072 美元(每天 1.73 美元)。
倒排索引
在这种方法中,我们将创建某种倒排索引。如果存在明显小于想要的玩家数量的有界分数范围(例如,0-999 分数与 30K 玩家),则此方法有效。它也适用于无限的分数范围,其中唯一分数的数量仍然明显小于玩家数量。
使用一个名为“scores”的单独集合,您有一个包含每个单独分数的文档(如果没有人拥有该分数,则不存在),其中包含一个名为player_count
.
当玩家获得新的总分时,您将在scores
集合中写入 1-2 次。一次写入是player_count
对他们的新分数+1 ,如果这不是他们第一次写 -1 到他们的旧分数。此方法适用于“您的最新分数是您当前的分数”和“您的最高分数是您当前的分数”样式的阶梯。
找出玩家的确切排名就像SELECT sum(player_count)+1 FROM scores WHERE score > {playerScore}
.
由于 Cloud Firestore 不支持sum()
,您可以执行上述操作,但在客户端求和。+1 是因为总和是在你之上的玩家数量,所以加 1 给你那个玩家的排名。
使用这种方法,您需要阅读最多 999 个文档,平均 500ish 才能获得玩家排名,但实际上,如果您删除零个玩家的分数,这会更少。
了解新分数的写入率很重要,因为您平均只能每 2 秒* 更新一次单个分数,对于 0-999 的完美分布分数范围,这意味着 500 个新分数/秒**。您可以通过为每个分数使用分布式计数器来增加这一点。
* 由于每个分数产生 2 次写入,因此每 2 秒只有 1 个新分数
** 假设平均游戏时间为 2 分钟,500 个新分数/秒可以支持 60000 名并发玩家,无需分布式计数器。如果您使用“最高分数是您当前的分数”,这在实践中会高得多。
分片 N 叉树
这是迄今为止最难的方法,但可以让您为所有玩家提供更快和实时的排名位置。可以将其视为上述倒排索引方法的读取优化版本,而上面的倒排索引方法是该方法的写入优化版本。
您可以按照此相关文章了解适用于一般方法的“数据存储中的快速可靠排名”。对于这种方法,您需要有一个有界分数(无界是可能的,但需要从下面进行更改)。
我不会推荐这种方法,因为您需要为任何具有半频繁更新的梯子的顶级节点执行分布式计数器,这可能会抵消读取时间的好处。
最后的想法
根据您为玩家显示排行榜的频率,您可以结合多种方法对其进行更多优化。
在更短的时间范围内将“倒排索引”与“定期更新”相结合,可以为所有玩家提供 O(1) 的排名访问权限。
只要在“定期更新”期间所有玩家的排行榜被查看 > 4 次,您就可以节省资金并拥有更快的排行榜。
基本上每个时期,比如说 5-15 分钟,您scores
按降序阅读所有文档。使用这个,保持运行总数players_count
。将每个乐谱重新写入一个名为scores_ranking
新字段的新集合中players_above
。这个新字段包含不包括当前分数的运行总数player_count
。
要获得玩家的等级,您现在需要做的就是从score_ranking
-> 他们的等级为players_above
+ 1读取玩家得分的文档。