Skip to content

多租户隔离:身份、数据权限、资源配额、缓存、MQ与数据库治理

多租户隔离解决的是“多个客户、医院、企业、部门共用一套系统时,怎样保证数据不串、资源不互相拖垮、故障不扩大、审计能说清楚”。它不是简单在表里加一个 tenant_id 字段,也不是只靠前端隐藏按钮。

相关基础可继续阅读:网关治理依赖治理容量规划分布式限流MySQL分库分表

学习目标

学完本页要能回答:

  1. 多租户隔离分为哪些层:身份、数据、资源、缓存、消息、数据库、运维。
  2. 为什么 Gateway 鉴权后,服务层和 SQL 仍必须带租户条件。
  3. tenant_id 应该怎样透传、校验、存储和审计。
  4. 热租户为什么会拖垮其他租户,怎样做资源配额和隔离。
  5. Redis Key、MQ Topic/Consumer、线程池、连接池怎样避免租户互相污染。
  6. 共享库、独立 Schema、独立库、独立集群怎样选。
  7. 出现租户串数据、热点租户、配额误伤时怎么排查。

一、多租户隔离不是一个字段

mermaid
flowchart TD
    A["多租户系统"] --> B["身份隔离"]
    A --> C["数据隔离"]
    A --> D["资源隔离"]
    A --> E["缓存隔离"]
    A --> F["消息隔离"]
    A --> G["数据库隔离"]
    A --> H["审计和运维隔离"]

只在表里加 tenant_id 解决不了这些问题:

问题例子
身份伪造外部请求自己传 X-Tenant-Id=vip
SQL漏条件查询资产列表忘了加 tenant_id
缓存串租户Redis key 只用 asset:1001,不同租户资产ID相同
热租户拖垮全站大医院批量采集占满线程池和DB连接
MQ基数爆炸每个租户动态创建 Topic 或 Binding
报表拖垮在线库某租户全量导出影响其他租户
审计不清不知道谁访问了哪个租户的数据

多租户设计的核心是:每一层都显式知道当前租户,并按风险做隔离和限额

二、租户身份从哪里来

租户身份必须来自可信认证链路,而不是直接信任客户端 Header。

mermaid
flowchart TD
    A["客户端请求"] --> B["Gateway校验Token"]
    B --> C["从Token或权限中心确定tenantId"]
    C --> D["删除外部伪造租户Header"]
    D --> E["写入可信X-Tenant-Id"]
    E --> F["服务端再次校验Token或签名上下文"]
    F --> G["Service层做数据权限判断"]

常见来源:

来源适合场景注意点
JWT Claim用户登录态携带租户要校验签名、issuer、audience、过期
权限中心查询用户可属于多个租户要明确当前选择的租户
子域名tenant-a.example.com仍要和登录身份绑定校验
路径参数/tenants/{tenantId}/assets不能只信路径,要校验用户是否属于该租户
服务账号批处理、定时任务scope 和租户范围必须最小化

错误做法:

  1. 前端传什么租户,后端就信什么。
  2. Gateway 校验后,下游服务完全不校验。
  3. Feign 透传所有 Header,导致外部伪造内部身份。
  4. 日志、Trace、审计不记录租户,事故后查不到影响面。

三、租户上下文如何透传

HTTP/Feign/RPC 调用中,租户上下文要白名单透传。

mermaid
flowchart TD
    A["Gateway生成可信上下文"] --> B["写入tenantId、userId、traceId"]
    B --> C["Feign拦截器白名单透传"]
    C --> D["下游入口校验来源"]
    D --> E["写入TenantContext"]
    E --> F["Service和Repository读取"]
    F --> G["finally清理ThreadLocal"]

JDK 8 最小 Demo:

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

        static void set(String tenantId) {
            if (tenantId == null || tenantId.length() == 0) {
                throw new IllegalArgumentException("tenantId required");
            }
            TENANT.set(tenantId);
        }

        static String getRequired() {
            String tenantId = TENANT.get();
            if (tenantId == null) {
                throw new IllegalStateException("tenant context missing");
            }
            return tenantId;
        }

        static void clear() {
            TENANT.remove();
        }
    }

    public static void main(String[] args) {
        try {
            TenantContext.set("tenant-a");
            System.out.println("current=" + TenantContext.getRequired());
        } finally {
            TenantContext.clear();
        }
    }
}

