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 而不是模拟器完成的。
根据 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 而不是模拟器完成的。