当事件循环等待数据库操作时,如何处理对 nodejs 服务器的传入请求
Posted
技术标签:
【中文标题】当事件循环等待数据库操作时,如何处理对 nodejs 服务器的传入请求【英文标题】:How does an incoming request to a nodejs server get handled when the event loop is waiting for a DB operation 【发布时间】:2019-10-14 02:52:06 【问题描述】:我的 API 中有一个路由,例如,我们将其命名为 /users/:userId/updateBalance。此路由将获取用户当前的余额,添加来自请求的任何内容,然后使用新计算的余额更新余额。像这样的请求每 30 分钟针对特定用户进入服务器,所以直到最近,我认为并发问题是不可能的。
最终发生的事情是某处发送的请求失败,仅在 30 分钟后再次发送,大约在另一个请求的一秒内。结果是,正如我在数据库中看到的那样,这两个请求都从数据库中获取了相同的余额,并且都添加了各自的金额。本质上,第二个请求实际上读取了一个过期的余额,通常它应该在请求 1 执行后执行。
为了更清楚地给出一个数字示例,假设请求 1 是在余额中添加 2 美元,请求 2 是添加 5 美元,而用户的余额是 10 美元。如果请求并行执行,则用户余额将以 12 美元或 15 美元结束,具体取决于请求 1 或请求 2 分别先完成,因为这两个请求都从数据库中获取 10 美元的余额。但是,显然我们希望请求 1 执行,将用户余额更新为 12 美元,然后请求 2 执行并将余额从 12 美元更新为 17 美元。
为了更好地了解这个过程的整体执行情况:收到请求,调用一个函数,该函数必须等待来自 DB 的余额,然后该函数计算新的余额并更新 db,之后执行完成。
所以我对此有几个问题。首先,节点在等待异步请求(如 mysql 数据库读取)时如何处理传入请求。鉴于我观察到的结果,我假设当第一个请求正在等待数据库时,第二个请求可以开始处理?否则,我不确定在节点等单线程环境中如何体验这种异步行为。
其次,我该如何控制和预防它。我曾想使用带有 forUpdate 锁的 MySQL 事务,但由于当前编写代码的方式,这似乎是不可能的。有没有办法告诉节点某个代码块不能“并行”执行?还是有其他选择?
【问题讨论】:
已经回复,但更多关于事件循环的信息here 【参考方案1】:你是对的,当节点等待数据库查询返回时,它会处理所有传入的请求并在第一个请求完成之前启动该请求的数据库调用。
防止这种 IMO 的最简单方法是使用队列。该路由处理程序不是直接在路由处理程序中处理余额更新,而是可以将事件推送到队列(在 Redis、AWS SQS、RabbitMQ 等中)以及您应用程序中的其他位置(甚至在完全不同的服务中)您会有一个消费者来监听该队列中的新事件。如果更新失败,请将其添加回队列的开头,添加一些等待时间,然后重试。
这样,无论您的第一个请求失败多少次,您的余额都是正确的,并且该余额的待处理更改将按照正确的顺序进行。如果队列中的事件反复失败,您甚至可以向某人发送电子邮件或通知以查看它,当问题得到解决时,对余额的未决更改将被添加到队列中,一旦它完成已修复,一切都会正确处理。
您甚至可以读取该队列并向您的用户显示信息,例如告诉用户余额有待更新,因此可能不准确。
希望这会有所帮助!
【讨论】:
【参考方案2】:首先,节点在等待MySQL数据库读取等异步请求时如何处理传入请求
nodejs 的事件循环使这种情况发生,否则您将拥有一个性能超低的完全同步程序。
在上下文中调用的每个异步函数都将在上下文本身执行后执行。
在上下文执行完成和异步函数执行之间,可以安排其他异步函数执行(这个“插入”由事件循环管理)。
李>如果等待异步函数,则上下文的剩余代码会在异步函数执行后的某个位置调度。
玩起来更清晰。示例 1:
// Expected result: 1, 3, 4, 2
function asyncFunction(x)
// setTimeout as example of async operation
setTimeout(() => console.log(x), 10)
function context()
console.log(1)
asyncFunction(2)
console.log(3)
context()
console.log(4)
示例 2:
// Expected result: 1, 2, 3
function asyncFunction(x)
// Promise as example of async operation
return new Promise((resolve) =>
console.log(x)
resolve()
)
async function context()
console.log(1)
await asyncFunction(2)
console.log(3)
context()
示例 3(与您的情况更相似):
// Expected result: 1, 2, 4, 5, 3, 6
function asyncFunction(x)
// Promise as example of async operation
return new Promise((resolve) =>
console.log(x)
resolve()
)
async function context(a, b, c)
console.log(a)
await asyncFunction(b)
console.log(c)
context(1, 2, 3)
context(4, 5, 6)
在你的例子中:
当服务器接收到连接时,处理程序的执行被调度
当处理程序被执行时,它会安排查询的执行,然后处理程序上下文的剩余部分被安排
在预定的执行之间,一切都可能发生。
【讨论】:
以上是关于当事件循环等待数据库操作时,如何处理对 nodejs 服务器的传入请求的主要内容,如果未能解决你的问题,请参考以下文章
apollo-client, onError, ApolloProvider, client.writeData(localstate)... 当服务器返回 401 时如何处理对用户的身份验证