注意:ThreadLocal 必须在 finally 清理。线程池复用线程,如果不清理,下一次请求可能继承上一个租户,造成严重串租户。

四、数据权限和 SQL 隔离

多租户数据权限至少要在 Service 层和数据访问层双重兜底。

mermaid
flowchart TD
    A["请求查询资产"] --> B["Service校验用户属于租户"]
    B --> C["校验医院、科室、资源归属"]
    C --> D["SQL强制带tenant_id"]
    D --> E["数据库索引按tenant_id开头"]
    E --> F["返回结果并记录审计"]

典型 SQL:

sql
select id, asset_name, status
from asset
where tenant_id = ?
  and hospital_id = ?
  and status = ?
order by created_at desc
limit 20;

索引建议:

sql
create index idx_asset_tenant_hospital_status_created
on asset(tenant_id, hospital_id, status, created_at);

为什么 tenant_id 常放在索引前部?

  1. 几乎所有租户内查询都带租户边界。
  2. 先缩小到租户范围,再按业务条件过滤。
  3. 避免扫描其他租户数据。
  4. 有利于权限和性能一起稳定。

但如果某个租户特别大,单靠 tenant_id 仍然可能扫描很多数据,要继续加医院、状态、时间、游标分页、分表或大租户独立隔离。

五、资源隔离和热租户治理

热租户是多租户系统最常见的稳定性风险:一个大客户流量、批处理、导出或采集任务异常,拖垮共享线程池、数据库连接池、Redis、MQ,最后影响所有租户。

mermaid
flowchart TD
    A["热租户突增请求"] --> B["占满入口并发"]
    B --> C["占满业务线程池"]
    C --> D["占满DB连接池"]
    D --> E["其他租户请求排队超时"]

治理手段:

层级做法
Gateway按租户、接口、IP、用户限流
服务内按租户并发、线程池、信号量隔离
连接池给关键下游设置租户或业务维度预算
MQ按租户限速、分区 key、重试隔离
数据库大租户独立库/表/分区或读写隔离
任务批处理按租户分片、限速、错峰
监控按租户统计 QPS、P99、错误率、资源占用

租户限流不能只配置一个全局 QPS。要区分:

类型示例说明
全局入口限流全站最大 10000 QPS保护系统总容量
租户配额租户A最多 1000 QPS防止热租户拖垮其他租户
接口配额导出接口每租户 5 QPS高成本接口单独保护
并发配额每租户最多 20 个导出任务防止慢任务占资源
下游配额每租户最多 10 个医院接口并发保护第三方或医院接口

六、缓存 Key 隔离

Redis Key 必须包含租户边界。

错误 Key风险
asset:1001不同租户资产ID相同会串数据
user:permissions:2001用户ID跨租户复用时权限串
dict:department:list不同租户字典不同却共用缓存

推荐 Key:

text
asset:{tenantId}:{assetId}
user:permissions:{tenantId}:{userId}
dict:department:{tenantId}:{version}
rate:tenant:{tenantId}:{api}:{second}

还要注意大 Key 和热 Key:

  1. 不要把一个租户所有资产塞进一个 Hash。
  2. 大租户数据按表、科室、页码、时间、分片拆 Key。
  3. 热租户缓存击穿要用互斥、逻辑过期、预热和限流。
  4. 本地缓存也要包含租户维度,并支持按租户失效。

七、MQ 隔离

MQ 不建议为无限租户动态创建无限 Topic。Topic、Queue、Binding 是 Broker 资源,不是普通字符串。

常见模式:

模式适合场景风险
共享 Topic,消息带 tenantId多数中小租户热租户可能影响同分区
按租户分区 key保证同租户局部顺序大租户可能单分区热点
大租户独立 Topic头部客户隔离运维复杂度上升
死信按业务类型隔离方便恢复不要按无限租户建死信

