requestId 的作用
在前端开发中,有浏览器这个载体,报错信息可以在浏览器的控制台看到。
后端开发的载体是服务器,上线出现的错误只能到服务器上去看打印的日志。
问题是服务器是支持并发请求的,当同一时间处理多个请求,每个请求的链路又比较长,且有的中间环节还有异步(请求其他服务)时,就很难分辨哪个日志属于哪个请求,所以日志和请求的关联关系对于后端开发查看日志来说就很重要了。
举例说明:
当 usename 为 123 的用户访问接口 /getList 时,先打印 username 123 ,然后请求远程的笔记接口。
随后 usename 为 789 的用户访问接口 /getList ,打印 username 789 然后请求远程的笔记接口。
然后两个用户的请求回来了,最终的打印结果为
现在无法区分返回的数据哪个是123的,哪个是789的。
当然你可以这样修改来查看归属:
但如果这个接口调用很多的其他模块方法,每一个模块都要透传这个标记参数,那就太麻烦了。
最佳实践:AsyncLocalStorage
AsyncLocalStorage 通过 Node.js 的 async_hooks 模块来跟踪异步操作的执行上下文。当你在一个“存储区域”(store)中运行一个函数时,AsyncLocalStorage 会记录这个上下文。任何在该函数内部触发的后续异步操作,都能访问到之前在这个上下文中存储的值。
使用方法
- 创建存储空间 (Store): 创建一个 AsyncLocalStorage 实例,就像一个独立的盒子。
- 运行并存储 (Run and Store): 使用 .run() 方法在这个“盒子”里启动一个函数,并提供一个初始值(存储空间)。
- 在任何地方访问 (Access Anywhere): 在该函数及其所有同步或异步后继代码中,你都可以使用 .getStore() 方法来获取当前异步上下文中的那个值。
nest中如何使用
1. 首先定义一个async模块,用来包裹整个请求的异步上下文
async.service.ts
async.module.ts
2. 定义一个logger模块,用来打印日志
logger.service.ts
logger.module.ts
import { Global, Module, Logger } from '@nestjs/common';
import * as winston from 'winston';
import { WinstonModule, utilities } from 'nest-winston';
import { ConfigService } from '@nestjs/config';
import { LoggerService } from 'src/logger.service';
import 'winston-daily-rotate-file';
@Global()
@Module({
imports: [
WinstonModule.forRootAsync({
useFactory: (configService: ConfigService) => {
const logger = new Logger('WinstonConfig');
const winstonConfig = configService.get('winston') as WinstonConfig;
logger.debug(`${JSON.stringify(winstonConfig)}`);
const { level, dirname, filename, datePattern, maxSize, maxFiles } =
winstonConfig;
return {
level: 'debug',
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
utilities.format.nestLike(),
),
}),
new winston.transports.DailyRotateFile({
level,
dirname,
filename,
datePattern,
maxSize,
maxFiles, // 最多保存多少天
zippedArchive: true, // 压缩
format: winston.format.combine(
winston.format.timestamp(),
winston.format.simple(),
),
}),
new winston.transports.DailyRotateFile({
level: 'error',
dirname,
filename: `error-${filename}`,
datePattern,
maxSize,
maxFiles,
zippedArchive: true,
format: winston.format.combine(
winston.format.timestamp(),
winston.format.simple(),
),
}),
],
};
},
inject: [ConfigService],
}),
],
providers: [LoggerService],
exports: [LoggerService],
})
export class LoggerModule {}
3. 定义一个拦截器:interceptor.ts,作用是在请求刚进入服务时,创建 XTransactionID(就是requestId),并且使用async模块进行包裹
通过以上的操作,当请求进来时,拦截器创建的XTransactionID,随后无论当前请求流转在哪些模块的service中,调用的logger 模块都可以获取到在拦截器创建的XTransactionID,保证整个请求周期内XTransactionID一致。
效果展示
cc5fbd73-1e0b-45ee-8958-9aa5b2402ffc 就是流转中的 XTransactionID,在经过多个模块时,依然保持一致。
绝对不推荐使用:Scope.REQUEST
之所以着重说它,是因为大部分AI工具都会推荐你使用这个方法,我刚开始也是用的这个方法,但是实践过程中发现很多问题,所以最后抛弃这种写法。
Scope.REQUEST 的具体的示例代码就不展示了,因为不推荐,以下说明原因:
1. 性能损耗与内存开销
Scope.REQUEST 会导致每个请求都创建新的提供者实例,而非单例复用。在高并发场景下,这会显著增加内存占用和垃圾回收(GC)压力,降低应用吞吐量。对于需要频繁访问的服务(如日志、追踪等),这种开销会被放大。
在nest中,默认创建的模块都是单例模块,它的特点就是在服务启动时实例化一次(也就是new 这个类),在重新启动服务前,这个实例都不会再初始化了,并且这个模块可以使用 onModuleInit, onApplicationBootstrap 等生命周期钩子。
使用 Scope.REQUEST 定义的模块就是懒加载模块,他在服务启动时并不会实例化,在每个请求流转执行到他时都会实例化,也就是请求流转涉及到的 class 的 constructor 在每次请求时都会执行,这个要特别注意!这就造成一定的性能开销。onModuleInit, onApplicationBootstrap 等生命周期钩子都不会执行了,因为他们已经错过了执行时机,这就像一场派对(应用启动)。onApplicationBootstrap 是派对正式开始时的鸣枪仪式。如果一个客人(懒加载模块)在派对开始一小时后才迟到入场,我们不可能为了他再鸣一次枪。不要小瞧这些小钩子,有的时候他们很有用。而懒加载会导致有些生命周期钩子不可用(onModuleInit, onApplicationBootstrap)
2. 依赖链污染
Nest 的依赖注入系统具有 "作用域继承" 特性:如果一个单例作用域(默认)的提供者依赖了 Scope.REQUEST 作用域的提供者,Nest 会被迫将该单例提供者也转为 Scope.REQUEST 作用域(否则会出现 "单例依赖请求作用域实例" 的矛盾)。这可能导致整个依赖链被 "污染",大量本应是单例的服务被迫变为请求级实例,进一步加剧性能问题。
在 NestJS 中,一旦一个模块直接或间接引入了任何 Scope.REQUEST 的模块,整个模块都会变成“懒加载模块”(Lazy-loaded module)。这个是一个非常值得注意的点,我之前就是使用Scope.REQUEST 定义的logger.service,由于日志打印每个模块基本都用,造成了所有模块几乎都是懒加载模块!
3. 与某些特性不兼容
部分 Nest 特性(如拦截器的 @Injectable() 单例模式、某些缓存策略)与 Scope.REQUEST 存在兼容性问题,可能导致不可预期的行为(如上下文丢失、实例复用异常等)。
目前还没有发现什么场景下必须使用Scope.REQUEST
最后编辑于:2026-01-11 09:11:52
©著作权归作者所有,转载或内容合作请联系作者。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,little笔记系信息发布平台,仅提供信息存储服务。