正确更新事务中的多个文档的方法

Ada*_*rsh 7 transactions node.js firebase google-cloud-firestore

TL; DR我应该使用Transaction.getAll()还是使用for循环并使用来逐个更新文档Transaction.get()

考虑以下架构:

- someTopLevelCollection
   - accountId1
       - projects
       - tasks
       - users
  - accountId2
       - projects
       - tasks
       - users
Run Code Online (Sandbox Code Playgroud)

每当在项目,任务子集合中创建项目/任务时,用户集合中的计数器都会更新,例如projectsCount和taskCount。

对用户的引用作为userId的数组保存在项目和任务中,如下所示:

注意:为简洁起见,删除了其他字段

项目/任务结构:

{
   "name": "someName",
   "status": "someStatus",
   "users": [
       "userId1",
       "userId2",
       ....
   ],
   ...
}
Run Code Online (Sandbox Code Playgroud)

现在,我需要使用事务为用户数组内的所有userId更新一个计数器。

方法1:

const accountId = context.params.accountId;
const userIds = snapshot.data().users;

userIds.forEach(userId => {

    const userDocRef = db.collection(someTopLevelCollection)
        .doc(accountId)
        .collection('users')
        .doc(userId);

    let transaction = db.runTransaction(transaction => {
        return transaction.get(userDocRef)
            .then(doc => {
                const snapshotData = doc.data();
                let newCounterValue = snapshotData[counterName] + 1;
                transaction.update(userDocRef, {counterName: newCounterValue});
                return Promise.resolve(`Incremented ${counterName} to ${newCounterValue}`);
            });

    }).then(result => {
        console.log('Transaction success!', result);
        return true;

    }).catch(err => {
        console.error('Transaction failure:', err);
        return false;
    });

});
Run Code Online (Sandbox Code Playgroud)

方法2:

const accountId = context.params.accountId;
const userIds = snapshot.data().users;

let userDocRefs = [];
userIds.forEach(userId => {
    const userDocRef = db.collection(someTopLevelCollection)
        .doc(accountId)
        .collection('users')
        .doc(userId);
    userDocRefs.push(userDocRef)
});

let transaction = db.runTransaction(transaction => {
    return transaction.getAll(userDocRefs)
        .then(docs => {

            docs.forEach(doc => {
                const snapshotData = doc.data();
                let newCounterValue = snapshotData[counterName] + 1;
                transaction.update(doc.ref, {counterName: newCounterValue});
            });
            return Promise.resolve('Completed transaction successfully');

        });

}).then(result => {
    console.log('Transaction success!', result);
    return true;

}).catch(err => {
    console.error('Transaction failure:', err);
    return false;

});
Run Code Online (Sandbox Code Playgroud)

以下是我的问题:

  1. getAll()在执行过程中外部发生文档更改时,是否再次获取所有文档以保持一致性。如果是这样,使用getAll()的用例是什么?
  2. 当使用for循环一个事务运行一个事务并且在该事务中正在修改的当前文档外部发生文档更改时,该事务是否仅针对该文档重试

谢谢。

小智 0

首先是简短的答案,然后是解释:

\n
    \n
  1. 是的,当您的 ref 中的引用发生外部更改时getAll(),整个事务将被重试(即所有文档将被重新读取)。下面有更多细微差别。
  2. \n
  3. 不,不会仅针对该一份文档重试事务。交易是全有或全无的。它要么全部成功,要么如果失败则一切都将重新运行(包括所有读取和所有写入)。但考虑到您提供的上下文,答案就更微妙了;下文将详细介绍这一点。
  4. \n
\n

有什么getAll

\n

第一个问题的更深层次答案很微妙,因为 Firestore在 mobile/web 和 server 之间的锁定方式不同

\n
    \n
  • 由于移动/网络的延迟较长,事务乐观地锁定文档引用。因此,如果您来自移动/网络,当您正在进行事务时,写入确实可能发生在您的文档之一上。在这些情况下,整个事务将被重试(即您传递给的整个函数db.runTransaction将再次运行 \xe2\x80\x94\xc2\xa0 也就是说,您将重新拉取所有文档的最新版本,然后尝试给他们每个人写信)。
  • \n
  • 但服务器端 Firestore 调用(例如使用管理界面)会悲观地锁定,因为它假定服务器具有低延迟和良好的连接。当涉及数据争用时,您getAll()对服务器的调用会阻止所有其他写入这些文档的尝试,直到您的事务完成。换句话说,在服务器端 Firestore 事务中不可能“在外部发生文档更改”。您肯定会成功阅读文档,并且在您进行交易时它们不会从外部更改。
  • \n
\n

然后你提到“为什么getAll”?原因是您希望通过批量获取来保证原子性。Firestore 事务要求在任何写入之前完成所有读取。当您需要自动阅读和编辑大量文档时,您可能会想要getAll

\n

因此,在您的示例“方法#1”中,您正在编辑的全套文档不能保证被原子更改。方法 #1 仅在每个文档级别 \xe2\x80\x94 提供原子性,这意味着如果这些更改在某种程度上需要彼此一致,则方法 #1 不是正确的处理方法。鉴于您对用户记录如何与包含所有 userId 的根文档相互关联的描述,我怀疑您会想要使用方法 #2。

\n

请注意,FWIW,Firestore 现在支持原子增量,可以实现方法 #1 想要的功能。原子增量保证在不需要事务的情况下,一个数字将以原子方式写入一个递增的值(即在您读取和写入该数字之间没有其他人写入该数字)。但是,如果您的更改需要跨越一堆文档以便彼此保持一致,这些原子增量仍然只能在每个文档(好吧,每个数字)级别 \xe2\x80\x94\xc2\xa0so 上得到保证,您仍然想使用方法#2。

\n

交易重试

\n

假设您在移动设备或网络上,因此您的事务仅对所有文档引用持有乐观锁。在这种情况下,当您正在进行事务时,另一个线程可能会写入您的文档之一。

\n

由于事务是全有或全无的,因此当有人写入您在事务中读取的单个文档时,整个事务都会重新运行。您传递给的整个函数runTransaction将再次重新尝试;一切都被重新阅读,一切都被重新更新。(不要问我在失败的事务重新尝试时是否会再次收取读/写费用......我不知道)。Firestore 客户端将自动重新尝试您的事务五次,之后放弃并引发异常。

\n

重新尝试整个事务,而不仅仅是更改一个文档是必要的,因为事务锁定的整个前提是保证一组相互依赖的更改要么发生在所有文档上,要么不会发生在任何文档上。因此,仅重新尝试单个文档读/写不会是您想要的(因为您选择使用事务大概是因为文档更改是相互关联的)。

\n