【问题标题】:Global leaderboard in Google App EngineGoogle App Engine 中的全球排行榜
【发布时间】:2014-03-25 14:25:02
【问题描述】:

我想使用 Google App Engine (Python) 为移动游戏构建一个后端,其中包括针对所有玩家的“实时”全球排行榜,用于持续一定天数的活动。

典型的用法如下: - 用户开始和结束战斗,获得积分(战斗2-5分钟) - 积分在活动期间累积在玩家的账户中。 - 玩家可以随时查看排行榜。 - 排行榜将返回前 10 名玩家,以及分数高于和低于玩家分数的 5 名玩家。

现在,实时方面没有真正的限制,板可以每 30 秒更新一次,到每小时更新一次。我希望它尽可能“快”,而不会花费太多。

由于我对GAE不是很熟悉,所以这是我想到的解决方案:

  • 每个玩家实体都有一个 event_points 属性
  • 使用 Cron 作业,定期查询数据存储区中得分不为零的所有玩家。查询是 已排序。
  • cron 作业然后遍历查询结果,写回每个 Player 实体中的排名。

一想到这个解决方案,感觉很“蛮力”。

此解决方案的问题在于所有实体的读写成本。 如果我们最终有 50K 活跃用户,这将意味着定期进行 50K+1 次读取和 50k+1 次写入的排序查询,这可能非常昂贵(取决于时间间隔)

我知道 memcache 可以是一种防止某些读取和某些写入的方法,但是如果某些实体不在 memcache 中,那么查询它是否有意义? 另外,我读过 memcache 可以随时刷新,所以除非有办法廉价地“备份”它,否则这似乎是一种危险的用途,因为数据相对重要。

有没有更简单的方法来解决这个问题?

【问题讨论】:

  • 投票结束这个问题的人不理解“基于意见”的含义。我提供的答案是基于 App Engine 的文档,而不是我个人的偏好。显然,解决问题的方法通常不止一种,但这是一个很好的问题,有一个具体的、详细描述的问题。

标签: python google-app-engine cron leaderboard


【解决方案1】:

这是一个谷歌开发者article 解释类似的问题和使用谷歌代码 JAM ranking library 的解决方案。可以在 Google 群组forum 中讨论对该库的进一步帮助和扩展。

该库基本上创建了一个 N 叉树,每个节点都包含特定范围内的分数计数。分数范围被进一步划分,直到叶子节点只有一个分数。树遍历( O log(n) )可用于查找得分高于特定得分的玩家数量。那就是玩家的等级。它还建议将分数提交请求聚合在一个拉任务队列中,然后在后端的后台线程中批量处理。

【讨论】:

  • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接的答案可能会失效。
  • @MikePrecup 添加简要说明
【解决方案2】:

您不需要 50,000 次读取或 50,000 次写入。解决方案是在您的 points 属性上设置排序顺序。每次更新时,数据存储都会自动更新其顺序,这意味着除了 points 属性之外,您不需要 rank 属性。因此,您不需要 cron 作业。

然后,当您需要检索排行榜时,您运行两个查询:一个查询与您的用户具有更多或相等点数的 6 个实体;第二 - 对于点数少于或相等的 6 个实体。合并结果,这就是您要向用户显示的内容。

对于前 10 个查询,您可能希望将其结果放入 Memcache 中,并设置过期时间,例如 5 分钟。当你需要它时,你首先检查 Memcache。如果未找到,请运行查询并更新 Memcache。

编辑:

澄清查询部分。您需要设置排序顺序和不等式过滤器的正确组合以获得所需的结果。根据 App Engine 文档,查询按以下顺序执行:

  1. 标识与查询的种类、过滤器对应的索引 属性、过滤器运算符和排序顺序。
  2. 从 第一个满足所有条件的实体的索引开始 查询的过滤条件。
  3. 继续扫描索引,返回 每个实体依次进行,直到遇到不满足的实体 过滤条件,或者到达索引的末尾,或者有 收集了查询请求的最大结果数。

因此,对于一个查询,您需要将 ASCENDING 顺序与 GREATER_THAN_OR_EQUAL 过滤器结合起来,而将 DESCENDING 顺序与 LESS_THAN_OR_EQUAL 过滤器结合起来用于另一个查询。在这两种情况下,您都将检索结果的限制设置为 6。

另一个注意事项:您将限制设置为 6 个实体,因为这两个查询都将返回用户本身。您可以添加另一个过滤器(userId NOT_EQUAL 到您的用户 ID),但我不推荐它 - 成本不值得节省。显然,您不能对积分使用 GREATER_THAN/LESS_THAN 过滤器,因为许多用户可能拥有相同数量的积分。

【讨论】:

  • 数据存储区中的第二个查询非常昂贵,因为它需要游标遍历前 N 个实体,因此他最终可能每个请求执行 50000 次读取。维护一个像 table[x]=[points], x=1-1000,points=list of points 在范围内的表格可能不是一个坏主意。这可以通过 cron 作业完成,并且需要 O(N) 操作。这可以很好地扩展。对于少量点,您可以简单地保留一张腌制地图,对于更多点,您可以为此创建一个专用实体。
  • 当然不是!无需使用游标。而且绝对不需要任何特殊的桌子。所有数据都已在索引中可用。
  • 这是对数据存储中的索引的扫描,而不是对实体本身的扫描。你不用付钱。您只需为检索到的实体付费。
  • 感谢您的回答,我一定会深入研究这个策略!
  • 想知道当有新的锦标赛时如何重置排行榜?所有的点都需要归零。
【解决方案3】:

这是否更简单是有争议的。

我假设排名不仅仅是对积分进行排序的问题,在这种情况下,这只是一个简单的查询。 I 排名涉及其他因素,而不仅仅是当前分数。

我会考虑为用户(实际上是一个队列)的每次更新点写出一个事件记录。任务运行收集所有当前事件记录,此外,您还维护一组代表排行榜顶部的记录。根据传入的事件记录调整这组记录。处理后丢弃事件记录。这会将您的读写限制为仅在一个小时间窗口内的活动事件。排行榜可能是一个单一的实体,通过键获取并缓存。

我假设您可能有不同的排名方案,例如当前活跃排名(当前 7 天)与所有时间排名。 (即一段时间不玩的玩家不会有很好的当前排名)。

当玩家查看他们的排名时,您可以通过两个简单的查询 Players.query(Players.score > somescore).fetch(5)Players.query(Players.score < somescore).fetch(5) 来做到这一点,这不应该花费太多,您可以缓存它们。

【讨论】:

    猜你喜欢
    • 2013-06-20
    • 1970-01-01
    • 2011-02-01
    • 2018-02-07
    • 1970-01-01
    • 1970-01-01
    • 2021-03-03
    • 2015-01-29
    • 2019-02-21
    相关资源
    最近更新 更多