在多租户 Node.js 应用中,最初只需从请求头读取
tenantId 并传递给服务即可。随着业务增长,tenantId、requestId、数据库选择、事务会话、日志元数据、调试状态等信息也会随请求携带。此时,单纯把这些值作为函数参数传递已无法满足需求,应用层需要手动维护并传递所有上下文信息,导致错误频发且难以维护。核心问题
- 执行状态传播:手动传递tenantId、事务会话等会导致遗漏或错误。
- 职责混乱:业务逻辑与基础设施(数据库路由、事务、日志)交织在一起。
- 可维护性差:每个层级都需关注同一组上下文,代码冗长且易错。
解决方案
Node.js 提供AsyncLocalStorage,可在异步调用链中存储共享状态。通过在请求边界(或任何执行边界)调用runWithContext,将tenantId、requestId、事务会话等放入共享上下文,后续所有业务代码可通过getContext()读取,无需显式传递。
优势
- 职责分离:业务层只关注业务逻辑,基础设施层负责租户解析、数据库路由、事务管理等。
- 事务统一:在事务边界设置会话后,所有模型操作自动使用同一事务,无需手动传递。
- 日志与监控统一:上下文中的requestId、tenantId自动附加到所有日志与指标,便于追踪与分析。
- 灵活扩展:同一机制可用于 HTTP 请求、队列任务、CLI 命令等多种执行源。
实践示例
```js
const storage = new AsyncLocalStorage();
function runWithContext(context, fn) {
return storage.run(context, fn);
}
function getContext() {
return storage.getStore();
}
// HTTP 中间件
app.use(async (req, res, next) => {
const tenantId = req.header('x-tenant-id');
if (!tenantId) return res.status(400).json({ error: 'Tenant is required.' });
const requestId = crypto.randomUUID();
await runWithContext({ tenantId, requestId }, async () => next());
});
```
结论
当多租户应用需要在业务层、事务、日志、监控等多处共享执行状态时,单纯的请求上下文已不足以满足需求。通过
AsyncLocalStorage 等执行上下文机制,将这些状态提升为运行时基础设施,既简化了代码,又提升了系统的一致性与可维护性。