当前位置: 首页 > ai >正文

Nestjs框架: 基于Mongodb的多租户功能集成和优化

概述

  • 基于前文,我们知道如何集成多租户的相关功能了, 现在我们继续集成Monodb的多租户形式
  • 需要注意的是,MongoDB 在 NestJS 中的使用过程中存在一些“坑点”
  • 如果按照默认方式集成,会发现连接数在不断增长,即使我们请求的是相同的数据库地址和租户信息
  • 这说明每次接口请求都在创建新的数据库连接,而不是复用已有连接,这个行为与我们预期不符
  • 数据库连接是有限资源,频繁创建连接会导致 IO 资源耗尽、性能下降等问题
  • 下面,我们分别来看看如何解决此类问题

开始集成


1 ) 编写 docker-compose.multi.yaml 文件

services:mongo:image: mongo:8  # 使用最新的 MongoDB 镜像container_name: mongo_apprestart: alwaysports:- "27018:27017"  # 将容器的 27017 端口映射到主机的 27017 端口environment:- MONGO_INITDB_ROOT_USERNAME=root  # 设置 MongoDB 的 root 用户名- MONGO_INITDB_ROOT_PASSWORD=123456_mongodb  # 设置 MongoDB 的 root 密码# 调整日志级别的例子,可选值如 "0"(致命错误)、"1"(警告+错误)、"2"(信息+警告+错误)...- MONGO_LOG_LEVEL=2- MONGO_SYSTEM_LOG_PATH=/data/logs/mongodb.logvolumes:- ./docker-dbconfig/mongo/conf/mongod.conf:/etc/mongod.conf- ./docker-dbconfig/mongo/data/db:/data/db  # 将容器内的 /data/db 目录映射到本地的 ./data/db 目录,用于数据持久化- ./docker-dbconfig/mongo/logs:/data/logsnetworks:- light_networkmongo-express:image: mongo-express:latestcontainer_name: mongo_express_apprestart: alwaysenvironment:ME_CONFIG_MONGODB_ADMINUSERNAME: rootME_CONFIG_MONGODB_ADMINPASSWORD: 123456_mongodbME_CONFIG_MONGODB_URL: mongodb://root:123456_mongodb@mongo:27017/ME_CONFIG_BASICAUTH: falseports:- 18081:8081networks:- light_networkmongo2:image: mongo:8  # 使用最新的 MongoDB 镜像container_name: mongo_app2restart: alwaysports:- "27019:27017"  # 将容器的 27017 端口映射到主机的 27017 端口environment:- MONGO_INITDB_ROOT_USERNAME=root  # 设置 MongoDB 的 root 用户名- MONGO_INITDB_ROOT_PASSWORD=123456_mongodb  # 设置 MongoDB 的 root 密码# 调整日志级别的例子,可选值如 "0"(致命错误)、"1"(警告+错误)、"2"(信息+警告+错误)...- MONGO_LOG_LEVEL=2- MONGO_SYSTEM_LOG_PATH=/data/logs/mongodb.logvolumes:- ./docker-dbconfig/mongo2/conf/mongod.conf:/etc/mongod.conf- ./docker-dbconfig/mongo2/data/db:/data/db  # 将容器内的 /data/db 目录映射到本地的 ./data/db 目录,用于数据持久化- ./docker-dbconfig/mongo2/logs:/data/logsnetworks:- light_networkmongo-express2:image: mongo-express:latestcontainer_name: mongo_express_app2restart: alwaysenvironment:ME_CONFIG_MONGODB_ADMINUSERNAME: rootME_CONFIG_MONGODB_ADMINPASSWORD: 123456_mongodbME_CONFIG_MONGODB_URL: mongodb://root:123456_mongodb@mongo2:27017/ME_CONFIG_BASICAUTH: falseports:- 18082:8081networks:- light_networknetworks:light_network:external: true
  • 启动服务,之后在 UI 管理界面给 2个数据库加入可识别的数据
  • 下面测试的时候,会看到数据

