跳到主要内容

设计多租户数据库

问题

SaaS 系统如何设计多租户的数据库架构?

答案

一、多租户隔离方案

方案隔离级别成本运维复杂度适合场景
独立数据库最强最高高(N 个库)大客户、合规要求高
独立 Schema中型 SaaS
共享表 + tenant_id最低小型 SaaS、大量租户
共享表 + RLS中偏强PostgreSQL 项目

二、共享表方案(最常用)

-- 所有表都有 tenant_id 列
CREATE TABLE users (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL, -- 租户 ID
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL,
INDEX idx_tenant_id (tenant_id),
UNIQUE KEY uk_tenant_email (tenant_id, email) -- 租户内唯一
);

CREATE TABLE orders (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
amount DECIMAL(10, 2),
INDEX idx_tenant_id (tenant_id)
);

应用层强制租户隔离

// 中间件:从认证信息中提取 tenant_id
@Injectable()
class TenantMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const tenantId = req.headers['x-tenant-id'] || req.user?.tenantId;
if (!tenantId) throw new ForbiddenException('Missing tenant');
req.tenantId = tenantId;
next();
}
}

// Repository:自动添加 tenant_id 条件
class BaseRepository<T> {
async findAll(tenantId: number, where?: Partial<T>): Promise<T[]> {
return this.repo.find({
where: { tenantId, ...where } // 强制 tenant_id
});
}

async create(tenantId: number, data: Partial<T>): Promise<T> {
return this.repo.save({ tenantId, ...data });
}
}
安全关键

必须在所有查询中包含 tenant_id 条件。忘记添加 WHERE tenant_id = ? 会导致数据泄露(租户 A 看到租户 B 的数据)。

最佳实践:

  1. 使用 ORM 的全局 Scope/Filter 自动注入 tenant_id
  2. 在 SQL 审计中检测缺少 tenant_id 的查询
  3. 数据库层使用 RLS 双重保护

三、PostgreSQL RLS 方案

-- 启用 RLS
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

-- 策略:只能访问自己租户的数据
CREATE POLICY tenant_isolation ON users
USING (tenant_id = current_setting('app.tenant_id')::BIGINT);

-- 同样为所有表添加策略
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id')::BIGINT);

-- 应用连接时设置租户上下文
SET app.tenant_id = '42';
SELECT * FROM users; -- 自动只返回 tenant_id = 42 的数据

四、独立数据库方案

// 动态数据源路由
class TenantDataSourceManager {
private dataSources = new Map<string, DataSource>();

async getDataSource(tenantId: string): Promise<DataSource> {
if (this.dataSources.has(tenantId)) {
return this.dataSources.get(tenantId)!;
}

// 从租户配置中获取连接信息
const config = await this.getTenantConfig(tenantId);
const ds = new DataSource({
type: 'mysql',
host: config.host,
database: config.database,
username: config.username,
password: config.password,
});
await ds.initialize();
this.dataSources.set(tenantId, ds);
return ds;
}
}

五、方案选型建议

租户数量 < 10,且有合规要求 → 独立数据库
租户数量 10~100 → 独立 Schema
租户数量 100~10000 → 共享表 + tenant_id
使用 PostgreSQL → 共享表 + RLS

常见面试问题

Q1: 共享表方案如何防止数据泄露?

答案

多层防御:

  1. 应用层:BaseRepository 自动注入 tenant_id
  2. ORM 层:全局 Scope/Filter(如 TypeORM 的 QueryBuilder 拦截器)
  3. 数据库层:PostgreSQL RLS 或 MySQL 视图
  4. 测试层:集成测试验证跨租户访问被拒绝
  5. 审计层:SQL 日志审计,检测缺少 tenant_id 的查询

Q2: 共享表方案的性能如何保证?

答案

  1. 索引:所有查询条件都要包含 (tenant_id, ...) 的联合索引
  2. 分区:大表按 tenant_id 做 RANGE/LIST 分区
  3. 大租户隔离:超大租户迁移到独立数据库
  4. 资源限制:per-tenant 连接数和查询并发限制

Q3: 如何实现租户级别的数据备份和恢复?

答案

方案独立数据库共享表
备份直接备份该库导出 WHERE tenant_id = ?
恢复直接恢复该库需要处理 ID 冲突
复杂度简单复杂(需要关联表一起操作)

相关链接