[007][租户模块]基于 TransmittableThreadLocal 与 TaskDecorator 的租户上下文传递设计
本项目代码:gitee.com/yunjiao-sou…
在 SaaS(Software as a Service)架构中,租户隔离是核心需求之一。每个请求都需要携带租户标识,并在整个处理链路中——包括 Web 层、Service 层以及异步任务——正确传递该标识,确保数据操作和业务逻辑严格限定在当前租户范围内。
本文结合一套自定义的 Spring Boot 扩展框架,详细分析其如何优雅地实现租户上下文在多线程环境下的传递。核心思路是:
- 使用
TransmittableThreadLocal(阿里 TTL)存储租户信息,解决普通ThreadLocal在线程池复用场景下的丢失问题。 - 通过
TaskDecorator+CompositeTaskDecorator机制,自动为所有异步任务装饰上下文传递逻辑。 - 结合 Web 拦截器(
HandlerInterceptor)在请求入口设置租户,并在请求结束清理。
一、租户上下文持有者:TenantContextHolder
public class TenantContextHolder {
private static final ThreadLocal<String> CURRENT_CONTEXT = new TransmittableThreadLocal<>();
public static String get() { /* 返回租户代码,默认大写,缺省为 DEFAULT_TENTANT_CODE */ }
public static void set(final String tenant) { CURRENT_CONTEXT.set(tenant); }
public static void clear() { CURRENT_CONTEXT.remove(); }
}
关键点:
- 使用
com.alibaba.ttl.TransmittableThreadLocal而非原生ThreadLocal,确保当任务提交到线程池时,父线程的租户上下文能“传递”给子线程。 get()方法提供默认租户代码,避免空指针;统一转为大写,保证租户标识规范化。set/clear方法成对出现,防止内存泄漏。
二、Web 层租户注入:TenantHandlerInterceptor
public class TenantHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, ...) {
String tenant = ServletUtils.getTenant(request); // 从 Header 或参数中获取
if (StringUtils.hasText(tenant)) {
TenantContextHolder.set(tenant);
}
return true;
}
@Override
public void afterCompletion(...) {
TenantContextHolder.clear(); // 请求结束必须清除
}
}
在 TenantCoreConfiguration 中注册该拦截器,并配置需要拦截的路径模式(通过 TenantProperties 注入)。
三、异步任务租户传递的核心:TaskDecorator
Spring 的 TaskExecutor 支持配置 TaskDecorator,用于装饰提交的 Runnable 或 Callable。框架设计了一套自动收集并组合装饰器的机制。
3.1 租户装饰器实现
public class TenantTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
String tenant = TenantContextHolder.get(); // 捕获当前线程的租户
return () -> {
try {
TenantContextHolder.set(tenant); // 子线程设置
runnable.run();
} finally {
TenantContextHolder.clear(); // 子线程清理
}
};
}
}
3.2 装饰器供应商:TaskDecoratorSupplier
@FunctionalInterface
public interface TaskDecoratorSupplier extends Supplier<TaskDecorator> { }
任何需要提供装饰器的模块,只需实现该接口并声明为 Spring Bean。框架会自动收集所有该类型的 Bean,并按顺序组合。
3.3 组合装饰器创建器:CompositeTaskDecoratorCreator
public class CompositeTaskDecoratorCreator implements Supplier<CompositeTaskDecorator> {
private final List<TaskDecoratorSupplier> taskDecoratorSuppliers;
@Override
public CompositeTaskDecorator get() {
// 双重检查锁,单例创建 CompositeTaskDecorator
List<TaskDecorator> decorators = taskDecoratorSuppliers.stream()
.map(TaskDecoratorSupplier::get)
.collect(Collectors.toList());
// 注意:这里反转了顺序!原因见下文。
instance = new CompositeTaskDecorator(decorators);
return instance;
}
}
为什么需要反转顺序?
CompositeTaskDecorator 按列表正向顺序执行装饰:第一个装饰器包裹最外层,最后一个装饰器最靠近核心业务逻辑。通常我们希望优先级高(Order 值小)的装饰器更靠近核心代码。
ObjectProvider.orderedStream() 返回的 Supplier 列表已是按 @Order 或 Ordered 升序排列(优先级高的在前)。经过 reverse 后,高优先级装饰器位于列表末尾,从而在执行时被更内层调用。
但在当前代码中,CompositeTaskDecoratorCreator 并未对列表进行反转,仅保留了原始顺序。这与类注释中的设计意图不一致。正确的实现应当先反转,或者由使用者确保 Supplier 的声明顺序已符合需求。这是一个潜在的改进点。
3.4 自动装配与使用
CommonCoreConfiguration创建CompositeTaskDecoratorCreatorBean,收集所有TaskDecoratorSupplier。TenantCoreConfiguration中声明TaskDecoratorSupplier返回TenantTaskDecorator。- 同时,
TenantCoreConfiguration利用CompositeTaskDecoratorCreator创建CompositeTaskDecorator并暴露为 Bean。 - 最后,业务方在配置
ThreadPoolTaskExecutor时,直接将CompositeTaskDecorator设置给setTaskDecorator(),即可让所有异步任务自动租户传递。
四、架构流程图解
HTTP Request
│
▼
TenantHandlerInterceptor.preHandle()
│ 设置 TenantContextHolder (主线程)
▼
Controller / Service
│ 可能调用 @Async 方法
▼
TaskExecutor.submit(Runnable)
│ 应用 CompositeTaskDecorator
│ ├── Decorator1 (如日志MDC)
│ ├── Decorator2 (TenantTaskDecorator)
│ └── ...
▼
子线程执行 Runnable
│ TenantTaskDecorator 已将租户上下文传递
│ 业务代码中 TenantContextHolder.get() 能获取正确租户
▼
Runnable 执行完毕 → finally 清理
五、技术亮点与注意事项
5.1 为什么不用 InheritableThreadLocal?
原生 InheritableThreadLocal 只能在创建子线程时复制父线程的值,但线程池复用线程时不会重新传递,导致租户错乱。TransmittableThreadLocal 配合 TTL 的 TtlRunnable/TtlExecutor 可以解决,但 Spring 的 TaskDecorator 机制提供了更轻量的方式——在装饰器中手动复制并设置。
5.2 组合装饰器顺序控制
实现类可以通过 @Order 或 Ordered 接口指定顺序。例如日志 MDC 装饰器通常优先级高于租户装饰器,因为日志输出需要租户信息。框架会按 orderedStream() 收集,但 CompositeTaskDecoratorCreator 目前未反转,使用者需要注意自己的装饰器逻辑是否依赖顺序。建议修正为反转,或在文档中明确说明。
5.3 租户默认值与缺省处理
TenantContextHolder.get() 返回默认租户 DEFAULT_TENANT_CODE,防止未设置租户时执行空逻辑,但也可能掩盖配置错误。可根据业务需要调整为抛出异常。
5.4 拦截器路径配置
通过 TenantProperties.getPathPatterns() 动态配置,例如 /api/**,避免对静态资源或开放接口进行租户拦截。
六、总结
本文介绍的租户传递方案具有以下优点:
- 非侵入:业务代码只需通过
TenantContextHolder.get()获取租户,无需手动传递参数。 - 自动覆盖:Web 请求和异步任务均自动注入租户,开发者无感知。
- 可扩展:基于
TaskDecoratorSupplier的 SPI 机制,其他模块可以轻松添加自己的装饰器(如请求追踪 ID、日志 MDC 等)。 - 线程安全:利用 TTL 和装饰器确保线程池场景下上下文的正确传递。
实际生产环境中,还需考虑定时任务、MQ 消费、@Scheduled 等线程模型,它们同样可以通过类似的 TaskDecorator 或自定义拦截器统一处理。本文的架构为多租户 SaaS 应用提供了一个可靠、干净的实现范式。