如何处理 NodeJS Express 请求竞争条件
How to handle NodeJS Express request race condition
假设我在快速服务器上有这个端点:
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 factor app. Specifically, rule 6 的规则,即您的应用程序应该是无状态进程。我们现在已将状态引入您的应用程序。
如果你确定你只会曾经运行这是一个单一的过程,你没问题.然而,当你通过部署多个节点(通过任何方法——PM2、Node 的 process.cluster、Docker、K8s 等)来横向扩展时,你就会遇到上述解决方案。节点服务器 1 不知道节点服务器 2 的本地状态,因此针对多节点应用程序的不同实例的多个请求无法共同管理处理映射的状态。
更可靠的解决方案是实施某种队列系统,可能会利用像 Redis 这样的独立基础设施。这样,您的所有节点都可以使用相同的 Redis 实例来共享状态,现在您可以扩展到应用程序的许多许多实例,并且所有这些实例都可以共享信息。
我真的没有关于如何构建它的所有细节,而且它似乎超出了这个问题的范围,但希望我已经给了你至少一个解决方案和一些想法在更广泛的层面上思考。
假设我在快速服务器上有这个端点:
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 factor app. Specifically, rule 6 的规则,即您的应用程序应该是无状态进程。我们现在已将状态引入您的应用程序。
如果你确定你只会曾经运行这是一个单一的过程,你没问题.然而,当你通过部署多个节点(通过任何方法——PM2、Node 的 process.cluster、Docker、K8s 等)来横向扩展时,你就会遇到上述解决方案。节点服务器 1 不知道节点服务器 2 的本地状态,因此针对多节点应用程序的不同实例的多个请求无法共同管理处理映射的状态。
更可靠的解决方案是实施某种队列系统,可能会利用像 Redis 这样的独立基础设施。这样,您的所有节点都可以使用相同的 Redis 实例来共享状态,现在您可以扩展到应用程序的许多许多实例,并且所有这些实例都可以共享信息。
我真的没有关于如何构建它的所有细节,而且它似乎超出了这个问题的范围,但希望我已经给了你至少一个解决方案和一些想法在更广泛的层面上思考。