使用 Firebase Firestore 实现幂等计数器

Idempotent counter implementation with Firebase Firestore

来自文档:

This may also result in multple invocations for a single event, so for the highest quality functions ensure that the functions are written to be idempotent.

因此,如果 Firestore 没有提供一种方法来计算集合中子文档的数量,我需要创建一个云函数来在节点上聚合此信息,比如 /counters/{type}/count

如果我执行写入触发器并增加值,我的计数器可能不会反映实际的文档计数,对吗?

我如何编写一个函数来完美地计算集合中的文档(又不会太昂贵 - 假设我不想在每次写入时读取整个集合)?

目前,由于缺乏围绕 Cloud Firestore + Cloud Functions 集成的保证,要 100% 确保计数准确的唯一方法是每次写入计数时读取整个集合。

正如您所说,这不是非常有效(在速度或成本方面)。

如果您想在每次写入时保持计数而不重复读取整个集合,请考虑向每个文档添加一个 counted 布尔值。

然后当文档进来时,您在事务中执行以下操作:

  1. 阅读文档。如果counted == true,退出
  2. 增加计数。
  3. 标记 counted 为真。

有关 Cloud Firestore 中事务的更多信息,请参阅文档: https://firebase.google.com/docs/firestore/manage-data/transactions

这个问题的答案将取决于您如何使用该集合的不同方面,以及“完美计数”对您意味着什么。

前言

首先,由于 Cloud Functions 调用与写入异步,这将导致计数器略微落后于集合的真实计数。我假设这没问题。

即使您通过阅读每个文档来计算集合,计数仍然可能过时,因为在您计算时可能已插入或删除了文档。

费用

你提到“不会太贵”。在这里,我们需要了解您读取计数的频率与添加或删除文档的频率。要维护一个单独的计数器,您将 reading/writing 它用于每个文档计数更改。由于写入的成本是读取的 3 倍,这意味着您需要对每个文档进行 4 次或更多次计数才能收回保持计数的成本。这里有一个公式考虑了文档生命周期内的平均计数,但我将把它留作 reader.

的练习。

幂等计数器

这是一个有趣的问题,也是分布式系统熟悉的问题。如果客户端请求添加 +1 计数器,并且请求超时(服务器从不响应)——再次请求是否安全?如果服务器确实应用了增量但随后遇到网络问题怎么办?如果没有呢?

下面我将回答一些处理这种情况的方法。

幂等计数器 - 事务 ID

处理此问题的一种方法是在增量请求中发送一个唯一的交易 ID (txid)。如果服务器之前已经处理过 txid,它知道这是一个重复的请求并且可以响应它已经完成了。

在您的用例中,如果您从不删除文档,则可以使用文档 ID 作为 txid。在计数器中,当您 +1 时,将文档 ID 添加到已处理增量的数组中。在执行此操作之前,请检查它是否已存在于数组中(表明它已被处理)。

上面的一个明显问题是数组将继续增长,最终变得太大。所以,我们要限制我们跟踪旧 ID 的时间。您可以使用时间戳并删除早于 'X' 的所有内容,或者您​​可以简单地将数组视为循环缓冲区以使其保持固定的最大大小。

这两种方法都适用于较慢的写入速率,但不足以提高写入速度。例如,在 1000 writes/second,这将是 5000 个文档 ID 仅用于覆盖 5 秒(我们在我们的限制文档中提到一个函数可能需要超过 5 秒才能执行)。

输入健忘的布隆过滤器

幂等计数器 - Forgetful Bloom Filters

此方法为您提供更高的写入速率支持,以换取您认为您以前见过文档 ID 的可能性非常小。

我不会在这里详细介绍实现,但是在这个博客中有一个很好的概述:Counters, Idempotence And Forgetful Bloom Filters

幂等计数器 - 删除

另一个复杂的问题是处理删除。如果您使用唯一 ID 并且确定它不会被重复使用(例如我们的原生自动 ID 支持),那么添加它并不难。只需在单独的 list/field 中重复您对添加所做的操作,并确保检查两个列表。

需要考虑的一件小事是 Cloud Functions 没有保证执行顺序。这意味着如果它们发生得足够近,您可能会在插入之前看到删除。

我的建议是,如果您在插入之前看到删除,请提前递减计数器,因为它很快就会被调整,如果您在删除之后看到插入,请进行递增。这是因为你只保留了这么多的历史记录,所以你无法判断插入和删除是否顺序错误,或者删除是否在插入之后太远了。

其他方法

根据集合大小、需要的准确度以及计数的使用频率,您可以定期调用 Cloud Functions 来计算计数并将其存储在文档中。您可以根据集合的大小对其进行动态缩放,以最大程度地减少延迟。对于非常小的集合,经常这样做,对于更大的集合,很少这样做。

如果您有一种机制来确定您已经计算过的文档(因此您只需要计算新文档),您也可以在此处应用成本优化。如果删除不频繁,您可以添加一个事件来减少删除计数器。