使用 Firebase Firestore 实现幂等计数器

Posted

技术标签:

【中文标题】使用 Firebase Firestore 实现幂等计数器【英文标题】:Idempotent counter implementation with Firebase Firestore 【发布时间】:2018-03-29 05:10:16 【问题描述】:

来自文档:

这也可能导致对单个事件的多次调用,因此对于最高质量的函数,请确保函数被编写为幂等的。

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

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

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

【问题讨论】:

【参考方案1】:

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

序言

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

即使您通过读取每个文档来计数集合,计数仍然可能是陈旧的,因为在您计数时可能已插入或删除文档。

费用

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

幂等计数器

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

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

幂等计数器 - 事务 ID

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

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

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

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

输入健忘的布隆过滤器

幂等计数器 - 健忘的布隆过滤器

这种方法为您提供了更高的写入速率支持,以换取您认为以前看过文档 ID 的可能性很小。

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

幂等计数器 - 删除

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

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

我的建议是,如果您在插入之前看到删除,请提前减少计数器,因为知道它很快就会正确,如果您在删除之后看到插入,请执行增量。这是因为您只保留了这么多历史记录,因此您无法判断插入和删除是否有问题,或者删除是否在插入之后太远。

其他方法

根据集合大小、需要的准确度以及计数的使用频率,您可以定期调用云函数来计算计数并将其存储在文档中。您可以根据集合的大小动态扩展它以最小化延迟。对于非常小的收藏,经常这样做,对于较大的收藏,就更不频繁地这样做。

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

【讨论】:

很棒的答案,丹。我可能会使用 txid 方法,因为我的应用程序有宽松的速度要求,但有 100% 的准确度要求,并且不时应用该功能来重新计算文档。我不介意计数器是否落后于实际计数,只要 100% 正确。【参考方案2】:

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

正如您所说,这并不是非常有效(就速度或成本而言)。

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

然后当一个文档进来时,你在一个事务中做如下操作:

    阅读文档。如果counted == true,退出 增加计数。 将 counted 标记为 true。

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

【讨论】:

以上是关于使用 Firebase Firestore 实现幂等计数器的主要内容,如果未能解决你的问题,请参考以下文章

使用Firestore需要firebase-firestore.js文件吗?

两个方向无限滚动与Firebase / Firestore后端

无法在firebase.firestore.CollectionReference中使用Array firebase.firestore.Query为什么?

使用 Flutter 和 Firebase 实现实时在线/离线状态

@firebase/firestore:Firestore (8.6.5):无法访问 Cloud Firestore 后端

仅当 Firebase Firestore 中不存在文档时才创建文档