并发请求共享同一个进程时,用全局变量保存当前请求 ID 会发生串线。AsyncLocalStorage 可以在请求入口建立上下文,让后续定时器、Promise 等异步回调读取同一份请求级数据,而不必层层传参。
环境:Node.js 20+;Linux、macOS 或 Windows;仅使用内置模块。若使用下面的并发请求命令,还需要 Bash 与 curl。
创建 server.mjs:
import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';
import { createServer } from 'node:http';
const requestContext = new AsyncLocalStorage();
function log(message) {
const context = requestContext.getStore();
console.log(JSON.stringify({
requestId: context?.requestId ?? null,
message,
}));
}
const server = createServer((request, response) => {
const requestId = request.headers['x-request-id'] ?? randomUUID();
requestContext.run({ requestId }, () => {
log(`${request.method} ${request.url}`);
const delay = request.url === '/slow' ? 100 : 20;
setTimeout(() => {
log('work finished');
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ requestId }));
}, delay);
});
});
server.listen(3000, '127.0.0.1', () => {
console.log('listening on http://127.0.0.1:3000');
});
启动服务:
node server.mjs
在另一个终端并发发起两个请求:
curl -s -H 'x-request-id: req-slow' http://127.0.0.1:3000/slow &
curl -s -H 'x-request-id: req-fast' http://127.0.0.1:3000/fast &
wait
printf '\n'
日志顺序可能交错,但每条 work finished 都会保留对应的 requestId。run() 只在回调及其创建的异步链上激活上下文,回调结束后会自动恢复先前状态。
实际项目里适合把 run() 放在 HTTP 中间件、队列消费者或任务调度入口。上下文只保存请求 ID、租户 ID这类轻量元数据;大对象会跟随异步资源延长生命周期。库代码通常优先使用 run(),对 enterWith() 要更谨慎,因为后者会影响当前同步执行后续创建的异步任务,边界不清时更容易让上下文泄漏到无关工作。