https://nodejs.org/api/async_hooks.html
js
{
AsyncLocalStorage: [class AsyncLocalStorage],
createHook: [Function: createHook],
executionAsyncId: [Function: executionAsyncId],
triggerAsyncId: [Function: triggerAsyncId],
executionAsyncResource: [Function: executionAsyncResource],
asyncWrapProviders: [Object: null prototype] {
},
AsyncResource: [class AsyncResource]
}上一章,简单介绍了 AsyncLocalStorage,本章一次介绍 createHook、executionAsyncId、triggerAsyncId、executionAsyncResource。
1.createHook
createHook 方法,创建一个 AsyncHook 对象,用于在异步函数调用开始和结束之间,插入自定义的代码。
js
const asyncHooks = require('async_hooks')
const fs = require('fs')
function log(...args) {
fs.writeSync(1, args.join(' ') + '\n')
}
log(asyncHooks.executionAsyncId())
asyncHooks.createHook({
init(asyncId, type, triggerAsyncId, resource) {
log('Init: ', `${type}(asyncId=${asyncId}, parentAsyncId: ${triggerAsyncId}), resource: ${resource}`)
},
before(asyncId) {
log('Before: ', asyncId)
},
after(asyncId) {
log('After: ', asyncId)
},
destroy(asyncId) {
log('Destory: ', asyncId)
}
}).enable()
setTimeout(() => {
// after 生命周期在回调函数最前边
log('Info', 'Async Before')
Promise.resolve(3).then(o => log('Info', o))
// after 生命周期在回调函数最后边
log('Info', 'Async After')
})
//=> Output
// Init: Timeout(asyncId=2, parentAsyncId: 1)
// Before: 2
// Info: Async Before
// Init: PROMISE(asyncId=3, parentAsyncId: 2)
// Init: PROMISE(asyncId=4, parentAsyncId: 3)
// Info: Async After
// After: 2
// Before: 4
// Info 3
// After: 4
// Destory: 22.executionAsyncId
executionAsyncId 方法,返回当前异步函数调用的 asyncId。
js
const async_hooks = require('node:async_hooks')
const fs = require('node:fs')
function log(...args) {
fs.writeSync(1, args.join(' ') + '\n')
}
log(async_hooks.executionAsyncId()) // 1
const path = '.'
fs.open(path, 'r', (err, fd) => {
console.log(async_hooks.executionAsyncId()) // 2
})3.triggerAsyncId
triggerAsyncId 方法,返回当前异步函数调用的父异步函数的 asyncId。
js
const async_hooks = require('node:async_hooks')
const fs = require('node:fs')
function log(...args) {
fs.writeSync(1, args.join(' ') + '\n')
}
log(async_hooks.triggerAsyncId()) // 0
const path = '.'
fs.open(path, 'r', (err, fd) => {
console.log(async_hooks.triggerAsyncId()) // 1
})4.executionAsyncResource
executionAsyncResource 方法,返回当前异步函数调用的父异步函数的 AsyncResource 对象。
js
const async_hooks = require('node:async_hooks')
const fs = require('node:fs')
function log(...args) {
fs.writeSync(1, args.join(' ') + '\n')
}
log(async_hooks.executionAsyncResource()) // [object Object]
const timer = setTimeout(() => {
log('timer', timer) // 2
log(async_hooks.executionAsyncResource()) // 2
}, 0)
const path = '.'
fs.open(path, 'r', (err, fd) => {
console.log(async_hooks.executionAsyncResource()) // FSReqCallback { oncomplete: [Function (anonymous)] }
})5.CLS Issue
CLS 全称为 Continuation-local storage,即持久化本地存储。
如下例中的同一个全局 session,如果 /a 和 /b 两次请求存在时间差,那么第二次请求的 session 值会覆盖第一次请求的 session 值。
但实际上,我们想要 /a 请求中获取到的是 /a,而 /b 请求中获取到的是 /b。
js
const http = require('node:http')
const asyncHooks = require('node:async_hooks')
const session = new Map()
const server = http.createServer(async (req, res) => {
console.log(req.url, asyncHooks.executionAsyncId())
const { url } = req
session.set('url', url)
if (url === '/a') {
await new Promise((resolve) => {
setTimeout(() => {
resolve()
}, 3000)
})
res.end(session.get('url'))
return
}
res.end(session.get('url'))
})
server.listen(3000, () => {
console.log('Server listening on port 3000')
})创建一个 sh 脚本,用于发送请求 /a 和 /b,并等待所有请求完成。来测试上面的脚本结果。
sh
#!/bin/bash
# 发送请求 A
curl http://127.0.0.1:3000/a &
# 等待 500 毫秒
sleep 0.5
# 发送请求 B
curl http://127.0.0.1:3000/b &
# 等待所有后台进程结束
wait为了解决上面的问题,我们可以使用 AsyncLocalStorage。
因为请求 a 和 b 对应的 executionAsyncId 是不相同的。每个请求的 executionAsyncId 都是唯一的,原因如下:
- 异步上下文独立性 每个
HTTP请求在Node.js中都会被处理为一个独立的异步操作。当请求a和b到达时,Node.js会为每个请求创建一个新的异步上下文。executionAsyncId是用于标识这些异步上下文的唯一ID,因此每个请求的异步上下文都会有一个独立的executionAsyncId。 - 事件驱动模型
Node.js依赖事件循环来处理I/O操作,包括HTTP请求。每当一个新的HTTP请求到达时,事件循环会将其放入队列并处理。在处理每个请求时,Node.js会创建一个新的异步操作,赋予其一个新的executionAsyncId。 由于a和b是两个独立的HTTP请求,它们在事件循环中是两个独立的事件。因此,Node.js为每个事件分配不同的executionAsyncId。 - 异步资源的唯一标识
executionAsyncId主要用于跟踪和区分不同的异步操作。在HTTP服务器的上下文中,每个客户端请求(如a和b)都是一个独立的异步操作,并且每个操作有自己的资源(如套接字、回调函数)。 为了确保这些异步操作在跟踪和调试时可以被正确识别和区分,Node.js使用executionAsyncId作为它们的唯一标识。
js
const asyncHooks = require('node:async_hooks')
const session = new Map()
const fs = require('node:fs')
function log(...args) {
fs.writeSync(1, args.join(' ') + '\n')
}
function timeout (id) {
session.set('a', id)
setTimeout(() => {
log('timeout', asyncHooks.executionAsyncId())
const a = session.get('a')
console.log(a)
})
}
timeout(1)
timeout(2)
timeout(3)另外也可以参考第三方开源项目:
cls-sessionnode-continuation-local-storage
js
const Session = require('cls-session')
const session = new Session()
function timeout (id) {
session.scope(() => {
session.set('a', id)
setTimeout(() => {
const a = session.get('a')
console.log(a)
})
})
}
timeout(1)
timeout(2)
timeout(3)