我有项目需要显示前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名中,那么获得当前登录用户排名的最佳做法是什么?
答案 0 :(得分:33)
在排行榜中找到任意玩家的排名,以扩展的方式是数据库常见的难题。
有几个因素会推动您需要选择的解决方案,例如:
典型的简单方法是计算所有得分较高的玩家,例如SELECT count(id) FROM players WHERE score > {playerScore}
。
此方法工作规模较小,但随着玩家群体的增长,它很快变得既慢又耗资源(在MongoDB和Cloud Firestore中)。
Cloud Firestore本身不支持count
,因为它是一种不可扩展的操作。您只需计算返回的文档,就需要在客户端实现它。或者,您可以使用Cloud Functions for Firebase在服务器端进行聚合,以避免返回文档的额外带宽。
不要给他们提供实时排名,而是将其更改为每隔一小时更新一次,例如每小时更新一次。例如,如果您查看Stack Overflow的排名,它们只会每天更新。
对于这种方法,如果运行时间超过540秒,您可以schedule a function或schedule App Engine。该函数将在ladder
集合中写出播放器列表,其中新的rank
字段填充了玩家等级。当玩家现在观看阶梯时,您可以轻松地在O(X)时间内获得顶级X +玩家自己的排名。
更好的是,您可以进一步优化并明确地将顶部X写为单个文档,因此要检索梯形图,您只需要阅读2个文档,top-X&玩家,节省金钱并加快速度。
这种方法适用于任何数量的玩家和任何写入速率,因为它是在带外完成的。您可能需要根据您的支付意愿调整频率。每小时30K玩家每小时0.072美元(每天1.73美元),除非你做了优化(例如,忽略所有0分的玩家,因为你知道他们最后被绑在一起)。
在这种方法中,我们会创建一些反向索引。如果有一个有限的得分范围显着小于想要玩家的数量(例如,0-999得分对比30K玩家),则该方法有效。它也适用于无限分数范围,其中独特分数的数量仍然远远小于玩家数量。
使用一个名为“得分”的单独收集,您可以获得一个文档,其中包含每个单独得分(如果没有人得分,则不存在),其中包含一个名为player_count
的字段。
当玩家获得新的总分时,您将在scores
集合中进行1-2次写作。对于他们的新分数,一次写入是+1到player_count
,如果他们的第一次没有-1到他们的旧分数。这种方法适用于"您的最新分数是您当前的分数"和"你的最高分是你目前的分数"风格梯子。
找出玩家的确切排名就像SELECT sum(player_count)+1 FROM scores WHERE score > {playerScore}
一样简单。
由于Cloud Firestore不支持sum()
,您可以执行上述操作,但在客户端进行总结。 +1是因为总和是你上面的玩家数量,所以加1会给你那个玩家的等级。
使用这种方法,您需要阅读最多999个文档,平均500个以获得玩家排名,但实际上如果删除没有玩家的分数,这将会更少。
新分数的写入率很重要,因为您只能平均每2秒*更新一次个人分数,对于0-999的完美分布分数,这意味着500个新分数/第二**。您可以使用distributed counters为每个分数增加此值。
*每2秒只有1个新分数,因为每个分数产生2次写入 **假设平均游戏时间为2分钟,则每秒500个新分数可以支持60000个没有分布式计数器的并发玩家。如果您使用"最高分是您当前的分数"这在实践中要高得多。
这是迄今为止最难的方法,但可以让你为所有玩家提供更快,更实时的排名位置。它可以被认为是上面的反向索引方法的读优化版本,而上面的反向索引方法是对此的写优化版本。
您可以针对'Fast and Reliable Ranking in Datastore'关于适用的一般方法的相关文章进行操作。对于这种方法,您希望获得有限的分数(无限制可能,但需要从下面进行更改)。
我不推荐这种方法,因为您需要为任何具有半频率更新的梯形图的顶级节点执行分布式计数器,这可能会抵消读取时间的好处。
根据您为玩家展示排行榜的频率,您可以结合使用方法来优化这一点。
结合反向索引'使用'定期更新'在较短的时间范围内,您可以为所有玩家提供O(1)排名访问权限。
只要在所有玩家上查看排行榜>在“定期更新”期间的4次'你会省钱并拥有更快的排行榜。
基本上每个时段,比如5-15分钟,您按降序从scores
读取所有文档。使用此功能,保持总计players_count
。将每个分数重新写入名为scores_ranking
的新集合中,并使用新字段players_above
。此新字段包含不包括当前分数player_count
的运行总计。
要获得玩家的排名,您现在需要做的就是阅读来自score_ranking
- >的玩家得分的文档。他们的等级为players_above
+ 1。
答案 1 :(得分:1)
另一种观点 - NoSQL 和文档存储使此类任务过于复杂。如果您使用过 Postgres,那么使用计数函数就非常简单了。 Firebase 很诱人,因为它很容易上手,但像这样的用例是关系数据库大放异彩的时候。 Supabase 值得一看 https://supabase.io/ 与 firebase 类似,因此您可以快速使用后端,但它是开源的并基于 Postgres 构建,因此您可以获得关系数据库。
答案 2 :(得分:0)
Dan没有提到的解决方案是使用安全规则与Google Cloud Functions结合使用。
创建高分榜的地图。例如:
然后:
这对我来说是最简单的选择。它也是实时的。
答案 3 :(得分:0)
我将在我的在线游戏中实现并且在您的用例中可用的此处未提及的一种解决方案是,估计用户的排名(如果他们不在任何可见的排行榜中),因为坦率地说,用户不在知道(或关心?)他们是第22882位还是第22838位。
如果第20位得分为250,并且总共有32,000名玩家,那么每个点平均值得127个位置,尽管您可能希望使用某种曲线,以便当他们向上移动时他们不会每次准确跳到127个位置-排名中的大多数跳都应该接近零点。
由您决定是否将其标识为估计值,并且可以在数字中加些盐,使其看起来真实可靠:
// Real rank: 22,838
// Display to user:
player rank: ~22.8k // rounded
player rank: 22,882nd // rounded with random salt of 44
我要去做。