multitenancy
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMulti-Tenant Architecture
多租户架构
Isolation Strategies
隔离策略
| Strategy | Isolation | Complexity | Cost |
|---|---|---|---|
| Database per tenant | Highest | High | High |
| Schema per tenant | High | Medium | Medium |
| Shared schema (tenant_id column) | Medium | Low | Low |
| Row-level security (RLS) | Medium-High | Medium | Low |
| 策略 | 隔离级别 | 复杂度 | 成本 |
|---|---|---|---|
| 单租户单数据库 | 最高 | 高 | 高 |
| 单租户单Schema | 高 | 中 | 中 |
| 共享Schema(含tenant_id列) | 中 | 低 | 低 |
| 行级安全(RLS) | 中高 | 中 | 低 |
Shared Schema with Tenant ID (most common)
带租户ID的共享Schema(最常用)
typescript
// Middleware: resolve tenant from subdomain or header
function tenantMiddleware(req: Request, res: Response, next: NextFunction) {
const host = req.hostname; // acme.myapp.com
const subdomain = host.split('.')[0];
const tenant = await tenantRepo.findBySubdomain(subdomain);
if (!tenant) return res.status(404).json({ error: 'Tenant not found' });
req.tenantId = tenant.id;
next();
}
// Always filter by tenant
app.get('/api/products', async (req, res) => {
const products = await db.product.findMany({
where: { tenantId: req.tenantId },
});
res.json(products);
});typescript
// Middleware: resolve tenant from subdomain or header
function tenantMiddleware(req: Request, res: Response, next: NextFunction) {
const host = req.hostname; // acme.myapp.com
const subdomain = host.split('.')[0];
const tenant = await tenantRepo.findBySubdomain(subdomain);
if (!tenant) return res.status(404).json({ error: 'Tenant not found' });
req.tenantId = tenant.id;
next();
}
// Always filter by tenant
app.get('/api/products', async (req, res) => {
const products = await db.product.findMany({
where: { tenantId: req.tenantId },
});
res.json(products);
});Prisma with Tenant Scoping
结合Prisma实现租户范围限定
typescript
// Extension to auto-apply tenant filter
const prisma = new PrismaClient().$extends({
query: {
$allOperations({ args, query, operation }) {
if (['findMany', 'findFirst', 'count', 'updateMany', 'deleteMany'].includes(operation)) {
args.where = { ...args.where, tenantId: getCurrentTenantId() };
}
if (['create', 'createMany'].includes(operation)) {
args.data = { ...args.data, tenantId: getCurrentTenantId() };
}
return query(args);
},
},
});typescript
// Extension to auto-apply tenant filter
const prisma = new PrismaClient().$extends({
query: {
$allOperations({ args, query, operation }) {
if (['findMany', 'findFirst', 'count', 'updateMany', 'deleteMany'].includes(operation)) {
args.where = { ...args.where, tenantId: getCurrentTenantId() };
}
if (['create', 'createMany'].includes(operation)) {
args.data = { ...args.data, tenantId: getCurrentTenantId() };
}
return query(args);
},
},
});PostgreSQL Row-Level Security
PostgreSQL行级安全(RLS)
sql
-- Enable RLS
ALTER TABLE products ENABLE ROW LEVEL SECURITY;
-- Policy: users see only their tenant's data
CREATE POLICY tenant_isolation ON products
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- Set tenant context per request
SET app.tenant_id = 'tenant-uuid-here';
SELECT * FROM products; -- auto-filteredtypescript
// Set tenant context on each request
pool.on('connect', async (client) => {
// Set after getting connection from pool
});
async function withTenant<T>(tenantId: string, fn: () => Promise<T>): Promise<T> {
const client = await pool.connect();
try {
await client.query(`SET app.tenant_id = $1`, [tenantId]);
return await fn();
} finally {
await client.query('RESET app.tenant_id');
client.release();
}
}sql
-- Enable RLS
ALTER TABLE products ENABLE ROW LEVEL SECURITY;
-- Policy: users see only their tenant's data
CREATE POLICY tenant_isolation ON products
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- Set tenant context per request
SET app.tenant_id = 'tenant-uuid-here';
SELECT * FROM products; -- auto-filteredtypescript
// Set tenant context on each request
pool.on('connect', async (client) => {
// Set after getting connection from pool
});
async function withTenant<T>(tenantId: string, fn: () => Promise<T>): Promise<T> {
const client = await pool.connect();
try {
await client.query(`SET app.tenant_id = $1`, [tenantId]);
return await fn();
} finally {
await client.query('RESET app.tenant_id');
client.release();
}
}Schema-Per-Tenant
单租户单Schema模式
typescript
// Dynamic schema selection
function getTenantSchema(tenantId: string): string {
return `tenant_${tenantId.replace(/-/g, '_')}`;
}
async function createTenantSchema(tenantId: string) {
const schema = getTenantSchema(tenantId);
await db.query(`CREATE SCHEMA IF NOT EXISTS ${schema}`);
await db.query(`SET search_path TO ${schema}`);
await runMigrations(); // Apply schema migrations
}typescript
// Dynamic schema selection
function getTenantSchema(tenantId: string): string {
return `tenant_${tenantId.replace(/-/g, '_')}`;
}
async function createTenantSchema(tenantId: string) {
const schema = getTenantSchema(tenantId);
await db.query(`CREATE SCHEMA IF NOT EXISTS ${schema}`);
await db.query(`SET search_path TO ${schema}`);
await runMigrations(); // Apply schema migrations
}Tenant Resolution Strategies
租户解析策略
| Strategy | Example | Best For |
|---|---|---|
| Subdomain | | B2B SaaS |
| Path prefix | | Simpler setup |
| Custom header | | API-first |
| JWT claim | | Authenticated APIs |
| 策略 | 示例 | 最佳适用场景 |
|---|---|---|
| 子域名 | | B2B SaaS |
| 路径前缀 | | 简单部署场景 |
| 自定义请求头 | | API优先场景 |
| JWT声明 | | 已认证API场景 |
Anti-Patterns
反模式
| Anti-Pattern | Fix |
|---|---|
| No tenant filter on queries | Use middleware or ORM extension to auto-apply |
| Tenant ID from client without validation | Derive from auth token or subdomain |
| No tenant data isolation testing | Write tests that verify cross-tenant isolation |
| Shared cache without tenant prefix | Prefix all cache keys with tenant ID |
| No tenant-aware rate limiting | Rate limit per tenant, not globally |
| 反模式 | 修复方案 |
|---|---|
| 查询未添加租户过滤 | 使用中间件或ORM扩展自动应用租户过滤 |
| 直接使用客户端传入的未验证租户ID | 从认证Token或子域名中获取租户信息 |
| 未进行租户数据隔离测试 | 编写自动化测试验证跨租户数据隔离 |
| 未带租户前缀的共享缓存 | 所有缓存键添加租户ID前缀 |
| 未按租户进行限流 | 按租户维度限流,而非全局限流 |
Production Checklist
生产环境检查清单
- Tenant resolution middleware on all routes
- Data isolation verified with automated tests
- Cache keys prefixed with tenant ID
- Rate limiting per tenant
- Tenant-scoped background jobs
- Tenant provisioning and deprovisioning flow
- Cross-tenant query prevention (RLS or ORM enforcement)
- 所有路由均配置租户解析中间件
- 通过自动化测试验证数据隔离
- 缓存键添加租户ID前缀
- 按租户维度进行限流
- 租户范围的后台任务
- 租户创建与销毁流程
- 防止跨租户查询(通过RLS或ORM强制实现)