首页 > 解决方案 > Firestore:存储由外部源生成的单调递增的 ID

问题描述

关于我想要达到的目标的背景

我正在为应用实施应用购买验证。理论上,应该执行以下步骤以避免多次向用户交付相同的购买:

  1. 该应用程序以购买验证数据为参数调用 Firebase Cloud Function。
  2. Cloud Function 调用相应的 Google 服务器或 Apple 服务器来检查购买是否有效。
  3. 云功能检查购买尚未交付给用户。
  4. 云功能将购买交付给用户。

为了实现第 3 步,我想到了使用 Firestore 集合来存储有关已交付的所有购买的信息。它应该是这样的结构:

Firestore root
  |
  |-> purchases (Collection)
        |
        |-> {purchaseID} (Document)
              |
              |-> sku: String (What was bought?)
              |-> uid: String (Who bought it?)

purchaseID 应该是订单 ID(用于在 Android 应用上进行的应用内购买)或交易 ID(用于在 iOS 应用上进行的应用内购买)。如果您不熟悉这些 ID:Google 的订单 ID 类似于GPA.1234-5678-9012-34567,而 Apple 的交易 ID 的格式类似于1234567890123456.

在测试过程中,我注意到订单 ID 似乎是随机的(即它们既不是单调递增也不是单调递减),但交易 ID 似乎是单调递增的。根据 Firestore 的最佳实践限制以及对另一个 Stackoverflow 问题的回答,使用单调递增的 ID 是一种反模式,因为这会在每秒写入超过 500 次时导致问题。虽然我预计每秒应用程序购买量不会达到 500 次(我当然怀疑是否有任何应用程序能够达到如此高的购买率),但我仍然希望避免反模式。

进一步详细说明为什么我需要存储这些单调递增的 ID

为了不向用户多次交付相同的应用产品,我需要跟踪已经交付的购买。让我尝试通过分享有关我们正在开发的应用程序的更多见解来解释这一点:该应用程序允许用户购买 5、10 或 20 张额外的支持票,从而允许他们提出相应数量的问题。为了实现这一点,每个用户的用户文档都有一个整数,表示用户可能会问多少个剩余问题。

假设使用 iOS 设备的用户购买了 10 张门票。该应用程序使用应用内购买的验证数据(基本上是一个非常长的 base64 编码字符串)来调用 Firebase 云函数来验证并将购买交付给该用户。Cloud Function 首先调用 Apple 服务器来检查验证数据是否有效。Apple 服务器使用包含交易 ID 23 的购买详细信息的 JSON 对象进行响应(在此示例中缩短了 ID 以提高可读性)。之后,Cloud Function 需要判断与事务 ID 23 相关的 10 张票是否已经送达。如果这 10 个工单尚未交付,我们会更新用户文档以反映该用户现在可以提出 10 个附加问题。此外,我们需要跟踪事务 ID 23 现在已交付。为了确保所有这些都以原子方式执行,上述检查和更新在一个事务中执行。

现在让我们假设同一用户稍后再购买 10 张票。同样,该应用程序将调用 Cloud Function 来验证和交付购买。这一次,Apple 服务器响应一个 JSON 对象,其中包含有关事务 ID 23(已交付的旧购买)和事务 ID 36(新购买)的详细信息。为了正确地将 10 张票(而不是 20 张)添加到用户计数器,我们需要检测交易 ID 为 23 的购买已经交付给用户,因此需要以某种方式存储交易 ID。

您现在可能想知道,如果只提供最新的购买(即具有最高交易 ID 的购买)是否是一个可行的解决方案。可悲的是,这可能会导致购买可能无法正确交付的一些问题。假设用户完全退出应用程序(即应用程序不再在后台运行),直接通过 App Store 购买 15 张门票(购买 10 和 5 张门票),然后再次打开我们的应用程序。该应用程序现在将注意到两次购买并调用 Cloud Function 两次。在两次通话期间,云功能将提供最新购买的 5 张门票。用户只收到了 10 张票,而不是额外收到 15 张票。

我想到的广义问题和可能的(非)解决方案

根据我的具体情况,出现了一个普遍的问题:应该如何存储从外部源生成的单调递增的 ID?

以下是我提出的一些想法和想法:

  1. 使用 Firestore 生成的 ID,将外部 ID 作为字段存储在文档中,并按照分片时间戳中的说明使用分片。虽然对于大多数用例来说这是一个很好的可扩展解决方案,但它可能并不适合所有人。以我的用例为例:为了避免多次向用户交付购买,步骤3(检查购买是否已经交付)和步骤4(交付购买)必须在一个事务中执行。但是,确定是否存在具有特定外部 ID 的文档需要 n 次查询(其中 n 是分片的数量),这在事务中是不可能的
  2. 置换外部 ID 的数字值并将置换后的 ID 用作文档 ID。例如,每个 1 可能排列为 8,每个 2 排列为 4,每个 3 排列为 7,依此类推。虽然这会导致相邻 ID 之间的距离稍微远一些,但这对防止热点没有帮助,因为相似的 ID 并没有分散得很远。此外,如果您需要在外部 ID 上订购(在我的用例中不需要),这不是一个选项。
  3. 使用反转的外部 ID 作为文档 ID。这将导致最低有效数字成为最高有效数字。虽然我确信这至少会有所帮助(通过使用十进制系统,这至少应该导致类似于使用 10 个分片的结果),但我不确定这是否会无限扩展。与我的第二个想法类似,如果您需要在外部 ID 上订购,这不是一个解决方案,但在我的用例中就足够了。
  4. 因为相似的外部 ID 会导致完全不同的文档 ID,所以可以使用完美(即无冲突)散列函数。但是,为特定用例找到完美的散列函数可能很困难。在外部 ID 达到一定长度之前,还可以使用加密散列函数,例如 SHA-256 或 SHA-512。这个想法让我觉得设计过度了。必须有一个更简单的解决方案。同样,如果您需要在外部 ID 上订购或者如果您需要从文档 ID 推断外部 ID,这将不是一个解决方案。
  5. 与使用散列函数的想法类似,也可以使用 AES 等加密算法来分散外部 ID。同样,如果您需要在外部 ID 上订购,这感觉过于工程化并且不是解决方案。

我敢肯定有人已经偶然发现了这个问题(特别是因为应用购买中有很多应用)。你是怎么解决这个问题的?我是否只是为了避免反模式而过度设计我的数据结构?还是有一些我完全想念的简单解决方案?

标签: firebasegoogle-cloud-firestore

解决方案


推荐阅读