2 ) 配置 .env

T1_MONGODB_URI="mongodb://root:123456_mongodb@localhost:27018/nestmongodb"
T2_MONGODB_URI="mongodb://root:123456_mongodb@localhost:27019/nestmongodb"

3 ) 编写 database/mongoose/mongoose.constant.ts

// 这个配置模拟调接口/读数据库获取的
export const tenantMap = new Map([['1', 'T1'],['2', 'T2']
]);
export const defaultTenant = tenantMap.values().next().value; // 第一个

4 ) 编写 database/mongoose/mongoose-config.service.ts

import { Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import {MongooseModuleOptions,MongooseOptionsFactory,
} from '@nestjs/mongoose';
import { Request } from 'express';
import { tenantMap, defaultTenant } from './mongoose.constant';
import { ConfigService } from '@nestjs/config';export class MongooseConfigService implements MongooseOptionsFactory {constructor(@Inject(REQUEST) private request: Request,private configService: ConfigService,) {}createMongooseOptions():| MongooseModuleOptions| Promise<MongooseModuleOptions> {const headers = this.request.headers;const tenantId = headers['x-tenant-id'] as string;console.log('tenantId: ', tenantId)if (tenantId && !tenantMap.has(tenantId)) {throw new Error('invalid tenantId');}const t_prefix = !tenantId ? defaultTenant : tenantMap.get(tenantId);const uri = this.configService.get<string>(`${t_prefix}_MONGODB_URI`);return { uri } as MongooseModuleOptions;}
}

5 ) 编写 app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { MongooseModule } from '@nestjs/mongoose';
import { UserSchema } from './user/user.schema'
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MongooseConfigService } from './database/mongoose/mongoose-config.service';@Module({imports: [// 1. 下面这个后续可以封装一个新的模块,来匹配 .env 和 其他配置ConfigModule.forRoot({  // 配置环境变量模块envFilePath: '.env', // 指定环境变量文件路径isGlobal: true, // 全局可用}),MongooseModule.forRootAsync({useClass: MongooseConfigService,}),MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]),],controllers: [AppController],providers: []
})export class AppModule {}

6 ) 编写 app.controller.ts

import { Controller, Get } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from './user/user.entity';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Document as Doc } from 'mongoose';@Controller()
export class AppController {constructor(@InjectModel('User') private userModel: Model<Doc>,) {}@Get('/multi-mongoose')async getMongooseUsers(): Promise<any> {const rs = await this.userModel.find()return rs;}
}

测试


1 ) 测试1

  • 请求

    curl --request GET \--url http://localhost:3000/multi-mongoose \--header 'x-tenant-id: 1'
    
  • 响应

    [{"_id": "6874d4b0d10e36e350dd588d","username": "mongo1","password": "123456"},{"_id": "6874d4d9d10e36e350dd588f","username": "lee","password": "123456"}
    ]
    

2 ) 测试2

  • 请求

    curl --request GET \--url http://localhost:3000/multi-mongoose \--header 'x-tenant-id: 2'
    
  • 响应

    [{"_id": "6874d4b0d10e36e350dd588d","username": "mongo2","password": "123456"},{"_id": "6874d4d9d10e36e350dd588f","username": "lee","password": "123456"}
    ]
    

3 )进入其中一个数据库,测试连接情况

docker exec -it mongo_app2 mongosh admin -u root # 输入密码
use nestmongodb;# 其实上面一个 use 命令 可以不用
db.serverStatus().connections

输出如下:

{current: 6,available: 838839,totalCreated: 38,rejected: 0,active: 2,threaded: 6,exhaustIsMaster: Long('0'),exhaustHello: Long('0'),awaitingTopologyChanges: Long('1'),loadBalanced: Long('0')
}

目前 active 有2个,当不断请求同一个接口,在此执行上述命令,可发现这个数字是累加的
这明显是不对的,访问相同链接应该使用同一个 connection, 而不是创建新的通道
当租户多起来的时候,会带来严重的性能问题

