使用 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
布尔值。
然后当文档进来时,您在事务中执行以下操作:
- 阅读文档。如果
counted == true
,退出
- 增加计数。
- 标记
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 来计算计数并将其存储在文档中。您可以根据集合的大小对其进行动态缩放,以最大程度地减少延迟。对于非常小的集合,经常这样做,对于更大的集合,很少这样做。
如果您有一种机制来确定您已经计算过的文档(因此您只需要计算新文档),您也可以在此处应用成本优化。如果删除不频繁,您可以添加一个事件来减少删除计数器。
来自文档:
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
布尔值。
然后当文档进来时,您在事务中执行以下操作:
- 阅读文档。如果
counted == true
,退出 - 增加计数。
- 标记
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 来计算计数并将其存储在文档中。您可以根据集合的大小对其进行动态缩放,以最大程度地减少延迟。对于非常小的集合,经常这样做,对于更大的集合,很少这样做。
如果您有一种机制来确定您已经计算过的文档(因此您只需要计算新文档),您也可以在此处应用成本优化。如果删除不频繁,您可以添加一个事件来减少删除计数器。