多租户隔离:身份、数据权限、资源配额、缓存、MQ与数据库治理
多租户隔离解决的是“多个客户、医院、企业、部门共用一套系统时,怎样保证数据不串、资源不互相拖垮、故障不扩大、审计能说清楚”。它不是简单在表里加一个 tenant_id 字段,也不是只靠前端隐藏按钮。
相关基础可继续阅读:网关治理、依赖治理、容量规划、分布式限流、MySQL分库分表。
学习目标
学完本页要能回答:
- 多租户隔离分为哪些层:身份、数据、资源、缓存、消息、数据库、运维。
- 为什么 Gateway 鉴权后,服务层和 SQL 仍必须带租户条件。
tenant_id应该怎样透传、校验、存储和审计。- 热租户为什么会拖垮其他租户,怎样做资源配额和隔离。
- Redis Key、MQ Topic/Consumer、线程池、连接池怎样避免租户互相污染。
- 共享库、独立 Schema、独立库、独立集群怎样选。
- 出现租户串数据、热点租户、配额误伤时怎么排查。
一、多租户隔离不是一个字段
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。
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 和租户范围必须最小化 |
错误做法:
- 前端传什么租户,后端就信什么。
- Gateway 校验后,下游服务完全不校验。
- Feign 透传所有 Header,导致外部伪造内部身份。
- 日志、Trace、审计不记录租户,事故后查不到影响面。
三、租户上下文如何透传
HTTP/Feign/RPC 调用中,租户上下文要白名单透传。
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:
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 层和数据访问层双重兜底。
flowchart TD
A["请求查询资产"] --> B["Service校验用户属于租户"]
B --> C["校验医院、科室、资源归属"]
C --> D["SQL强制带tenant_id"]
D --> E["数据库索引按tenant_id开头"]
E --> F["返回结果并记录审计"]典型 SQL:
select id, asset_name, status
from asset
where tenant_id = ?
and hospital_id = ?
and status = ?
order by created_at desc
limit 20;索引建议:
create index idx_asset_tenant_hospital_status_created
on asset(tenant_id, hospital_id, status, created_at);为什么 tenant_id 常放在索引前部?
- 几乎所有租户内查询都带租户边界。
- 先缩小到租户范围,再按业务条件过滤。
- 避免扫描其他租户数据。
- 有利于权限和性能一起稳定。
但如果某个租户特别大,单靠 tenant_id 仍然可能扫描很多数据,要继续加医院、状态、时间、游标分页、分表或大租户独立隔离。
五、资源隔离和热租户治理
热租户是多租户系统最常见的稳定性风险:一个大客户流量、批处理、导出或采集任务异常,拖垮共享线程池、数据库连接池、Redis、MQ,最后影响所有租户。
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:
asset:{tenantId}:{assetId}
user:permissions:{tenantId}:{userId}
dict:department:{tenantId}:{version}
rate:tenant:{tenantId}:{api}:{second}还要注意大 Key 和热 Key:
- 不要把一个租户所有资产塞进一个 Hash。
- 大租户数据按表、科室、页码、时间、分片拆 Key。
- 热租户缓存击穿要用互斥、逻辑过期、预热和限流。
- 本地缓存也要包含租户维度,并支持按租户失效。
七、MQ 隔离
MQ 不建议为无限租户动态创建无限 Topic。Topic、Queue、Binding 是 Broker 资源,不是普通字符串。
常见模式:
| 模式 | 适合场景 | 风险 |
|---|---|---|
| 共享 Topic,消息带 tenantId | 多数中小租户 | 热租户可能影响同分区 |
| 按租户分区 key | 保证同租户局部顺序 | 大租户可能单分区热点 |
| 大租户独立 Topic | 头部客户隔离 | 运维复杂度上升 |
| 死信按业务类型隔离 | 方便恢复 | 不要按无限租户建死信 |
消费者处理时:
- 消息必须带 tenantId。
- 消费日志去重维度要包含消费者和 eventId。
- 下游写库 SQL 必须带 tenant_id。
- 热租户消费慢时,要看分区、单条耗时、下游 DB 和重试。
- 毒消息不要阻塞所有租户,进入死信后按租户定位影响面。
八、数据库隔离模型
| 模型 | 优点 | 缺点 | 适合 |
|---|---|---|---|
| 共享库共享表 | 成本低、开发简单 | 隔离弱,SQL必须严控tenant_id | 中小租户、多数SaaS早期 |
| 共享库独立Schema | 逻辑隔离更强 | 迁移和连接管理复杂 | 租户数量中等 |
| 独立库 | 隔离强,可单独备份恢复 | 成本和运维复杂 | 大客户、合规要求高 |
| 独立集群 | 故障域完全隔离 | 成本最高 | 超大租户、金融/医疗核心 |
选型不是越隔离越好,而是看租户规模、合规、成本、运维能力、故障影响面和迁移路径。常见做法是:普通租户共享表,头部大租户单独库或单独集群。
九、商业场景:医疗数据采集平台
需求:同一平台服务多家医院,医院接口质量、数据量、采集窗口不同。
设计:
- Gateway 根据登录用户或服务账号确定医院/租户。
- 采集任务按
tenantId + hospitalId + sourceId + batchNo幂等。 - 每家医院有独立采集并发、超时、重试和熔断配置。
- 大医院采集任务进入独立线程池或队列。
- 数据落库所有表带
tenant_id和hospital_id。 - Redis 缓存 key 带租户和医院。
- MQ 消息带 tenantId,消费者写库再次校验。
- ES 文档带 tenantId,查询必须过滤租户。
- 审计日志记录用户、租户、医院、接口、traceId。
十、生产 Runbook
10.1 怀疑租户串数据
- 先拿到 traceId、userId、tenantId、资源ID。
- 查 Gateway 是否清洗并重写租户 Header。
- 查 Feign/RPC 是否白名单透传租户。
- 查 Service 层是否校验资源归属。
- 查 SQL 是否缺少 tenant_id 条件。
- 查 Redis Key 是否缺少租户维度。
- 查 ES 查询 DSL 是否带 tenant filter。
- 查审计日志确认影响范围。
10.2 某个租户拖垮全站
- 按租户看 QPS、P99、错误率和接口 TopN。
- 看该租户是否占满线程池、连接池、MQ 分区或 Redis 热 Key。
- 临时降低该租户入口限流和高成本接口并发。
- 对弱依赖降级,保护其他租户核心链路。
- 批处理任务暂停、限速或迁移到独立队列。
- 复盘是否需要大租户独立库、独立 Topic 或独立集群。
10.3 租户限流误伤
- 查限流 key 是否取错,例如 NAT IP 被当成用户。
- 查租户识别是否为空,导致多个租户落到同一个 key。
- 查 Gateway、服务内、Sentinel 是否多层重复限流。
- 查配置发布版本和实例是否一致。
- 查是否热点接口应该单独限流,而不是限制全租户。
十一、面试标准回答
多租户隔离不是只加 tenant_id 字段,而是身份、数据、资源、缓存、消息、数据库和审计的全链路隔离。入口由 Gateway 校验 Token,删除外部伪造租户 Header,并写入可信租户上下文;下游服务仍要校验 Token 或受信任上下文,在 Service 层判断用户是否有该租户、医院、科室或资源权限;SQL 必须强制带 tenant_id,索引也要贴合租户查询。资源上要按租户做限流、并发、线程池、连接池、MQ和批处理配额,防止热租户拖垮其他租户。缓存 Key、消息、ES 文档、审计日志都必须包含租户维度。
十二、本章小结
多租户系统的正确性来自“每一层都不默认信任上一层”。Gateway 是第一道门,Service 是业务裁决,数据库是最终约束,缓存和消息都要带租户维度,资源池要能防止热租户放大。只要有一层漏了租户边界,就可能出现串数据、越权或全站雪崩。