4 ) 分析原因

@nestjs/mongoose 包的 mongoose-core.module.ts 中 在 使用 factory 方法的时候,会调用 createMongooseConnection

private static async createMongooseConnection(uri: string,mongooseOptions: ConnectOptions,factoryOptions: {lazyConnection?: boolean;onConnectionCreate?: MongooseModuleOptions['onConnectionCreate'];},
): Promise<Connection> {const connection = mongoose.createConnection(uri, mongooseOptions);if (factoryOptions?.lazyConnection) {return connection;}factoryOptions?.onConnectionCreate?.(connection);return connection.asPromise();
}

这个是每次都会创建的根本原因,这个包也没有提供 类似 datasourceFactory 的功能
现在需要定制 mongoose 的 module 中的 forRootSync 的逻辑

定制 mongoose 的 forRootAsync 方法

现在需要对官方 @nestjs/mongoose 包中的核心模块的方法进行裁剪和优化
找到对应的文件贴到自己项目中进行修改

1 )新建 src/database/mongoose/mongoose.module.ts

import { Module, DynamicModule } from '@nestjs/common';
import { MongooseModuleAsyncOptions, MongooseModuleOptions, MongooseModule as NestMongooseModule
} from '@nestjs/mongoose';
import { MongooseCoreModule } from './mongoose-core.module';@Module({})
export class MongooseModule extends NestMongooseModule {static forRoot(uri: string,options: MongooseModuleOptions = {},): DynamicModule {return {module: MongooseModule,imports: [MongooseCoreModule.forRoot(uri, options)],}}static forRootAsync(options: MongooseModuleAsyncOptions): DynamicModule {return {module: MongooseModule,imports: [MongooseCoreModule.forRootAsync(options)],}}
}

这是最外层,用于引入 MongooseCoreModule 模块, 重写有问题的源码

2 )新建 src/database/mongoose/mongoose-core.module.ts

