You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 00:39:18