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

有没有有效的方法?

我正在使用Cloud Firestore作为数据库.我认为 Select 它而不是MongoDB是错误的,但是我正在进行这个项目,所以我必须使用Cloud Firestore.

这款应用程序将有3万用户使用.有没有办法在不获得全部3万用户的情况下做到这一点呢?

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

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

推荐答案

在排行榜中找到任意玩家的排名,以一种可zoom 的方式,是数据库中常见的难题.

有几个因素将驱动您需要 Select 的解决方案,例如:

  • 玩家总数
  • 单个玩家添加分数的评级
  • 添加新分数的速率(并发玩家*以上)
  • 得分范围:有界或无界
  • 分数分布(统一,或是他们的"热门分数")

Simplistic approach

典型的简单方法是计算所有得分较高的玩家,例如SELECT count(id) FROM players WHERE score > {playerScore}.

这种方法在低规模下有效,但随着玩家基数的增长,它很快就会变得很慢,而且资源昂贵(无论是在MongoDB还是在Cloud Firestore中都是如此).

Cloud Firestore本身并不支持count,因为它是不可伸缩的操作.您需要在客户端通过简单地计算返回的文档来实现它.或者,您可以使用Cloud Functions for Firebase在服务器端进行聚合,以避免返回文档的额外带宽.

Periodic Update

与其给他们一个实时排名,不如改成每隔一段时间就更新一次,比如每小时更新一次.例如,如果您查看Stack Overflow的排名,它们只会每天更新.

对于此方法,您可以 Select schedule a function,如果运行时间超过540秒,则可以 Select schedule App Engine.该函数将写出球员列表,就像ladder集合中的那样,具有填充有球员排名的新的rank字段.当玩家现在查看梯子时,你可以很容易地在O(X)时间内获得前X+玩家自己的排名.

更好的是,您还可以进一步优化并明确地将top X写为单个文档,因此要检索阶梯,您只需要阅读两个文档,top-X&;玩家,省钱,让它更快.

这种方法确实适用于任何数量的玩家和任何写入速率,因为它是在带外完成的.不过,随着你的成长,你可能需要根据你的支付意愿来调整频率.如果你不进行优化(例如,忽略所有0分球员,因为你知道他们最后一个打成平局),那么每小时3万名球员将是每小时0.072美元(每天1.73美元).

Inverted Index

在这个方法中,我们将创建一个倒排索引.如果得分范围明显小于玩家数量(例如,0-999分vs 30K玩家),则此方法有效.它还可以用于无限的分数范围,其中唯一分数的数量仍然显著小于玩家的数量.

使用名为"分数"的单独集合,您可以使用名为player_count的字段 for each 单独分数(如果没有人拥有该分数则不存在)拥有一个文档.

当玩家获得新的总分时,你将在scores个集合中进行1-2次写入.其中一次是写给他们的新分数+1到player_count,如果这不是他们第一次写-1到他们的旧分数.这种方法既适用于"您的最新分数就是您的当前分数",也适用于"您的最高分数就是您的当前分数"样式的阶梯.

找出一个玩家的确切排名就像SELECT sum(player_count)+1 FROM scores WHERE score > {playerScore}左右那么容易.

由于Cloud Firestore不支持sum(),所以您可以在客户端执行上述操作,但可以进行汇总.+1是因为总和是比你高的玩家的数量,所以加1可以得到该玩家的排名.

使用这种方法,您将需要阅读最多999个文档,平均500ish才能获得球员排名,尽管在实践中,如果您删除没有球员的分数,这将会更少.

了解新分数的写入速率很重要,因为您平均每2秒只能更新一次单个分数*,对于完美分布的分数范围0-999来说,这意味着500个新分数/秒**.您可以通过对每个分数使用distributed counters来增加此值.

* Only 1 new score per 2 seconds since each score generates 2 writes
** Assuming average game time of 2 minute, 500 new scores/second could support 60000 concurrent players without distributed counters. If you're using a "Highest score is your current score" this will be much higher in practice.

Sharded N-ary Tree

这是到目前为止最难的方法,但可以让你对所有玩家都有更快和实时的排名位置.可以将其视为上述倒排索引方法的读取优化版本,而上述倒排索引方法是此方法的写入优化版本.

您可以在适用的一般方法上阅读这篇'Fast and Reliable Ranking in Datastore'美元的相关文章.对于这种方法,您需要有一个有界分数(对于无界分数是可能的,但需要对以下内容进行更改).

我不推荐这种方法,因为对于任何具有半频繁更新的梯形图,您都需要为顶级 node 执行分布式计数器,这可能会抵消读取时间的好处.

Ternary tree example

Final thoughts

根据你为玩家展示排行榜的频率,你可以结合多种方法来优化它.

在较短的时间范围内结合"反向索引"和"定期更新"可以为所有玩家提供O(1)的排名访问权限.

只要在"定期更新"期间,所有玩家的排行榜被查看&>4次,你就会省钱,而且排行榜速度更快.

基本上是每段时间,比如说5-15分钟,你按降序阅读scores份文件中的所有文件.用这个,保持总共players_count个.用新字段players_above将每个分数重新写入名为scores_ranking的新集合.这个新字段包含不包括当前分数player_count的运行总数.

要获得球员的排名,您现在需要做的就是阅读球员从score_ranking->开始的分数文档;他们的排名是players_above+1.

Database相关问答推荐

如何限制报表中返回的行数?

如何在华为Appcube中创建和使用对象(模型)?

嵌套事务的目的

Postgresql:优化数字字段的列大小

数据库设计:多表与单表

如何在 MS Access 中实现 SQL INTERSECT 和 MINUS 操作

如何识别 DB2 端口号

PostgreSQL 是否对只读事务进行了一些性能优化

如何在 SQL Server 中生成随机数据?

用于 sql server 的免费国家、城市数据库

错误:mysqlnd cannot connect to MySQL 4.1+ using the old insecure authentication

应用程序服务器 JDBC 资源的 DataSource 或 ConnectionPoolDataSource

此平台不支持 LocalDB

为数据库应用程序留下审计跟踪/更改历史的有效策略?

Sqlite 判断表是否为空

传递依赖有什么问题?

可以将 SQLAlchemy 配置为非阻塞吗?

数据库与平面文本文件:当性能不是问题时, Select 一个而不是另一个的一些技术原因是什么?

以编程方式嵌入 Java h2 数据库

Guice、JDBC 和管理数据库连接