2. 多租户架构基础:多租户概念、隔离模式与租户上下文管理

2.1 多租户核心概念

多租户(Multi-Tenancy)是一种软件架构模式,允许单个应用实例同时服务多个租户,每个租户拥有独立的逻辑视图与数据边界。在计费模型中,租户通常对应一个企业客户或组织,其内部的用户、订阅、用量与账单数据必须严格隔离。

关键定义: 租户 ≠ 用户。一个租户包含多个用户,共享同一套计费策略、定价计划与资源配额。
术语 说明 计费场景示例
租户 (Tenant) 独立的组织或客户实体 某企业客户“Acme Corp”
租户管理员 拥有租户级管理权限的用户 可查看本租户账单、修改订阅
租户用户 隶属于租户的普通用户 消耗 API 调用次数,触发计费事件
租户上下文 标识当前请求所属租户的信息载体 JWT 中的 tenant_id 声明

2.2 三种隔离模式对比

选择隔离模式直接影响计费数据的安全性、查询性能与运维复杂度。以下是三种主流模式的详细对比:

隔离模式 数据存储方式 隔离强度 计费场景适用性 运维成本
数据库隔离 每个租户独占一个物理/虚拟数据库 最高 金融级计费、合规要求严格的行业 高(备份、迁移、连接管理)
Schema 隔离 共享数据库,每个租户独占一个 Schema 高 中大型 SaaS,需要独立表结构扩展 中(Schema 迁移需逐租户执行)
表隔离 共享数据库与 Schema,通过 tenant_id 列区分 低 标准化计费模型,租户量级大(万级) 低(需防范 SQL 注入与越权)
架构建议: 计费系统的核心表(如 billing_invoices、usage_records)建议采用表隔离 + 强制租户过滤,配合数据库级 Row-Level Security(RLS)策略,兼顾扩展性与安全性。

2.3 隔离模式架构图

下图展示了三种隔离模式在数据库层的物理与逻辑布局:

多租户三种隔离模式架构对比 多租户隔离模式架构对比 数据库隔离 租户 A 数据库 独立实例 / 独立连接 租户 B 数据库 独立实例 / 独立连接 租户 C 数据库 独立实例 / 独立连接 Schema 隔离 租户 A Schema 共享数据库实例 租户 B Schema 共享数据库实例 租户 C Schema 共享数据库实例 表隔离 billing_invoices tenant_id 列过滤 usage_records tenant_id 列过滤 subscriptions tenant_id 列过滤 隔离强度:数据库隔离 > Schema 隔离 > 表隔离 | 运维成本:数据库隔离 > Schema 隔离 > 表隔离

2.4 租户上下文管理

租户上下文是贯穿整个请求链路的身份标识。在计费接口中,每一次用量上报、账单查询都必须携带正确的租户上下文,否则会导致数据泄露或计费错误。

2.4.1 上下文传递链路

  1. 客户端请求:在 HTTP Header 中携带 X-Tenant-ID 或 JWT 中的 tenant_id 声明。
  2. API 网关/反向代理:解析并校验租户 ID 的合法性,拒绝无效或格式错误的请求。
  3. 鉴权拦截器:从请求中提取租户上下文,注入到当前线程的 TenantContext 对象中。
  4. 业务服务层:从 TenantContext 获取租户 ID,用于数据查询过滤、计费策略路由。
  5. 数据访问层:在 SQL 查询中自动追加 WHERE tenant_id = ? 条件,或通过 RLS 策略强制过滤。
常见陷阱: 异步任务(如消息队列、定时任务)中容易丢失租户上下文。解决方案是在消息体中显式携带 tenant_id,并在消费端重新绑定上下文。

2.4.2 上下文管理代码示例

以下是一个基于 ThreadLocal 的租户上下文管理实现(Java 示例):

public class TenantContext {
    private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();

    public static void setTenantId(String tenantId) {
        if (tenantId == null || tenantId.isBlank()) {
            throw new IllegalArgumentException("tenantId must not be null or blank");
        }
        CURRENT_TENANT.set(tenantId);
    }

    public static String getTenantId() {
        String tenantId = CURRENT_TENANT.get();
        if (tenantId == null) {
            throw new IllegalStateException("Tenant context not set for current thread");
        }
        return tenantId;
    }

    public static void clear() {
        CURRENT_TENANT.remove();
    }
}

2.4.3 上下文传递流程图

下图展示了从 HTTP 请求到数据库查询的完整租户上下文传递链路:

租户上下文传递链路 租户上下文传递链路 客户端请求 API 网关 鉴权拦截器 业务服务 数据访问层 数据库 上下文传递关键动作 1. 客户端:Header 携带 X-Tenant-ID 或 JWT 2. 网关:校验租户 ID 合法性,拒绝无效请求 3. 拦截器:提取 tenant_id 注入 TenantContext

2.5 隔离模式选型决策表

在计费系统设计阶段,可参考以下决策矩阵选择隔离模式:

决策条件 推荐模式 理由
租户数量 < 100,合规要求极高 数据库隔离 物理隔离,审计日志独立,满足 SOC2/PCI-DSS
租户数量 100~1000,需要定制表结构 Schema 隔离 逻辑隔离,支持按租户扩展字段
租户数量 > 1000,计费模型标准化 表隔离 + RLS 运维成本低,水平扩展容易
混合场景(部分大租户 + 大量小租户) 混合模式 大租户独享数据库,小租户共享表
实践要点: 无论选择哪种隔离模式,租户上下文必须在所有计费接口中强制校验。建议在 API 网关层统一注入 tenant_id,并在业务层通过 AOP 或中间件自动绑定,避免开发人员遗漏。