消费者处理时:

  1. 消息必须带 tenantId。
  2. 消费日志去重维度要包含消费者和 eventId。
  3. 下游写库 SQL 必须带 tenant_id。
  4. 热租户消费慢时,要看分区、单条耗时、下游 DB 和重试。
  5. 毒消息不要阻塞所有租户,进入死信后按租户定位影响面。

八、数据库隔离模型

模型优点缺点适合
共享库共享表成本低、开发简单隔离弱,SQL必须严控tenant_id中小租户、多数SaaS早期
共享库独立Schema逻辑隔离更强迁移和连接管理复杂租户数量中等
独立库隔离强,可单独备份恢复成本和运维复杂大客户、合规要求高
独立集群故障域完全隔离成本最高超大租户、金融/医疗核心

选型不是越隔离越好,而是看租户规模、合规、成本、运维能力、故障影响面和迁移路径。常见做法是:普通租户共享表,头部大租户单独库或单独集群。

九、商业场景:医疗数据采集平台

需求:同一平台服务多家医院,医院接口质量、数据量、采集窗口不同。

设计:

  1. Gateway 根据登录用户或服务账号确定医院/租户。
  2. 采集任务按 tenantId + hospitalId + sourceId + batchNo 幂等。
  3. 每家医院有独立采集并发、超时、重试和熔断配置。
  4. 大医院采集任务进入独立线程池或队列。
  5. 数据落库所有表带 tenant_idhospital_id
  6. Redis 缓存 key 带租户和医院。
  7. MQ 消息带 tenantId,消费者写库再次校验。
  8. ES 文档带 tenantId,查询必须过滤租户。
  9. 审计日志记录用户、租户、医院、接口、traceId。

十、生产 Runbook

10.1 怀疑租户串数据

  1. 先拿到 traceId、userId、tenantId、资源ID。
  2. 查 Gateway 是否清洗并重写租户 Header。
  3. 查 Feign/RPC 是否白名单透传租户。
  4. 查 Service 层是否校验资源归属。
  5. 查 SQL 是否缺少 tenant_id 条件。
  6. 查 Redis Key 是否缺少租户维度。
  7. 查 ES 查询 DSL 是否带 tenant filter。
  8. 查审计日志确认影响范围。

10.2 某个租户拖垮全站

  1. 按租户看 QPS、P99、错误率和接口 TopN。
  2. 看该租户是否占满线程池、连接池、MQ 分区或 Redis 热 Key。
  3. 临时降低该租户入口限流和高成本接口并发。
  4. 对弱依赖降级,保护其他租户核心链路。
  5. 批处理任务暂停、限速或迁移到独立队列。
  6. 复盘是否需要大租户独立库、独立 Topic 或独立集群。

10.3 租户限流误伤

  1. 查限流 key 是否取错,例如 NAT IP 被当成用户。
  2. 查租户识别是否为空,导致多个租户落到同一个 key。
  3. 查 Gateway、服务内、Sentinel 是否多层重复限流。
  4. 查配置发布版本和实例是否一致。
  5. 查是否热点接口应该单独限流,而不是限制全租户。

十一、面试标准回答

多租户隔离不是只加 tenant_id 字段,而是身份、数据、资源、缓存、消息、数据库和审计的全链路隔离。入口由 Gateway 校验 Token,删除外部伪造租户 Header,并写入可信租户上下文;下游服务仍要校验 Token 或受信任上下文,在 Service 层判断用户是否有该租户、医院、科室或资源权限;SQL 必须强制带 tenant_id,索引也要贴合租户查询。资源上要按租户做限流、并发、线程池、连接池、MQ和批处理配额,防止热租户拖垮其他租户。缓存 Key、消息、ES 文档、审计日志都必须包含租户维度。

十二、本章小结

多租户系统的正确性来自“每一层都不默认信任上一层”。Gateway 是第一道门,Service 是业务裁决,数据库是最终约束,缓存和消息都要带租户维度,资源池要能防止热租户放大。只要有一层漏了租户边界,就可能出现串数据、越权或全站雪崩。