一、引言
在多租户(Multi-Tenant)SaaS 系统中,"租户上下文传播" 是确保租户隔离、访问安全与数据正确性的基础设施能力。
它决定了系统能否在请求链路、异步任务和分布式微服务中保持租户标识一致,从而实现全局的数据与逻辑隔离。
本文从架构层面系统解析 SaaS 租户上下文传播机制 的设计原则、技术路径与工程实现。
二、背景与核心问题
SaaS 系统中的多租户架构主要分为三种:
| 模式 | 描述 | 优缺点 |
|---|---|---|
| 共享表(Shared Schema) | 多个租户共用同一数据库表,通过 tenant_id 字段区分 |
成本低、隔离弱 |
| 独立 Schema | 每个租户单独 Schema,表结构相同 | 中等成本、较好隔离 |
| 独立数据库(Database per Tenant) | 每个租户独立数据库连接 | 隔离强、运维复杂 |
不论哪种模式,都需要解决同一个核心问题:
在请求链路中,如何让系统的每一层都"知道"当前请求属于哪个租户?
这就是"租户上下文传播"的问题。
三、架构原则
SaaS 租户上下文传播的设计遵循以下三大原则:
-
上下文透明性(Transparency)
业务层不感知租户传递过程,只需访问
TenantContext.getTenantId()。 -
线程安全性(Thread Safety)
同一请求线程内上下文一致,不同请求间互不干扰。
-
跨域传播能力(Propagation Capability)
能在异步任务、消息队列、微服务调用等场景中正确传递租户标识。
四、架构分层设计
1. 入口层(API 层)
职责: 解析并设置租户标识。
典型实现: Servlet Filter 或 Spring WebFilter。
java
public class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
String tenant = request.getHeader("X-Tenant-ID");
try {
TenantContext.setTenant(tenant);
chain.doFilter(req, res);
} finally {
TenantContext.clear();
}
}
}
入口层通过 ThreadLocal 存储租户标识,实现请求线程内可见。
2. 上下文层(ThreadLocal 存储)
java
public class TenantContext {
private static final ThreadLocal<String> TENANT_HOLDER = new ThreadLocal<>();
public static void setTenant(String tenantId) { TENANT_HOLDER.set(tenantId); }
public static String getTenant() { return TENANT_HOLDER.get(); }
public static void clear() { TENANT_HOLDER.remove(); }
}
设计要点:
-
线程级上下文,访问性能极高;
-
必须在请求结束后清理;
-
可扩展为多维上下文(如 UserContext、TraceContext)。
3. 框架层(数据访问与ORM)
在数据访问层,租户上下文的任务是"被利用"。
典型场景包括:
(1) 动态数据源路由
java
public class TenantRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TenantContext.getTenant();
}
}
每个租户的数据库连接池独立,框架根据租户上下文动态选择数据源。
(2) ORM 层租户隔离
-
Hibernate: 通过
MultiTenantConnectionProvider与CurrentTenantIdentifierResolver支持多租户; -
MyBatis: 使用
Interceptor拦截 SQL,自动注入租户条件; -
JPA: 可结合 EntityManager 的多租户机制实现。
4. 异步任务层(线程传播)
在使用 @Async、线程池、定时任务时,ThreadLocal 无法自动传播。
必须通过 TaskDecorator 或包装 Runnable 实现上下文复制:
java
@Bean
public TaskDecorator tenantContextDecorator() {
return runnable -> {
String tenant = TenantContext.getTenant();
return () -> {
TenantContext.setTenant(tenant);
try {
runnable.run();
} finally {
TenantContext.clear();
}
};
};
}
这样可以确保异步任务与原始请求保持同一租户上下文。
5. 分布式传播层(跨服务)
在微服务架构中,请求经过多个服务,需要在调用链上传递租户信息。
解决方案:
- Feign 拦截器 自动注入租户头:
java
public class TenantFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String tenant = TenantContext.getTenant();
if (tenant != null) {
template.header("X-Tenant-ID", tenant);
}
}
}
-
下游服务 再次通过 Filter 解析并写入 ThreadLocal;
-
消息队列场景:在消息 Header 中附带租户信息。
五、架构整体流程图(逻辑示意)
java
┌─────────────────────────────────────┐
│ Client / API │
└─────────────────────────────────────┘
│
▼
┌────────────────────────────┐
│ TenantFilter(提取租户) │
│ └── 设置 ThreadLocal │
└────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ Spring 调用链 (Controller→Service) │
│ 通过 TenantContext 获取租户 │
└──────────────────────────────┘
│
▼
┌────────────────────────────┐
│ AbstractRoutingDataSource │
│ └── determineCurrentLookupKey() │
│ 从 ThreadLocal 取租户ID │
└────────────────────────────┘
│
▼
┌────────────────────────────┐
│ 异步任务 / 微服务调用 │
│ └── 通过 Header / TaskDecorator 传递租户 │
└────────────────────────────┘
六、扩展与前瞻设计
1. 上下文容器化
未来的 SaaS 框架可将租户上下文扩展为独立容器(如 ContextMap),不仅包含 tenant,还可携带 user、traceId、locale 等信息,实现全链路上下文统一管理。
2. Reactor / WebFlux 场景
对于响应式系统,应使用 Reactor Context 替代 ThreadLocal:
java
Mono.deferContextual(ctx -> {
String tenant = ctx.get("tenantId");
return processTenant(tenant);
});
3. 全局传播机制(Context Propagation)
未来趋势是引入标准化上下文传播协议(如 W3C Context Propagation、OpenTelemetry Baggage)以支持跨语言传播租户标识。
七、总结
| 层级 | 职责 | 关键机制 |
|---|---|---|
| API 层 | 解析租户并注入上下文 | Filter |
| 上下文层 | 线程级存储租户标识 | ThreadLocal |
| 数据访问层 | 动态切换数据源 / Schema | AbstractRoutingDataSource |
| 异步层 | 上下文跨线程传播 | TaskDecorator |
| 分布式层 | 上下文跨服务传播 | Header / Feign Interceptor |