多租户架构是SaaS系统的核心设计模式,允许多个租户共享同一套系统实例,同时保证数据隔离与资源独立。在IMS系统中,多租户设计需要在共享效率与隔离安全之间取得平衡,本文详细介绍三种隔离模型及其实战实现方案。
租户隔离模型
三种隔离策略
// 多租户隔离模型定义
const IsolationModel = {
// 模式1: 共享数据库,共享Schema(tenant_id字段隔离)
SHARED_SCHEMA: 'shared_schema',
// 模式2: 共享数据库,独立Schema(每个租户一个Schema)
SHARED_DB_ISOLATED_SCHEMA: 'shared_db_isolated_schema',
// 模式3: 独立数据库(每个租户一个数据库实例)
ISOLATED_DB: 'isolated_db'
};
// 租户配置
class TenantConfig {
constructor() {
this.tenants = new Map();
}
registerTenant(tenantId, config) {
this.tenants.set(tenantId, {
id: tenantId,
name: config.name,
isolation: config.isolation || IsolationModel.SHARED_SCHEMA,
database: config.database || null,
schema: config.schema || null,
quota: {
maxUsers: config.maxUsers || 100,
maxStorage: config.maxStorage || '10GB',
maxForms: config.maxForms || 50,
maxWorkflows: config.maxWorkflows || 20
},
features: config.features || [],
status: 'active',
createdAt: new Date()
});
}
getTenant(tenantId) {
return this.tenants.get(tenantId);
}
}
租户上下文管理
// 租户上下文 - 贯穿请求生命周期
class TenantContext {
static AsyncLocalStorage = new AsyncLocalStorage();
// 设置当前租户
static set(tenantId) {
const tenant = tenantConfig.getTenant(tenantId);
if (!tenant) throw new Error(`租户不存在: ${tenantId}`);
TenantContext.AsyncLocalStorage.run(
{ tenantId, tenant },
() => {}
);
}
// 获取当前租户ID
static getTenantId() {
const store = TenantContext.AsyncLocalStorage.getStore();
return store?.tenantId;
}
// 获取当前租户信息
static getTenant() {
const store = TenantContext.AsyncLocalStorage.getStore();
return store?.tenant;
}
}
// 中间件:自动识别租户
async function tenantMiddleware(ctx, next) {
let tenantId = null;
// 方式1: 从子域名提取
const subdomain = ctx.host.split('.')[0];
if (subdomain !== 'www' && subdomain !== 'app') {
tenantId = subdomain;
}
// 方式2: 从请求头提取
if (!tenantId) {
tenantId = ctx.get('X-Tenant-Id');
}
// 方式3: 从Token中提取
if (!tenantId && ctx.state.user) {
tenantId = ctx.state.user.tenantId;
}
if (!tenantId) {
ctx.throw(400, '无法识别租户');
}
// 验证租户
const tenant = tenantConfig.getTenant(tenantId);
if (!tenant || tenant.status !== 'active') {
ctx.throw(403, '租户不可用');
}
// 设置租户上下文
await new Promise(resolve => {
TenantContext.AsyncLocalStorage.run(
{ tenantId, tenant },
async () => {
await next();
resolve();
}
);
});
}
数据隔离实现
共享Schema模式
// 租户数据隔离 - 字段级隔离
class TenantIsolatedRepository {
constructor(model) {
this.model = model;
}
// 自动注入 tenant_id 查询条件
async find(query = {}) {
const tenantId = TenantContext.getTenantId();
return this.model.findAll({
where: {
...query,
tenant_id: tenantId // 强制加租户条件
}
});
}
// 创建时自动设置 tenant_id
async create(data) {
const tenantId = TenantContext.getTenantId();
return this.model.create({
...data,
tenant_id: tenantId
});
}
// 更新时限制租户范围
async update(id, data) {
const tenantId = TenantContext.getTenantId();
const [count] = await this.model.update(data, {
where: { id, tenant_id: tenantId }
});
if (count === 0) throw new Error('记录不存在或无权操作');
return count;
}
// 删除时限制租户范围
async delete(id) {
const tenantId = TenantContext.getTenantId();
const count = await this.model.destroy({
where: { id, tenant_id: tenantId }
});
if (count === 0) throw new Error('记录不存在或无权操作');
return count;
}
}
// 数据库表设计 - 所有表必须有 tenant_id
/*
* CREATE TABLE ims_users (
* id BIGINT PRIMARY KEY AUTO_INCREMENT,
* tenant_id BIGINT NOT NULL, -- 租户ID
* username VARCHAR(64) NOT NULL,
* email VARCHAR(128),
* department_id BIGINT,
* status TINYINT DEFAULT 1,
* created_at DATETIME DEFAULT NOW(),
* INDEX idx_tenant_id (tenant_id),
* UNIQUE KEY uk_tenant_username (tenant_id, username)
* );
*/
独立Schema模式
// 独立Schema - 数据库路由
class SchemaIsolationManager {
constructor() {
this.connections = new Map();
}
// 获取租户专属Schema连接
getConnection(tenantId) {
if (this.connections.has(tenantId)) {
return this.connections.get(tenantId);
}
const schemaName = `tenant_${tenantId}`;
const connection = createPool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
charset: 'utf8mb4',
// 设置默认Schema
initSql: [`SET search_path TO ${schemaName}`]
});
this.connections.set(tenantId, connection);
return connection;
}
// 创建租户Schema
async createSchema(tenantId) {
const schemaName = `tenant_${tenantId}`;
const conn = this.getAdminConnection();
// 创建Schema
await conn.query(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
// 执行初始化DDL
const initSQL = readFileSync('./sql/tenant_init.sql', 'utf8');
const statements = initSQL
.replace(/\$\{schema\}/g, schemaName)
.split(';')
.filter(s => s.trim());
for (const sql of statements) {
await conn.query(sql);
}
}
// 删除租户Schema
async dropSchema(tenantId) {
const schemaName = `tenant_${tenantId}`;
const conn = this.getAdminConnection();
await conn.query(`DROP SCHEMA IF EXISTS ${schemaName} CASCADE`);
this.connections.delete(tenantId);
}
}
租户路由
动态数据源路由
// 多租户数据源路由
class TenantDataSourceRouter {
constructor() {
this.dataSources = new Map();
this.defaultSource = null;
}
// 注册数据源
registerDataSource(tenantId, config) {
const dataSource = createDataSource(config);
this.dataSources.set(tenantId, dataSource);
}
// 路由到正确的数据源
route() {
const tenantId = TenantContext.getTenantId();
const tenant = tenantConfig.getTenant(tenantId);
switch (tenant.isolation) {
case IsolationModel.SHARED_SCHEMA:
// 共享数据源,查询时加 tenant_id 条件
return this.defaultSource;
case IsolationModel.SHARED_DB_ISOLATED_SCHEMA:
// Schema隔离
return schemaManager.getConnection(tenantId);
case IsolationModel.ISOLATED_DB:
// 独立数据库
return this.dataSources.get(tenantId);
default:
return this.defaultSource;
}
}
}
资源配额管理
租户资源限制
// 租户资源配额管理
class TenantQuotaManager {
constructor() {
this.usage = new Map(); // tenantId -> UsageStats
}
// 检查资源配额
async checkQuota(tenantId, resource, amount = 1) {
const tenant = tenantConfig.getTenant(tenantId);
const currentUsage = await this.getUsage(tenantId, resource);
const limits = {
users: tenant.quota.maxUsers,
storage: parseSize(tenant.quota.maxStorage),
forms: tenant.quota.maxForms,
workflows: tenant.quota.maxWorkflows
};
const limit = limits[resource];
if (limit === undefined) return true;
if (currentUsage + amount > limit) {
throw new QuotaExceededError(
`租户 ${tenantId} 的 ${resource} 配额已满`,
{ resource, current: currentUsage, limit }
);
}
return true;
}
// 获取当前用量
async getUsage(tenantId, resource) {
switch (resource) {
case 'users':
return await db.count('users', { tenant_id: tenantId });
case 'forms':
return await db.count('forms', { tenant_id: tenantId });
case 'workflows':
return await db.count('workflow_defs', { tenant_id: tenantId });
case 'storage':
return await this.calculateStorage(tenantId);
}
}
// 计算存储用量
async calculateStorage(tenantId) {
const files = await db.query(
'SELECT SUM(file_size) as total FROM files WHERE tenant_id = ?',
[tenantId]
);
return files[0].total || 0;
}
}
// 配额中间件
function quotaGuard(resource) {
return async (ctx, next) => {
const tenantId = TenantContext.getTenantId();
try {
await quotaManager.checkQuota(tenantId, resource);
await next();
} catch (err) {
if (err instanceof QuotaExceededError) {
ctx.throw(429, err.message);
}
throw err;
}
};
}
租户功能开关
功能特性控制
// 租户功能管理
class TenantFeatureManager {
constructor() {
this.featureDefinitions = new Map();
}
// 定义功能特性
defineFeature(featureId, config) {
this.featureDefinitions.set(featureId, {
id: featureId,
name: config.name,
description: config.description,
tier: config.tier, // 'basic' | 'professional' | 'enterprise'
dependencies: config.dependencies || []
});
}
// 检查租户是否拥有某功能
hasFeature(tenantId, featureId) {
const tenant = tenantConfig.getTenant(tenantId);
if (!tenant) return false;
// 检查是否显式启用
if (tenant.features.includes(featureId)) {
return true;
}
// 检查套餐等级
const feature = this.featureDefinitions.get(featureId);
const tierOrder = ['basic', 'professional', 'enterprise'];
const featureTierIndex = tierOrder.indexOf(feature.tier);
const tenantTierIndex = tierOrder.indexOf(tenant.tier);
return tenantTierIndex >= featureTierIndex;
}
// 功能守卫装饰器
requireFeature(featureId) {
return function(target, propertyKey, descriptor) {
const original = descriptor.value;
descriptor.value = async function(...args) {
const tenantId = TenantContext.getTenantId();
if (!featureManager.hasFeature(tenantId, featureId)) {
throw new FeatureNotAvailableError(featureId);
}
return original.apply(this, args);
};
return descriptor;
};
}
}
租户生命周期
租户开通与销毁
// 租户生命周期管理
class TenantLifecycleManager {
// 开通租户
async provision(tenantConfig) {
const tenantId = generateId();
// 1. 创建租户记录
await db.insert('tenants', {
id: tenantId,
name: tenantConfig.name,
isolation: tenantConfig.isolation,
tier: tenantConfig.tier,
status: 'provisioning'
});
// 2. 初始化数据存储
switch (tenantConfig.isolation) {
case IsolationModel.ISOLATED_DB:
await this.createTenantDatabase(tenantId);
break;
case IsolationModel.SHARED_DB_ISOLATED_SCHEMA:
await schemaManager.createSchema(tenantId);
break;
case IsolationModel.SHARED_SCHEMA:
// 无需额外操作
break;
}
// 3. 初始化基础数据
await this.initTenantData(tenantId, tenantConfig);
// 4. 创建管理员账号
await this.createAdminUser(tenantId, tenantConfig.admin);
// 5. 更新状态
await db.update('tenants',
{ status: 'active' },
{ id: tenantId }
);
return tenantId;
}
// 销毁租户
async deprovision(tenantId) {
const tenant = tenantConfig.getTenant(tenantId);
// 1. 标记为删除中
await db.update('tenants',
{ status: 'deleting' },
{ id: tenantId }
);
// 2. 清理数据存储
switch (tenant.isolation) {
case IsolationModel.ISOLATED_DB:
await this.dropTenantDatabase(tenantId);
break;
case IsolationModel.SHARED_DB_ISOLATED_SCHEMA:
await schemaManager.dropSchema(tenantId);
break;
case IsolationModel.SHARED_SCHEMA:
// 删除租户所有数据
await this.deleteTenantData(tenantId);
break;
}
// 3. 清理文件存储
await this.cleanupStorage(tenantId);
// 4. 清理缓存
await this.invalidateCache(tenantId);
// 5. 删除租户记录
await db.delete('tenants', { id: tenantId });
}
}
总结
- 三种隔离模型 - 共享Schema最轻量,独立数据库最安全,按需选择
- 租户上下文 - 基于AsyncLocalStorage实现请求级租户隔离
- 自动路由 - 根据租户隔离策略自动切换数据源
- 资源配额 - 限制每个租户的用户数、存储、表单等资源
- 功能开关 - 按套餐等级控制功能可用性
- 生命周期管理 - 自动化租户开通与销毁,确保数据完整性
多租户架构是SaaS产品的核心竞争力,合理的隔离策略与资源管控让IMS系统兼顾效率与安全。