使用 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 后端