我有一个Firestore数据库,它看起来像这样:
车辆收集
组织收集
用户集合
一个集合用于车辆,一个用于组织,一个面向用户。在组织集合中,每个文档都有一个名为vehicles的字段,其中包含该公司拥有的车辆集合中的车辆。用户集合中的每个文档都有一个名为vehicles的字段,其中包含该用户可以访问的所有车辆。在我的应用程序中,我可以删除整个组织(删除组织集合中的文档)。然后我有云函数来处理其余的。或者至少应该这样。
exports.deleteVehiclesInOrg = functions.firestore.document("/orgs/{orgId}").onDelete((snap) => {
const deletedOrgVehicles = snap.data().vehicles;
return deleteVehiclesInOrg(deletedOrgVehicles);
});
const deleteVehiclesInOrg = async(deletedVehicles: string[]) => {
for (const vehicle of deletedVehicles) {
await admin.firestore().doc(vehicles/${vehicle}).delete();
}
return null;
};
上述触发功能删除了该组织中的所有车辆,当车辆集合中的文件被删除时,这些车辆将触发以下功能:
const getIndexOfVehicleInUser = (vehicle: string,user: FirebaseFirestore.DocumentData) => {
for (let i = 0; i < user.vehicles.length; i++) {
if (user.vehicles[i].vehicleId === vehicle) {
return I;
}
}return null;
};
const deleteVehiclefromUsers = async (uids: [{ userId: string }],vehicleId: string) => {
for (const user of uids) {
const userSnap = await admin.firestore().doc(`users/${user.userId}`).get();
const userDoc = userSnap.data();
if (userDoc) {
const index = getIndexOfVehicleInUser(vehicleId,userDoc);
userDoc.vehicles.splice(index, 1);
await admin.firestore().doc(`users/${user.userId}`).update({ vehicles: userDoc.vehicles });
}
}
return null;
};
exports.deleteVehicleFromUsers = functions.firestore.document("/vehicles/{vehicleId}").onDelete((snap, context) => {
const deletedVehicleId = context.params.vehicleId;
const deletedVehicleUsers = snap.data().users;
return deleteVehiclefromUsers(deletedVehicleUsers, deletedVehicleId);});
deleteVehiclesInOrg函数应该触发,firebase函数总是删除组织文档中的所有车辆。这将触发deleteVehicleFromUsers功能,该功能将从用户文档中删除车辆。我的问题是,有时会,有时不会。大多数时候,如果我有大约10辆车,它只会删除大约6-8辆。但每次所有的车辆都被移走了。
当另一个函数(deleteVehiclesInOrg)删除了应该触发函数deleteVeicleFromUsers的文档时,是否有我没有正确处理的承诺,或者是否有可能依赖这样的后台触发函数?
欢迎使用StackOverflow@andreas!
这是我的赌注:(只是猜测…)
deleteVehiclefromUsers
:中的此行
await admin.firestore().doc(`users/${user.userId}`).update({ vehicles: userDoc.vehicles });
由不同的触发器同时执行,如果它们都使用同一个用户文档,则会覆盖彼此的vehicles
数组。请记住,触发器是异步的,因此它们可以同时执行,而无需等待其他触发器首先完成。
示例:vehicles = [A, B, C, D]
- 触发器1读取用户并删除
C
=>vehicles = [A, B, D]
- 触发器2读取用户并移除CCD_ 6=>
vehicles = [A, B, C]
- 触发器1写入用户并存储=>
vehicles = [A, B, D]
- 触发器2写入用户并存储=>
vehicles = [A, B, C]
最终的vehicles
是[A, B, C]
而不是[A, B]
。
证明事实确实如此:
在触发器的开始/结束处添加一些日志,以确保它们确实被触发,以及它们正在更新的用户文档id。如果你删除了10辆车,而你的触发器(至少)没有触发10次,那么你的问题就在其他地方
(是的,一个触发器偶尔会触发不止一次)。
如何解决:
使用firestore交易。通过这种方式,您将get()
作为用户文档,update()
作为原子文档,这意味着您将把vehicles
数组写入您读取的同一个数组(而不是写入已由另一个触发器写入的数组)。
这将是这样的:(未测试)
const userRef = admin.firestore().doc(`users/${user.userId}`);
await db.runTransaction(async (t) => {
const userSnap = await t.get(userRef);
if (userSnap.exists) {
const userDoc = userSnap.data();
const index = getIndexOfVehicleInUser(vehicleId,userDoc);
userDoc.vehicles.splice(index, 1);
t.update(userRef, { vehicles: userDoc.vehicles });
}
});
我个人建议仔细阅读有关交易的内容,这是一个需要牢记的重要概念。还要注意,在发生碰撞的情况下,事务可能会运行不止一次,如果在同一文档上运行许多事务,则可能会永久失败(例如,一次删除100辆属于同一用户的车辆)。
额外:
作为对您评论的回应,我所做的是估计大量可能要删除的文件,并用";睡眠;介于两者之间。我知道这听起来很糟糕,但它很有效,而且它给了触发器足够的时间让它们不会碰撞。
您只需要确保原始触发器(onDelete组织)有足够的时间来完成,并且用户上的事务足够分散,不会发生太多冲突。
";数字";取决于您的用例。举个例子:
- 你估计一个庞大的组织将有1000辆车,假设你的计算是基于2000辆车
- 函数的超时时间最多为9分钟。试着在…5分钟内把这一切都做好。您可以使用
runWith()
来调整超时。例如:functions.firestore.runWith({ memory: '1GB', timeoutSeconds: 540 }).document("/orgs/{orgId}").onDelete(...);
- 考虑最坏的情况:组织中的单个用户可以访问所有车辆
您可以删除批量为20辆的车辆,其间睡眠1秒:1000辆车/20辆车(按批量)=50次迭代x(1秒睡眠+~0.2秒消防)=>60秒,非常粗略。
我会改变这个功能:(再次,根本没有测试)
const deleteVehiclesInOrg = async(deletedVehicles: string[]) => {
const db = admin.firestore();
const bulkWriter = db.bulkWriter();
let i = 0;
for (const vehicle of deletedVehicles) {
const docRef = db.doc(vehicles/${vehicle});
bulkWriter.delete( docRef );
i++;
if ( i % 20 == 0 ) { // bulk size
bulkWriter.flush();
await new Promise(r => setTimeout(r, 1000)); // sleep for a sec
}
}
await bulkWriter.close(); // flush and wait the remaining are committed
return null;
};
更好的是,为了更多地分发它们,你可以使用10的批量大小并睡眠500毫秒。(或者批量5并睡眠250毫秒,你明白了…)
此外,您应该在users
中的事务周围有一个try-catch
,这样您至少可以在事务由于达到最大冲突而最终失败的情况下错误日志。
PS:注意bulkWriter
的使用,它比单独删除要高效得多。在上面的同一链接中找到这些信息(firestore事务和批量写入)