NestJS 多租户架构设计与实现
学习目标
- 理解多租户架构的基本概念和优势
- 掌握 NestJS 应用中实现多租户的核心策略
- 学习不同的数据分离方案及其适用场景
- 实现租户识别和上下文管理
- 了解多租户架构的最佳实践和性能优化
什么是多租户架构
多租户架构是一种软件架构模式,允许单个应用实例同时为多个客户(租户)提供服务,每个租户的数据和配置相互隔离。这种架构模式在 SaaS(软件即服务)应用中尤为常见,可以显著降低运营成本并提高资源利用率。
多租户架构的优势
- 成本效益:共享硬件和软件资源,降低基础设施成本
- 易于维护:单个代码库和部署实例,简化维护和更新
- 可扩展性:可以轻松添加新租户,无需额外的部署工作
- 资源利用率:更高效地利用服务器资源
多租户架构的挑战
- 数据隔离:确保租户数据的安全性和隔离性
- 性能管理:防止单个租户影响其他租户的性能
- 定制化:在共享架构中满足不同租户的定制需求
- 扩展性:随着租户数量增长,系统需要保持可扩展
多租户架构的实现策略
1. 租户隔离策略
物理隔离
- 描述:为每个租户提供完全独立的应用实例和数据库
- 优势:最高级别的隔离和安全性,性能不会相互影响
- 劣势:成本高,资源利用率低,维护复杂
- 适用场景:大型企业客户,对安全性和性能有严格要求的场景
逻辑隔离
- 描述:多个租户共享应用实例,但使用不同的数据库或模式
- 优势:较好的隔离性,同时保持较低的成本
- 劣势:数据库管理复杂度增加
- 适用场景:中型客户,对隔离性有一定要求的场景
数据列隔离
- 描述:所有租户共享同一个数据库和表,通过租户ID列来区分数据
- 优势:成本最低,资源利用率最高
- 劣势:隔离性较差,需要严格的访问控制
- 适用场景:小型客户,数据敏感度较低的场景
2. 数据分离方案
方案一:每个租户一个数据库
// database.config.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
// 根据租户ID获取数据库配置
export const getDatabaseConfig = (tenantId: string): TypeOrmModuleOptions => {
return {
type: 'postgres',
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: `app_tenant_${tenantId}`, // 每个租户一个数据库
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
synchronize: false,
migrationsRun: true,
migrations: [__dirname + '/../migrations/*{.ts,.js}'],
};
};方案二:每个租户一个模式
// database.config.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
export const getDatabaseConfig = (tenantId: string): TypeOrmModuleOptions => {
return {
type: 'postgres',
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
schema: `tenant_${tenantId}`, // 每个租户一个模式
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
synchronize: false,
migrationsRun: true,
migrations: [__dirname + '/../migrations/*{.ts,.js}'],
};
};方案三:共享数据库和表,使用租户ID列
// user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
email: string;
@Column()
tenantId: string; // 租户ID列,用于区分不同租户的数据
// 其他字段...
}3. 租户识别机制
基于URL的租户识别
- 描述:通过URL路径或子域名来识别租户
- 示例:
https://tenant1.example.com或https://example.com/tenant1 - 实现方式:使用中间件或守卫解析URL中的租户信息
// tenant.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class TenantMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
// 从子域名获取租户ID
const hostname = req.hostname;
const parts = hostname.split('.');
if (parts.length > 2) {
const tenantId = parts[0];
(req as any).tenantId = tenantId;
}
// 或从路径获取租户ID
// const pathParts = req.path.split('/');
// if (pathParts.length > 1) {
// const tenantId = pathParts[1];
// (req as any).tenantId = tenantId;
// }
next();
}
}基于请求头的租户识别
- 描述:通过自定义请求头来识别租户
- 示例:
X-Tenant-ID: tenant1 - 实现方式:使用中间件或守卫解析请求头中的租户信息
// tenant.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class TenantMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const tenantId = req.headers['x-tenant-id'] as string;
if (tenantId) {
(req as any).tenantId = tenantId;
}
next();
}
}基于认证令牌的租户识别
- 描述:在JWT或其他认证令牌中包含租户信息
- 实现方式:在认证过程中解析令牌中的租户信息
// auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(private jwtService: JwtService) {}
generateToken(userId: number, tenantId: string) {
return this.jwtService.sign({
sub: userId,
tenantId: tenantId, // 在令牌中包含租户ID
});
}
validateToken(token: string) {
return this.jwtService.verify(token);
}
}4. 租户上下文管理
在 NestJS 应用中,我们可以使用请求作用域的提供者来管理租户上下文,确保每个请求都能访问到当前租户的信息。
// tenant-context.service.ts
import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class TenantContextService {
private _tenantId: string;
constructor(@Inject(REQUEST) private request: Request) {
this._tenantId = (request as any).tenantId;
}
get tenantId(): string {
return this._tenantId;
}
set tenantId(tenantId: string) {
this._tenantId = tenantId;
}
}实践案例:实现多租户应用
1. 项目初始化
# 创建新项目
nest new multi-tenant-app
# 安装依赖
cd multi-tenant-app
npm install @nestjs/typeorm typeorm pg @nestjs/jwt passport-jwt2. 配置模块
// tenant.module.ts
import { Module, Scope, RequestMethod } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TenantMiddleware } from './tenant.middleware';
import { TenantContextService } from './tenant-context.service';
import { getDatabaseConfig } from './database.config';
@Module({
imports: [
TypeOrmModule.forRootAsync({
useFactory: (tenantContext: TenantContextService) => {
return getDatabaseConfig(tenantContext.tenantId);
},
inject: [TenantContextService],
scope: Scope.REQUEST,
}),
],
providers: [TenantContextService],
exports: [TenantContextService],
})
export class TenantModule {
configure(consumer) {
consumer
.apply(TenantMiddleware)
.forRoutes({ path: '*', method: RequestMethod.ALL });
}
}3. 数据访问层
// user.repository.ts
import { Repository, EntityRepository } from 'typeorm';
import { User } from './user.entity';
import { TenantContextService } from './tenant-context.service';
@EntityRepository(User)
export class UserRepository extends Repository<User> {
constructor(private tenantContext: TenantContextService) {
super();
}
// 自动添加租户ID过滤
findAll() {
return this.find({ where: { tenantId: this.tenantContext.tenantId } });
}
findOneById(id: number) {
return this.findOne({ where: { id, tenantId: this.tenantContext.tenantId } });
}
createUser(user: Partial<User>) {
const newUser = this.create({
...user,
tenantId: this.tenantContext.tenantId,
});
return this.save(newUser);
}
}4. 业务逻辑层
// user.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { User } from './user.entity';
@Injectable()
export class UserService {
constructor(private userRepository: UserRepository) {}
async getUsers() {
return this.userRepository.findAll();
}
async getUserById(id: number) {
const user = await this.userRepository.findOneById(id);
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
async createUser(userData: Partial<User>) {
return this.userRepository.createUser(userData);
}
}5. 控制器
// user.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { UserService } from './user.service';
import { User } from './user.entity';
@Controller('users')
export class UserController {
constructor(private userService: UserService) {}
@Get()
async getUsers() {
return this.userService.getUsers();
}
@Get(':id')
async getUserById(@Param('id') id: number) {
return this.userService.getUserById(id);
}
@Post()
async createUser(@Body() userData: Partial<User>) {
return this.userService.createUser(userData);
}
}6. 主模块
// app.module.ts
import { Module } from '@nestjs/common';
import { TenantModule } from './tenant/tenant.module';
import { UserModule } from './user/user.module';
@Module({
imports: [TenantModule, UserModule],
})
export class AppModule {}多租户架构的最佳实践
1. 性能优化
- 数据库索引:为租户ID列创建索引,提高查询性能
- 查询优化:避免全表扫描,使用分页和过滤
- 缓存策略:实现租户级别的缓存,避免缓存冲突
- 连接池管理:合理配置数据库连接池,避免连接泄漏
2. 安全性
- 输入验证:严格验证所有用户输入,防止SQL注入等攻击
- 数据加密:对敏感数据进行加密存储
- 访问控制:实现细粒度的访问控制,确保租户只能访问自己的数据
- 审计日志:记录租户的关键操作,便于审计和故障排查
3. 可扩展性
- 模块化设计:采用模块化设计,便于功能扩展
- 异步处理:使用消息队列处理耗时操作,提高系统响应速度
- 水平扩展:设计支持水平扩展的架构,应对租户数量增长
4. 监控和维护
- 租户级监控:监控每个租户的使用情况和性能指标
- 健康检查:实现租户级别的健康检查,及时发现问题
- 备份策略:制定合理的备份策略,确保数据安全
- 故障隔离:实现故障隔离机制,防止单个租户的故障影响其他租户
常见问题与解决方案
1. 租户数据迁移
问题:如何在多租户架构中进行数据迁移?
解决方案:
- 对于每个租户一个数据库的方案:使用迁移工具为每个数据库执行迁移
- 对于共享数据库的方案:在迁移脚本中考虑租户ID列的影响
2. 租户数据清理
问题:如何安全地清理租户数据?
解决方案:
- 实现软删除机制,标记数据为已删除
- 定期执行清理任务,删除已标记的数据
- 对于每个租户一个数据库的方案:可以直接删除整个数据库
3. 租户配额管理
问题:如何限制单个租户的资源使用?
解决方案:
- 实现租户配额系统,限制存储、带宽等资源使用
- 监控租户的资源使用情况,及时发出警报
- 设计分级服务方案,根据租户等级提供不同的资源配额
总结
多租户架构是构建 SaaS 应用的重要技术方案,它可以帮助我们更高效地利用资源,降低运营成本,同时为多个客户提供个性化的服务。在 NestJS 应用中实现多租户架构,需要考虑租户隔离策略、数据分离方案、租户识别机制和上下文管理等多个方面。
通过本教程的学习,我们了解了:
- 多租户架构的基本概念和优势
- 不同的租户隔离策略及其适用场景
- 数据分离的三种主要方案
- 租户识别的多种实现方式
- 如何在 NestJS 应用中实现租户上下文管理
- 多租户架构的最佳实践和常见问题解决方案
通过合理的设计和实现,我们可以构建一个安全、高效、可扩展的多租户 NestJS 应用,为不同的客户提供优质的服务体验。
互动问题
你认为在什么场景下,物理隔离的多租户方案是必要的?
如何在共享数据库的多租户架构中,确保查询性能不受租户数量增长的影响?
除了本教程中介绍的租户识别方式,你还知道哪些其他的租户识别方法?
在多租户架构中,如何实现租户级别的自定义配置?
如何处理多租户架构中的数据备份和恢复问题?