Firebase 云功能:Firestore 事务不会在并发编辑时重新运行

Firebase Cloud Function: Firestore transaction doesn't rerun on concurrent edit

根据 Firebase 的 docs on Firestore transactions:

In the case of a concurrent edit, Cloud Firestore runs the entire transaction again.

当 运行使用管理 SDK 在 Firebase Cloud Functions 中执行事务时也是如此吗?根据我的测试,它似乎没有这样做。

我用这个虚构的例子测试了这个。如果所有 cars 都已删除,我将删除 carsSummary/index 文档。为确保不存在竞争条件,我将其包装在事务中。

  try {
    await db.runTransaction(async transaction => {
      const results = await transaction.get(db.collection(`cars`));
      
      console.log('Running transaction');
      await sleep(10000); // during these 10 seconds, a new car gets added

      if (results.size === 0)
        transaction.delete(db.doc(`carsSummary/index`));
    });
  } catch (error) {
    console.error(error);
  }

通过上面的测试,如果在 sleep(10000) 期间添加 car,则删除操作不会正确执行,从而使 results 查询无效。但是,交易不会重新 运行(即 Running transaction console.log 只被调用一次)。这是正确的行为吗? Firebase 文档有误吗?

答案就在此处的 Firestore 文档中:https://googleapis.dev/nodejs/firestore/latest/Transaction.html#get

get(refOrQuery) → {Promise} Retrieve a document or a query result from the database. Holds a pessimistic lock on all returned documents.

数据库对返回的文档持有悲观锁,因此它不会尝试在并发编辑时重新运行事务。

请注意,如果您使用模拟器(至少在 Firebase CLI v9.16.0 中),行为会有所不同,即使使用 Admin SDK,事务也会在并发编辑时重新运行:https://github.com/firebase/firebase-tools/issues/3928

为了详细说明我的另一个答案,我做了进一步的测试并观察到一个有趣的行为。事实证明,当尝试 set 文档时,Admin SDK 事务会在并发编辑时重新 运行。举个例子:

const writeOnce = async path => {
  await admin.firestore().runTransaction(async transaction => {
    const ref = admin.firestore().doc(path);

    const snap = await transaction.get(ref);
    if (snap.exists) return;

    await sleep(10000);

    await transaction.set(ref, {
      createdAt: admin.firestore.FieldValue.serverTimestamp(),
    });
  });
};

如果 2 个进程同时执行该函数,第一个将成功 set 文档。第二个交易将失效,因此 set 命令将静默失败。然后它重新运行s 事务,当它这样做时,它会找到第一个进程写入的文档并提前退出。

另一个有趣的实验是在第一个进程中执行上面的函数,然后在第二个进程中并发执行这个函数(没有事务):

const writeOnceNoTransaction = async path => {
  const ref = admin.firestore().doc(path);

  const snap = await ref.get();
  if (snap.exists) return;

  await ref.set({
    createdAt: admin.firestore.FieldValue.serverTimestamp(),
  });
};

第一个进程中的事务对文档采用悲观写锁定,因此第二个进程将在提交 set 之前暂停。一旦第一个进程中的事务完成,第二个进程将继续并覆盖第一个进程中的更改。

这些测试是使用云中的 Firestore 而不是模拟器完成的。