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.comhttps://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-jwt

2. 配置模块

// 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 应用,为不同的客户提供优质的服务体验。

互动问题

  1. 你认为在什么场景下,物理隔离的多租户方案是必要的?

  2. 如何在共享数据库的多租户架构中,确保查询性能不受租户数量增长的影响?

  3. 除了本教程中介绍的租户识别方式,你还知道哪些其他的租户识别方法?

  4. 在多租户架构中,如何实现租户级别的自定义配置?

  5. 如何处理多租户架构中的数据备份和恢复问题?

« 上一篇 错误处理策略 下一篇 » NestJS 事件驱动架构设计与实现