TypeORM多租户场景下连接缓存、连接池及作用域实现咨询
TypeORM 按租户缓存复用连接的实现方案
TypeORM 原生没有提供多租户维度的连接缓存能力,通过全局单例连接映射表+独立DataSource连接池的方式即可实现同租户连接复用,无需使用请求作用域Provider。
核心逻辑:
- 维护全局单例
Map结构,key为租户ID/对应schema名,value存储该租户已初始化完成的DataSource实例(TypeORM 0.3+版本使用DataSource替代旧版Connection,旧版逻辑完全一致) - 封装统一的连接获取方法:请求到达拿到租户标识后,优先查缓存中是否存在已初始化的对应租户DataSource,存在则直接返回;不存在才执行初始化,初始化完成后写入缓存。初始化阶段加并发锁,避免同一时间多个同租户请求触发重复建连。
- 每个租户的DataSource自带独立连接池,同租户所有请求共用该连接池内的连接,完全符合连接池复用设计,不会出现同租户重复建连的问题。
参考实现代码:
import { DataSource } from 'typeorm'; import { Cat } from './entities/cat.entity'; // 全局单例租户连接缓存 const tenantDataSources = new Map<string, DataSource>(); // 初始化并发锁,避免同时间重复创建同租户连接 const initLocks = new Map<string, Promise<DataSource>>(); export async function getTenantDataSource(tenantId: string, schema: string): Promise<DataSource> { // 命中缓存直接返回已初始化连接 const existDs = tenantDataSources.get(tenantId); if (existDs?.isInitialized) return existDs; // 已有同租户连接正在初始化,等待结果返回即可 if (initLocks.has(tenantId)) return initLocks.get(tenantId); // 初始化新租户连接 const initPromise = (async () => { const newDs = new DataSource({ type: 'postgres', host: '数据库地址', port: 5432, username: '账号', password: '密码', database: '库名', schema: schema, synchronize: false, entities: [Cat /* 其余实体类统一注册在这里 */], poolSize: 5, // 单租户连接池大小,根据业务QPS调整 }); await newDs.initialize(); tenantDataSources.set(tenantId, newDs); initLocks.delete(tenantId); return newDs; })(); initLocks.set(tenantId, initPromise); return initPromise; }
业务方法内初始化Repository的实现方案
该方案的核心是保持服务为默认单例作用域,不在构造函数中绑定固定Repository实例,而是在业务方法执行时,基于当前请求的租户上下文获取对应租户的DataSource,再即时拿到该租户下的Repository操作数据库,完全规避请求作用域带来的服务反复重建开销。
实现要点:
- 用Node原生
AsyncLocalStorage存储请求级别的租户上下文,不需要额外引入第三方上下文库,也不需要在每个方法手动传租户ID - 在服务内部封装统一的Repository获取私有方法,减少重复代码,避免代码冗余
参考实现代码:
首先是租户上下文与中间件实现:
import { AsyncLocalStorage } from 'async_hooks'; import { Injectable, NestMiddleware, NextFunction, Request, Response } from '@nestjs/common'; // 全局请求上下文实例 export const requestContext = new AsyncLocalStorage<{ tenantId: string; schema: string }>(); @Injectable() export class TenantMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { // 从请求头/Token中解析租户信息,按实际业务规则调整 const tenantId = req.headers['x-tenant-id'] as string; const schema = `tenant_${tenantId}`; // 写入上下文 requestContext.run({ tenantId, schema }, () => next()); } }
服务层实现:
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Cat } from './entities/cat.entity'; import { getTenantDataSource, requestContext } from './tenant.util'; import { CreateCatDto } from './dto/create-cat.dto'; @Injectable() export class CatsService { // 构造函数不注入请求作用域连接,也不初始化固定Repository constructor() {} // 封装内部统一获取当前租户Repository的方法,减少重复代码 private async getCatsRepository() { const ctx = requestContext.getStore(); if (!ctx) throw new Error('租户上下文不存在'); const ds = await getTenantDataSource(ctx.tenantId, ctx.schema); return ds.getRepository(Cat); } // 业务方法执行时再获取对应租户的Repository操作 async findAll() { const repo = await this.getCatsRepository(); return repo.find(); } async create(dto: CreateCatDto) { const repo = await this.getCatsRepository(); const newCat = repo.create(dto); return repo.save(newCat); } }
实践提示
- 配套租户生命周期管理逻辑,租户下线时主动调用对应DataSource的
destroy()方法销毁连接池,避免连接和内存泄漏 - 若租户量级较大,给缓存的DataSource加LRU淘汰策略,回收长期无访问的冷租户连接,控制总连接数在数据库承载范围内
- TypeORM 0.2.x旧版本可将上述代码中的DataSource替换为Connection,逻辑完全一致
内容的提问来源于stack exchange,提问作者juztcode
相关产品推荐
相关产品推荐