import {DynamicModule,Global,Inject,Module,OnApplicationShutdown,Provider,Type,
} from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import * as mongoose from 'mongoose';
import { ConnectOptions, Connection } from 'mongoose';
import { defer, lastValueFrom } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { handleRetry } from './mongoose.utils';
import {MONGOOSE_CONNECTION_NAME,MONGOOSE_MODULE_OPTIONS,
} from './mongoose.constants';import {MongooseModuleOptions,MongooseModuleAsyncOptions,MongooseModuleFactoryOptions,MongooseOptionsFactory,getConnectionToken,
} from '@nestjs/mongoose';@Global()
@Module({})
export class MongooseCoreModule implements OnApplicationShutdown {private static connections: Record<string, mongoose.Connection> = {};constructor(@Inject(MONGOOSE_CONNECTION_NAME) private readonly connectionName: string,private readonly moduleRef: ModuleRef,) {}static forRoot(uri: string,options: MongooseModuleOptions = {},): DynamicModule {const {retryAttempts,retryDelay,connectionName,connectionFactory,connectionErrorFactory,lazyConnection,onConnectionCreate,verboseRetryLog,...mongooseOptions} = options;const mongooseConnectionFactory =connectionFactory || ((connection) => connection);const mongooseConnectionError =connectionErrorFactory || ((error) => error);const mongooseConnectionName = getConnectionToken(connectionName);const mongooseConnectionNameProvider = {provide: MONGOOSE_CONNECTION_NAME,useValue: mongooseConnectionName,};const connectionProvider = {provide: mongooseConnectionName,useFactory: async (): Promise<any> =>await lastValueFrom(defer(async () =>mongooseConnectionFactory(await this.createMongooseConnection(uri, mongooseOptions, {lazyConnection,onConnectionCreate,}),mongooseConnectionName,),).pipe(handleRetry(retryAttempts, retryDelay, verboseRetryLog),catchError((error) => {throw mongooseConnectionError(error);}),),),};return {module: MongooseCoreModule,providers: [connectionProvider, mongooseConnectionNameProvider],exports: [connectionProvider],};}static forRootAsync(options: MongooseModuleAsyncOptions): DynamicModule {const mongooseConnectionName = getConnectionToken(options.connectionName);const mongooseConnectionNameProvider = {provide: MONGOOSE_CONNECTION_NAME,useValue: mongooseConnectionName,};const connectionProvider = {provide: mongooseConnectionName,useFactory: async (mongooseModuleOptions: MongooseModuleFactoryOptions,): Promise<any> => {const {retryAttempts,retryDelay,uri,connectionFactory,connectionErrorFactory,lazyConnection,onConnectionCreate,verboseRetryLog,...mongooseOptions} = mongooseModuleOptions;const mongooseConnectionFactory =connectionFactory || ((connection) => connection);const mongooseConnectionError =connectionErrorFactory || ((error) => error);return await lastValueFrom(defer(async () =>mongooseConnectionFactory(await this.createMongooseConnection(uri as string,mongooseOptions,{ lazyConnection, onConnectionCreate },),mongooseConnectionName,),).pipe(handleRetry(retryAttempts, retryDelay, verboseRetryLog),catchError((error) => {throw mongooseConnectionError(error);}),),);},inject: [MONGOOSE_MODULE_OPTIONS],};const asyncProviders = this.createAsyncProviders(options);return {module: MongooseCoreModule,imports: options.imports,providers: [...asyncProviders,connectionProvider,mongooseConnectionNameProvider,],exports: [connectionProvider],};}private static createAsyncProviders(options: MongooseModuleAsyncOptions,): Provider[] {if (options.useExisting || options.useFactory) {return [this.createAsyncOptionsProvider(options)];}const useClass = options.useClass as Type<MongooseOptionsFactory>;return [this.createAsyncOptionsProvider(options),{provide: useClass,useClass,},];}private static createAsyncOptionsProvider(options: MongooseModuleAsyncOptions,): Provider {if (options.useFactory) {return {provide: MONGOOSE_MODULE_OPTIONS,useFactory: options.useFactory,inject: options.inject || [],};}// `as Type<MongooseOptionsFactory>` is a workaround for microsoft/TypeScript#31603const inject = [(options.useClass || options.useExisting) as Type<MongooseOptionsFactory>,];return {provide: MONGOOSE_MODULE_OPTIONS,useFactory: async (optionsFactory: MongooseOptionsFactory) =>await optionsFactory.createMongooseOptions(),inject,};}private static async createMongooseConnection(uri: string,mongooseOptions: ConnectOptions,factoryOptions: {lazyConnection?: boolean;onConnectionCreate?: MongooseModuleOptions['onConnectionCreate'];},): Promise<Connection> {// 添加这里if (this.connections[uri]) {return this.connections[uri];}const connection = mongoose.createConnection(uri, mongooseOptions);this.connections[uri] = connection; // 这里赋值if (factoryOptions?.lazyConnection) {return connection;}factoryOptions?.onConnectionCreate?.(connection);return connection.asPromise();}async onApplicationShutdown() {const connection = this.moduleRef.get<any>(this.connectionName);if (connection) {await connection.close();}const connectionClients = Object.values(MongooseCoreModule.connections);if (connectionClients.length > 0) {// 销毁所有 mongoose connectionfor (const client of connectionClients) {client?.close();}}}
}

这里,注意 createMongooseConnection 以及 onApplicationShutdown 中的 处理
对连接进行优化处理,以及在异常关闭时对客户端进行销毁

3 )src/database/mongoose/mongoose.constants.ts

// 这个配置模拟调接口/读数据库获取的
export const tenantMap = new Map([['1', 'T1'],['2', 'T2']
]);
export const defaultTenant = tenantMap.values().next().value; // 第一个// 新增如下
export const DEFAULT_DB_CONNECTION = 'DatabaseConnection';
export const MONGOOSE_MODULE_OPTIONS = 'MongooseModuleOptions';
export const MONGOOSE_CONNECTION_NAME = 'MongooseConnectionName';export const RAW_OBJECT_DEFINITION = 'RAW_OBJECT_DEFINITION';

