IMS系统多租户架构设计完全指南

架构设计多租户

多租户架构是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系统兼顾效率与安全。