[英]How does firestore's optimistic locking in transactions handle concurrent updates that cause a violation of a security rule?
Firestore 文档似乎没有指定何时或如何在事务中评估安全规则,以及它如何与重试和乐观锁定交互。
我的用例很简单,我的文档上有一个lastUpdatedAt
字段,我正在使用事务来确保我获取最新文档并检查lastUpdatedAt
字段,以便我可以在发布更新之前解决任何冲突。
在伪代码中,模式是这样的
async function saveDocument(data: MyType, docRef: DocumentReference)
await firebase.firestore().runTransaction(async (transaction) => {
const latestDocRef = await transaction.get(docRef)
const latestDoc = latestDocRef.data()
if(latestDoc.lastUpdatedAt > data.lastUpdatedAt){
data = resolveConflicts(data, latestDoc)
data.lastUpdatedAt = latestDoc.lastUpdatedAt
}
await transaction.update(docRef, data)
}
}
然后在安全规则中,我检查以确保只允许更新最新或更新的 lastUpdatedAt
function hasNoLastUpdatedAtConflict(){
return (!("lastUpdatedAt" in resource.data) ||
resource.data.lastUpdatedAt == null ||
request.resource.data.lastUpdatedAt >= resource.data.lastUpdatedAt);
}
//in individualRules
allow update: if hasNoLastUpdatedAtConflict() && someOtherConditionsEtc();
文档说
在移动/Web SDK 中,事务会跟踪您在事务中阅读的所有文档。 仅当这些文档在事务执行期间没有更改时,事务才完成其写操作。 如果任何文档确实发生了变化,事务处理程序将重试事务。 如果事务重试几次后仍不能得到干净的结果,则事务因数据争用而失败。
但是他们没有指定该行为如何与安全规则交互。 我上面的交易有时因违反安全规则而失败。 它只在实时 Firestore 环境中失败,我无法在模拟器中使其失败。 我怀疑发生的事情是:
我想我可以在交易因违反安全规则而被拒绝的情况下实现客户端重试,但如果确实发生了这种情况,那将是非常令人惊讶的行为。
是否有人了解安全规则的实际行为和乐观锁定的 Firestore 事务?
如果在您调用transaction.get(docRef)
和提交事务之间发生写入,事务将被重试并且您的处理程序将再次执行(最多重试次数)。
经过全面测试后,我确认使用来自 iOS/android 库的乐观锁定的 firestore 事务可能会因在事务中读取文档后发生并发写入而被拒绝并出现权限拒绝错误。
发生以下情况:
{id: 1, lastUpdatedAt: 1, data: "foo"}
{id: 1, lastUpdatedAt: 2, data: "foo"}
.update({lastUpdatedAt: 1, data: "bar"})
firestore/permission-denied
因为此更新违反了request.resource.data.lastUpdatedAt >= resource.data.lastUpdatedAt
的安全规则事务不会像文档建议的那样重试,即使它执行了对陈旧数据的读取。 这意味着如果并发写入可能导致事务违反安全规则,则 Firestore 库的用户不能依赖正在重试的事务。
这是令人惊讶且未记录的行为!
根据文档和代码示例,您可以使用getAfter()安全规则 function 确保相关文档始终以原子方式更新,并且始终作为事务或批量写入的一部分。它可用于访问和验证文档的 state在一组操作完成之后但在 Cloud Firestore 提交操作之前。 与 get() 一样,getAfter() function 采用完全指定的文档路径。 您可以使用 getAfter() 来定义必须作为事务或批处理一起发生的写入集。
这些是一些访问呼叫限制,您可能想看看。
文档建议 getAfter 对于在记录整个事务的 state 之后(在内存中的一种“暂存”环境中)但在事务实际更改数据库之前检查数据库的内容很有用,每个人都可以看到。 这与 get() 不同,因为 get() 仅在事务最终提交之前查看数据库的实际内容。 简而言之,getAfter() 使用整个事务或批处理的整个阶段写入,而 get 使用数据库实际存在的内容。
getAfter() 在您需要检查可能已在事务或批次中更改的其他文档时很有用,并且仍然有机会通过失败规则拒绝整个事务或批次。 因此,例如,如果在单个事务中写入的两个文档必须有一些共同的字段值才能保持一致,则需要使用 getAfter() 来验证两者之间的相等性。
注意点:写入的安全规则在数据库中的任何内容被写入更改之前生效。 这就是安全规则能够安全有效地拒绝访问的方式,而不必回滚已经发生的任何写入。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.