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: 2
2.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)