多租户架构设计
SaaS产品的架构基础
🎯学习目标
- 1理解多租户架构的核心概念
- 2掌握数据隔离的实现方式
- 3学会多租户系统的资源管理
开篇:为什么需要多租户
如果你的AI产品要服务多个企业客户,每个客户的数据需要隔离。多租户架构就是解决这个问题的。
本节课讨论如何设计一个安全、高效的多租户系统。
多租户架构概述
**什么是多租户?** 一个应用实例服务多个客户(租户),每个租户的数据和配置相互隔离。
**三种隔离模型**:
**1. 独立数据库** - 每个租户一个数据库 - 隔离性最强 - 成本最高
**2. 共享数据库,独立Schema** - 共享数据库实例,每个租户一个Schema - 隔离性中等 - 成本适中
**3. 共享数据库,共享Schema** - 通过租户ID区分数据 - 隔离性最弱 - 成本最低
**选择依据**: - 安全要求高的选独立数据库 - 成本敏感的选共享Schema - 大部分SaaS选择共享Schema+租户ID
💡 多租户三种隔离模型:独立数据库、独立Schema、共享Schema,各有取舍。
数据隔离实现
**共享Schema方案实现**:
**1. 数据库层面**: - 每个表添加tenant_id字段 - 创建复合索引:(tenant_id, id) - 查询必须带tenant_id条件
**2. 应用层面**: - 请求上下文传递租户信息 - 中间件自动注入租户条件 - ORM层自动过滤
**3. 安全层面**: - API层验证租户身份 - 越权访问检测 - 数据导出审核
**关键原则**: - 永远不要相信客户端传的tenant_id - 从认证Token中提取租户信息 - 所有查询必须带租户过滤
代码示例:多租户中间件
Express中间件自动注入租户过滤:
// 多租户中间件
function tenantMiddleware(req, res, next) {
// 从JWT Token中获取租户ID(不信任客户端)
const token = req.headers.authorization?.split(' ')[1];
const decoded = verifyToken(token);
if (!decoded.tenantId) {
return res.status(401).json({ error: 'Tenant not found' });
}
// 注入到请求上下文
req.tenantId = decoded.tenantId;
next();
}
// 数据库查询封装
class TenantAwareRepository {
constructor(model, tenantId) {
this.model = model;
this.tenantId = tenantId;
}
async findAll(filter = {}) {
// 自动添加租户过滤
return this.model.findAll({
where: {
...filter,
tenantId: this.tenantId, // 关键:自动注入
},
});
}
async create(data) {
// 自动添加租户ID
return this.model.create({
...data,
tenantId: this.tenantId, // 关键:自动注入
});
}
}
// 使用示例
app.get('/api/documents', tenantMiddleware, async (req, res) => {
const repo = new TenantAwareRepository(Document, req.tenantId);
const docs = await repo.findAll();
res.json(docs);
});资源配额管理
**为什么需要配额?** 防止单个租户占用过多资源,影响其他租户。
**配额维度**:
**1. 调用量限制** - 每日/每月API调用次数 - 每分钟请求频率(Rate Limit) - 并发请求限制
**2. 数据量限制** - 文档数量上限 - 向量存储容量 - 数据库存储空间
**3. 功能限制** - 可用模型(大模型额外付费) - 高级功能(如自定义Agent) - 导出数据量
**实现方式**: - Redis计数器:实时统计调用量 - 配置表:存储租户配额 - 中间件:配额检查和拒绝
代码示例:配额检查中间件
实现API调用配额限制:
import Redis from 'ioredis';
const redis = new Redis();
// 配额检查中间件
function quotaMiddleware(quotaConfig) {
return async (req, res, next) => {
const tenantId = req.tenantId;
const today = new Date().toISOString().split('T')[0];
// 1. 检查日配额
const dailyKey = `quota:${tenantId}:daily:${today}`;
const dailyCount = await redis.incr(dailyKey);
await redis.expire(dailyKey, 86400); // 24小时过期
if (dailyCount > quotaConfig.dailyLimit) {
return res.status(429).json({
error: 'Daily quota exceeded',
limit: quotaConfig.dailyLimit,
used: dailyCount,
});
}
// 2. 检查频率限制(每分钟)
const minuteKey = `quota:${tenantId}:minute:${Date.now() / 60000 | 0}`;
const minuteCount = await redis.incr(minuteKey);
await redis.expire(minuteKey, 60);
if (minuteCount > quotaConfig.rateLimit) {
return res.status(429).json({
error: 'Rate limit exceeded',
limit: quotaConfig.rateLimit,
});
}
next();
};
}
// 使用
app.get('/api/chat',
tenantMiddleware,
quotaMiddleware({ dailyLimit: 1000, rateLimit: 10 }),
chatHandler
);实战:AI SaaS的多租户方案
**场景**:企业知识库SaaS
**架构设计**:
**数据隔离**: - 向量数据库:Pinecone namespace(每个租户一个命名空间) - 文档存储:共享S3,路径区分(/tenant-{id}/) - 元数据存储:共享数据库,tenant_id字段
**配额设计**: - 基础版:1000次查询/天,1000文档 - 专业版:10000次查询/天,10000文档 - 企业版:无限制,按量付费
**安全设计**: - JWT认证,租户ID在Token中 - API Key绑定租户 - 所有查询自动注入租户过滤 - 管理后台跨租户操作需审批
合规考虑
某些行业(医疗、金融)要求数据物理隔离,必须选择独立数据库方案。多租户架构选择需要考虑合规要求。
📝课后小结
多租户架构有三种隔离模型:独立数据库、独立Schema、共享Schema。共享Schema方案成本最低但需要严格的租户过滤。配额管理防止资源滥用。架构选择需要考虑合规要求。
✓课后练习
多租户架构中,最经济的隔离方案是?
答案:共享Schema+租户ID
共享Schema通过tenant_id区分数据,资源利用率最高,成本最低。
为什么租户ID不能信任客户端传入?
答案:客户端可能伪造,导致数据越权访问
恶意用户可能修改tenant_id访问其他租户数据,必须从认证Token中安全获取租户信息。