首先,让我们修复您的 releaseAmountFromStack 函数,这样您就不会使用 Promise 构造函数来返回 Promise 的 API(称为 Explicit Promise Construction Antipattern)。如果您的stackRef 查询或releaseItem 函数抛出错误,您的代码将遇到UnhandledPromiseRejection,因为Promise 链都没有catch 处理程序。
function releaseAmountFromStack(amount) {
const db = admin.firestore();
const stackRef = db.collection("stack");
return stackRef
.orderBy("expirationTime", "asc")
.limit(1)
.get()
.then((querySnapshot) => {
if(querySnapshot.empty) {
return Promise.reject("Out of stock");
}
const itemToRelease = querySnapshot.docs[0];
return releaseItem(itemToRelease.ref, amount);
})
.then((releasedAmount) => {
const amountLeft = amount - releasedAmount;
if (amountLeft <= 0)
return releasedAmount;
return releaseAmountFromStack(amountLeft)
.then((nextReleasedAmount) => nextReleasedAmount + releasedAmount)
.catch((err) => {
if (err === "Out of stock") {
return releasedAmount;
} else {
// rethrow unexpected errors
throw err;
}
});
});
}
由于该函数涉及嵌套的 Promise 链,因此切换到 async/await 语法可以将其扁平化为:
async function releaseAmountFromStack(amount) {
const db = admin.firestore();
const stackRef = db.collection("stack");
const querySnapshot = await stackRef
.orderBy("expirationTime", "asc")
.limit(1)
.get();
if (querySnapshot.empty)
return 0; // out of stock
const itemToRelease = querySnapshot.docs[0];
const releasedAmount = await releaseItem(itemToRelease.ref, amount);
const amountLeft = amount - releasedAmount;
if (amountLeft <= 0) {
// nothing left to release, return released amount
return releasedAmount;
}
// If here, there is more to release, trigger the next recursion
const nextReleasedAmount = await releaseAmountFromStack(amountLeft);
return nextReleasedAmount + releasedAmount;
}
注意:在上面的扁平化版本中,我们不需要处理 catch,因为我们可以直接返回 0。这意味着任何不相关的错误都会正常抛出。
接下来我们可以转到releaseItem,这是您问题的真正原因。在这里,您没有处理另一个实例正在删除您正在阅读的项目的情况,您只是假设它存在,然后最终处理NaN 和/或负余额。作为最终发生的一个示例,您可以得到以下事件序列(它比这更复杂,因为您使用的是FieldValue.increment() - 添加一个服务器客户端也执行事务):
Client A Server Client B
Release 50 Release 80
┌┐ Get Doc #1 ┌┐ ┌┐
││ ────────────────► ││ ││
││ ││ ││
││ ││ ││
││ Doc #1 Snapshot ││ Get Doc #1 ││
││ { amount: 10 } ││ ◄──────────────── ││
││ ◄──────────────── ││ ││
││ ││ ││
││ ││ Doc #1 Snapshot ││
││ ││ { amount: 10 } ││
││ Result: Delete Doc #1 ││ ────────────────► ││
││ ────────────────► ││ ││
││ ACCEPTED ││ ││
││ ◄──────────────── ││ ││
││ ││ Result: Delete Doc #1 ││
││ Get Doc #2 ││ ◄──────────────── ││
││ ────────────────► ││ ││
││ ││ REJECTED ││
││ ││ New Doc #1 Snapshot ││
││ ││ <null> ││
││ Doc #2 Snapshot ││ ────────────────► ││
││ { amount: 15 } ││ ││
││ ◄──────────────── ││ ││
││ ││ ││
││ ││ Result: Set Doc #1 to -10 ││
││ ││ ◄──────────────── ││
││ Result: Delete Doc #2 ││ ▲ ││
││ ────────────────► ││ │ ││
││ ACCEPTED ││ ERROR ││
││ ◄──────────────── ││ ││
└┘ └┘ └┘
因为您正在使用交易,您已经知道amount 的当前值,因此您可以在客户端上进行计算并写入新金额,而不是使用FieldValue.increment()。该运算符更适合对不需要知道当前值的值进行简单更新。
function releaseItem(itemRef, amountToRelease) {
const db = admin.firestore();
return db.runTransaction((transaction) => {
return transaction.get(itemRef).then((itemDoc) => {
if (!itemDoc.exists) {
// target has been deleted, do nothing & return
// amount that was released (0)
return 0;
}
const itemAmount = itemDoc.get("amount");
const actualReleaseAmount = Math.min(amountToRelease, itemAmount);
if (actualReleaseAmount >= itemAmount) {
// exhausted supply. delete item
transaction.delete(itemDoc.ref);
} else {
// have leftover supply. update amount
transaction.set(itemDoc.ref, {
amount: itemAmount - actualReleaseAmount,
}, {merge: true});
}
// return amount that was released
return actualReleaseAmount;
});
});
}
这不会完全解决您的问题,因为如果两个或多个客户端尝试同时从同一个堆栈中取出项目,您仍然会遇到数据争用问题。作为这方面的一个示例,请参阅以下事件流程(再次,时间将成为事情如何发展的主要因素):
Client A Server Client B
Release 50 Release 80
┌┐ Get Doc #1 ┌┐ ┌┐
││ ────────────────► ││ ││
││ ││ ││
││ ││ ││
││ Doc #1 Snapshot ││ Get Doc #1 ││
││ { amount: 10 } ││ ◄──────────────── ││
││ ◄──────────────── ││ ││
││ ││ ││
││ ││ Doc #1 Snapshot ││
││ ││ { amount: 10 } ││
││ Result: Delete Doc #1 ││ ────────────────► ││
││ ────────────────► ││ ││
││ ACCEPTED ││ ││
││ ◄──────────────── ││ ││
││ ││ Result: Delete Doc #1 ││
││ Get Doc #2 ││ ◄──────────────── ││
││ ────────────────► ││ ││
││ ││ REJECTED ││
││ ││ New Doc #1 Snapshot ││
││ ││ <null> ││
││ Doc #2 Snapshot ││ ────────────────► ││
││ { amount: 15 } ││ ││
││ ◄──────────────── ││ Result: Cancelled ││
││ ││ ◄──────────────── ││
││ ││ ││
││ ││ ││
││ Result: Delete Doc #2 ││ Get Doc #2 ││
││ ────────────────► ││ ◄──────────────── ││
││ ACCEPTED ││ ││
││ ◄──────────────── ││ ││
││ ││ Doc #2 Snapshot ││
││ Get Doc #3 ││ <null> ││
││ ────────────────► ││ ────────────────► ││
││ ││ ││
││ ││ Result: Cancelled ││
││ ││ ◄──────────────── ││
││ Doc #3 Snapshot ││ ││
││ { amount: 10 } ││ ││
││ ◄──────────────── ││ Get Doc #3 ││
││ ││ ◄──────────────── ││
││ ││ ││
││ ││ ││
││ Result: Delete Doc #3 ││ Doc #3 Snapshot ││
││ ────────────────► ││ <null> ││
││ ACCEPTED ││ ────────────────► ││
││ ◄──────────────── ││ ││
││ ││ Result: Cancelled ││
││ Get Doc #4 ││ ◄──────────────── ││
└┘ ────────────────► └┘ └┘
解决此问题的一种方法是一次拉下多个项目,适当地使用它们并将更改写回,所有这些都在一个事务中。再说一次,不处理问题,只是减少客户一遍又一遍地争夺相同文件的可能性。
async function releaseAmountFromStack(amount) {
const db = admin.firestore();
const stackQuery = db
.collection("stack")
.orderBy("expirationTime", "asc")
.limit(1);
const releasedAmount = await _releaseAmountFromStack(stackQuery, amount);
const amountLeft = amount - releasedAmount;
if (amountLeft <= 0) {
// nothing left to release, return released amount
return releasedAmount;
}
// If here, there is more to release, trigger the next recursion
const nextReleasedAmount = await releaseAmountFromStack(amountLeft)
.catch((err) => {
if (err !== "Empty stack") throw err;
return 0;
});
return nextReleasedAmount + releasedAmount;
}
function _releaseAmountFromStack(query, amountToRelease) => {
return db.runTransaction((transaction) => {
return transaction.get(query).then((querySnapshot) => {
if (!querySnapshot.empty) {
// nothing in stack, return released amount (0)
return Promise.reject("Empty stack");
}
let remainingAmountToRelease = amountToRelease;
for (const doc of querySnapshot.docs) {
const itemAmount = doc.get("amount");
const amountChange = Math.min(itemAmount, remainingAmountToRelease);
if (amountChange >= itemAmount) {
transaction.delete(doc.ref);
} else {
remainingAmountToRelease -= amountChange;
transaction.set(doc.ref, {
amount: itemAmount - amountChange
}, { merge: true });
}
if (remainingAmountToRelease <= 0) break; // stop iterating early
}
// return amount that was released
return /* totalAmountReleased = */ amountToRelease - remainingAmountToRelease;
});
});
}