3 )src/database/mongoose/mongoose.utils.ts

import { Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { delay, retryWhen, scan } from 'rxjs/operators';export function handleRetry(retryAttempts = 9,retryDelay = 3000,verboseRetryLog = false,
): <T>(source: Observable<T>) => Observable<T> {const logger = new Logger('MongooseModule');return <T>(source: Observable<T>) =>source.pipe(retryWhen((e) =>e.pipe(scan((errorCount, error) => {const verboseMessage = verboseRetryLog? ` Message: ${error.message}.`: '';const retryMessage =retryAttempts > 0 ? ` Retrying (${errorCount + 1})...` : '';logger.error(['Unable to connect to the database.',verboseMessage,retryMessage,].join(''),error.stack,);if (errorCount + 1 >= retryAttempts) {throw error;}return errorCount + 1;}, 0),delay(retryDelay),),),);
}
  • 工具包的部分函数

4 )测试

  • 重启项目,连入其中一个mongo 的 docker 容器,进入数据库,执行 db.serverStatus().connections
  • 调用对应数据库的租户标识的接口,再次执行 db.serverStatus().connections
  • 可看到 currentactive 增加了,后面多次调用同一租户接口则不会再增加
  • 结束程序,则会销毁客户端,响应数字会同步减少
  • 这样就完成了相关功能

总结

  • 问题本质:Mongoose 在 NestJS 中的连接未复用,导致连接数异常增长
  • 核心影响:IO 资源浪费、性能下降、数据库连接池耗尽
  • 解决思路:
    • 在服务层缓存已有连接
    • 修改 Mongoose 模块源码逻辑
    • 使用第三方连接池库进行封装
    • 最佳实践:
      • 多租户系统中应确保数据库连接的唯一性与复用性
      • 建议对 Mongoose 模块进行轻量级封装以适配业务需求
http://www.xdnf.cn/news/16255.html

相关文章:

  • 算子推理是什么
  • 电脑开机后网络连接慢?
  • (Python)文件储存的认识,文件路径(文件储存基础教程)(Windows系统文件路径)(基础教程)
  • 【17】C# 窗体应用WinForm ——【文本框TextBox、富文本框RichTextBox 】属性、方法、实例应用
  • C++:list(2)list的模拟实现
  • Java中配置两个r2db连接不同的数据库
  • JavaScript:现代Web开发的核心动力
  • Mistral AI开源 Magistral-Small-2507
  • C++查询mysql数据
  • Codeforces Round 181 (Rated for Div. 2)
  • Bert项目--新闻标题文本分类
  • DAY31 整数矩阵及其运算
  • 告别镜像拉取慢!CNB无痛加速方案,一键起飞
  • [论文阅读] 人工智能 + 软件工程 | NoCode-bench:评估LLM无代码功能添加能力的新基准
  • JVM常见工具
  • swagger基本注解@Tag、@Operation、@Parameters、@Parameter、@ApiResponse、@Schema
  • 基于图神经网络的星间路由与计算卸载强化学习算法设计与实现
  • 【Linux手册】操作系统如何管理存储在外设上的文件
  • 基于 Claude Code 与 BrowserCat MCP 的浏览器自动化全链路构建实践
  • iOS 26,双版本更新来了
  • 【web大前端】001_前端开发入门:创建你的第一个网页
  • 二十八、【Linux系统域名解析】DNS安装、子域授权、缓存DNS、分离解析、多域名解析
  • 前端开发 Vue 结合Sentry 实现性能监控
  • 配置DNS正反向解析
  • 告别复杂配置!Spring Boot优雅集成百度OCR的终极方案
  • JAVA算法题练习day1
  • 常见代码八股
  • 【深度之眼机器学习笔记】04-01-决策树简介、熵,04-02-条件熵及计算举例,04-03-信息增益、ID3算法
  • 力扣671. 二叉树中第二小的节点
  • Spring框架