如何处理NodeJS Express请求竞争条件



假设我在express服务器上有这个端点:

app.get('/', async (req, res) => {
var foo = await databaseGetFoo();
if (foo == true) {
foo = false;
somethingThatShouldOnlyBeDoneOnce();
await databaseSetFoo(foo);
}
})

我认为这创建了一个竞争条件,如果端点被同时调用两次?如果是这样,我该如何防止这种竞态条件的发生?

好的,根据这些评论,我对你想要什么有了更好的理解。

假设somethingThatShouldOnlyBeDoneOnce正在做一些异步的事情(比如写数据库),你是正确的,一个用户(或多个用户)多次调用该端点可能会导致该操作重复发生。

使用您关于允许每个用户单独评论的评论,并假设您在中间件堆栈中有可以通过会话或其他方式唯一标识用户的早期中间件,您可以天真地实现这样的东西,这应该使您远离麻烦(通常披露这是未经测试的,等等):

let processingMap = {};
app.get('/', async (req, res, next) => {
if (!processingMap[req.user.userId]) {
// add the user to the processing map
processingMap = {
...processingMap,
[req.user.userId]: true
};
const hasUserAlreadySubmittedComment = await queryDBForCommentByUser(req.user.userId);
if (!hasUserAlreadySubmittedComment) {
// we now know we're the only comment in process
// and the user hasn't previously submitted a comment,
// so submit it now:
await writeCommentToDB();
delete processingMap[req.user.userId];
res.send('Nice, comment submitted');
} else {
delete processingMap[req.user.userId];
const err = new Error('Sorry, only one comment per user');
err.statusCode = 400;
next(err)
}
} else {
delete processingMap[req.user.userId];
const err = new Error('Request already in process for this user');
err.statusCode = 400;
next(err);
}
})

由于插入到processingMap中的操作都是同步的,而Node一次只能做一件事,因此用户命中该路由处理程序的第一个请求实际上将锁定该用户,直到我们处理完请求后将锁移除。

但是…这是一个幼稚的解决方案,它违反了12因素应用程序的规则。特别是规则6,即你的应用程序应该是无状态的进程。现在我们已经将状态引入到应用程序中。

如果您确定您将只永远将此作为单个进程运行,则没问题。然而,当您通过部署多个节点(通过任何方法——PM2, Node的进程)进行水平扩展时。集群、Docker、k8等),那么您就可以使用上述解决方案了。Node Server 1不知道Node Server 2的本地状态,因此多个请求击中多节点应用程序的不同实例不能共同管理处理映射的状态。

更强大的解决方案是实现某种队列系统,可能利用像Redis这样的单独基础设施。这样你所有的节点都可以使用相同的Redis实例来共享状态,现在你可以扩展到很多很多的应用程序实例,它们都可以共享信息。

我真的没有关于如何构建它的所有细节,而且它似乎超出了这个问题的范围,但希望我至少给了你一个解决方案,以及在更广泛的层面上思考的一些想法。