Mongoose官方模块的问题及解决思路
前面的课程主要涉及关系型数据库的 ORM 库(Prisma 和 TypeORM),本节扩展到非关系型数据库 MongoDB。虽然 MongoDB 也和 TypeORM 一样有 NestJS 官方模块支持,但在多租户场景下存在严重的连接泄漏问题。
环境准备
使用 Docker Compose 配置多个 MongoDB 实例:
# docker-compose.mongo.yml
services:
mongo:
image: mongo:6
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: 123456
mongo-1:
image: mongo:6
ports:
- "27018:27017" # 宿主机端口 27018
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: 123456
mongo-express:
image: mongo-express
ports:
- "8081:8081"
environment:
ME_CONFIG_MONGODB_URL: mongodb://root:123456@mongo:27017/
mongo-express-1:
image: mongo-express
ports:
- "8082:8082"
environment:
ME_CONFIG_MONGODB_URL: mongodb://root:123456@mongo-1:27017/
yaml
# 启动 MongoDB 服务
docker compose -f docker-compose.mongo.yml up -d
bash
创建 MongooseConfigService
仿照 TypeORM 的多租户配置方式,创建 MongooseConfigService:
import { Request } from 'express';
import { MongooseModuleOptions, MongooseOptionsFactory } from '@nestjs/mongoose';
export class MongooseConfigService implements MongooseOptionsFactory {
constructor(@Inject(REQUEST) private request: Request) {}
createMongooseOptions(): MongooseModuleOptions {
const { tenantId } = this.request.headers as any;
if (tenantId === 'default') {
return {
uri: 'mongodb://root:123456@localhost:27017/testdb?authSource=admin',
};
} else if (tenantId === 'default1') {
return {
uri: 'mongodb://root:123456@localhost:27018/testdb?authSource=admin',
};
}
throw new Error(`Unknown tenant: ${tenantId}`);
}
}
typescript
AppModule 中配置 Mongoose
@Module({
imports: [
MongooseModule.forRootAsync({
useClass: MongooseConfigService,
}),
MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]),
],
})
export class AppModule {}
typescript
连接泄漏问题
表面上看功能正常,但通过 MongoDB 的管理命令可以观察到连接数在不断增长:
# 连接到 MongoDB 容器
docker exec -it <container_name> mongosh -u root -p 123456
# 查看连接数
db.serverStatus().connections
bash
测试结果:
| 操作 | 连接数 |
|---|---|
| 启动后 | 2 |
| 请求 3 次 default | 5(每次请求新增 1 个连接) |
| 再请求 2 次 default1 | 7 |
相同 URL 的请求没有复用已有连接,每次请求都在创建新的 Mongoose Connection。
问题根因分析
MongooseModule.forRootAsync({ useClass: MongooseConfigService })
|
v
createMongooseOptions() -> 返回 { uri: '...' }
|
v
注入到 MONGOOSE_MODULE_OPTIONS
|
v
connectionFactory 中调用 createMongooseConnection(uri)
|
v
每次工厂函数执行都创建新连接,不检查是否已有相同 URI 的连接
text
官方模块的 createMongooseConnection 方法没有任何连接复用逻辑,每被调用一次就创建一个全新的 Connection 实例。
解决思路
面对第三方模块的问题,有两种解决方式:
方式一:完整复制源码
- 将
@nestjs/mongoose的所有源码复制到项目中直接修改 - 优点:简单直接,不受后续包更新影响
- 缺点:代码量大,无法享受官方更新
方式二:继承并覆写关键方法
- 创建自定义
MongooseModule继承官方模块 - 只覆写
forRootAsync和连接创建逻辑 - 优点:改动最小,复用官方大部分代码
- 缺点:需要处理内部 API 不被导出的问题
下一节将详细介绍方式二的实现过程。
本节总结
- MongoDB 官方模块在多租户场景下存在连接泄漏问题
- 问题根因是
createMongooseConnection每次都创建新连接,不做复用 - 数据库连接的 I/O 资源有限,连接泄漏会导致性能下降甚至服务不可用
- 解决思路有两种:完整复制源码 或 继承覆写关键方法
↑