Firebase 排行榜排名

IT技术 javascript database firebase angularfire2 google-cloud-firestore
2021-02-08 00:34:04

我有一个项目,我需要显示前 20 名的排行榜,如果用户不在排行榜中,他们将以其当前排名出现在第 21 位。

有没有有效的方法?

我使用 Cloud Firestore 作为数据库。我相信选择它而不是 MongoDB 是错误的,但我正处于项目的中间,所以我必须使用 Cloud Firestore 来完成。

该应用程序将被 30K 用户使用。有没有办法在不获得所有 30k 用户的情况下做到这一点?

 this.authProvider.afs.collection('profiles', ref => ref.where('status', '==', 1)
        .where('point', '>', 0)
        .orderBy('point', 'desc').limit(20))

这是我为获得前 20 名所做的代码,但是如果他们不在前 20 名中,那么获得当前登录用户排名的最佳做法是什么?

5个回答

以可扩展的方式在排行榜中查找任意玩家的排名是数据库常见的难题。

有几个因素将推动您需要选择的解决方案,例如:

  • 总人数
  • 评价个别玩家的得分
  • 添加新分数的比率(同时玩家 * 以上)
  • 分数范围:有界或无界
  • 分数分布(统一,或者是他们的“热门分数”)

简单化的方法

典型的简单方法是计算所有得分较高的玩家,例如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读取玩家得分的文档

哇。我在 stackoverflow 上见过的最佳答案。我肯定需要再读几遍您的答案,以了解如何实施它。感谢您抽出时间来回答。
2021-03-15 00:34:04
很好的答案,但我想再添加一个解决方案。它在 firebase 中不支持,但 couchdb 总是可以在查询时给出偏移量。具有索引的 NoSQL 数据库在像 couchdb 这样的查询时应该保持分片计数和总和以抵消。遗憾的是没有多少数据库支持这个功能
2021-03-20 00:34:04
精彩的分析。但就像大多数谷歌求职面试一样,你的重点一直是优化时间,但我也想优化我的 Firebase 账单。稍微延迟排名在节省 Firebase 账单方面是可以的。
2021-03-21 00:34:04
@Thaina 没有得到你。您能否详细说明,如果可能,将其作为单独的详细答案发布。
2021-03-31 00:34:04
谢谢西蒙!没问题,希望它有帮助:)
2021-04-01 00:34:04

我将在我的在线游戏中实施并且可能在您的用例中使用的一种此处未提及的解决方案是估计用户的排名,如果他们不在任何可见的排行榜中,因为坦率地说,用户不会知道(或关心?)他们是否排名第 22,882 或 22,838。

如果第 20 名的得分为 250 分并且总共有 32,000 名玩家,那么每个低于 250 的点平均value 127 个位置,尽管您可能想要使用某种曲线,以便他们向上移动一个点到可见的底部排行榜他们每次都不会精确地跳 127 个位置 - 排名中的大多数跳跃应该更接近于零点。

是否要将此估计排名确定为估计值取决于您,您可以在数字中添加一些随机盐,使其看起来真实:

// Real rank: 22,838

// Display to user:
player rank: ~22.8k    // rounded
player rank: 22,882nd  // rounded with random salt of 44

我会做后者。

另一种观点 - NoSQL 和文档存储使这种类型的任务过于复杂。如果您使用过 Postgres,那么使用计数函数就非常简单了。Firebase 很诱人,因为它很容易上手,但像这样的用例是关系数据库大放异彩的时候。Supabase 值得一看https://supabase.io/ 类似于firebase ,因此您可以快速使用后端,但它是开源的并基于 Postgres 构建,因此您可以获得关系数据库。

Dan 没有提到的一个解决方案是将安全规则与 Google Cloud Functions 结合使用。

创建高分地图。例子:

  • 高分(前 20 名)

然后:

  1. 授予用户对 highScores 的写/读访问权限。
  2. 给文档/地图 highScores 属性中的最小分数。
  3. 让用户只在他的分数 > 最小分数时写入 highScores。
  4. 在 Google Cloud Functions 中创建一个写入触发器,该触发器将在写入新的高分时激活。在该函数中,删除最小的分数。

这在我看来是最简单的选择。它也是实时的。

绝对是前 20 名减少负荷的好主意,尽管对要求的困难部分(任意玩家的排名)没有帮助。一个警告是 Cloud Functions 对订单执行的保证,这会在此处产生竞争问题,您需要在 Cloud Functions 中处理。
2021-03-14 00:34:04
为什么用户需要对 highScores 的写访问权限?然后,他们可以写入任何值。我相信 highScores 必须只写在服务器端,否则它可能会被黑客入侵。
2021-03-23 00:34:04

你可以用云存储做点什么。所以手动创建一个包含所有用户分数的文件(按顺序),然​​后您只需阅读该文件并找到该文件中分数的位置。

然后要写入文件,您可以设置一个 CRON 作业来定期添加带有标志 isWrittenToFile false 的所有文档,将它们全部添加到文件中(并将它们标记为 true)。这样你就不会吃掉你的写作。每次用户想要查看他们的位置时读取文件可能不是那么密集。它可以通过云功能完成。