spring boot 多租户需结合 system 属性(启动/运行期设租户id)、threadlocal(请求级隔离)、abstractroutingdatasource(动态路由)及全链路组件适配实现,核心是上下文传播而非配置替换。

Spring Boot 本身不直接提供多租户支持,但可通过 System 属性注入 + 动态数据源 + 线程上下文隔离 实现运行期租户隔离。关键不在“System 属性”本身,而在于它如何作为轻量级、启动后可变的全局配置入口,配合 Spring 的生命周期和 Bean 创建时机,实现租户上下文的传递与切换。
用 System 属性标识当前租户(启动/运行期均可)
System 属性(System.setProperty("tenant.id", "t1"))比 @Value("${tenant.id}") 更灵活:它可在应用启动后任意时刻修改,适合运行期动态切换租户(如后台管理触发切换)。注意它仅作用于当前 JVM,不跨进程。
- 启动时设:在
main方法最前调用System.setProperty("tenant.id", args[0]) - 运行时设:通过 Admin Endpoint 或定时任务调用
System.setProperty("tenant.id", newTenantId) - 避免硬编码:建议封装为
TenantContextHolder.setTenantId(String),内部委托给 System 属性或 ThreadLocal(更推荐后者,见下条)
结合 ThreadLocal 实现请求级租户上下文(真正隔离核心)
System 属性是 JVM 级共享,无法区分并发请求。必须搭配 ThreadLocal<string></string> 在每次请求开始时绑定租户 ID(如从 Header、Subdomain 或 Session 中提取),并在请求结束时清除。
- 定义
TenantContext工具类,含getTenantId()、setTenantId(String)、clear() - 用
OncePerRequestFilter拦截 HTTP 请求,解析租户标识(例如request.getHeader("X-Tenant-ID")),调用TenantContext.setTenantId(...) - 务必在 filter 的
finally块中执行TenantContext.clear(),防止线程复用导致脏数据
动态数据源路由(根据租户上下文选择 DataSource)
继承 AbstractRoutingDataSource,重写 determineCurrentLookupKey(),返回当前租户 ID;再将各租户的 DataSource(如 HikariCP)注册为子数据源。
- 租户数据源可预加载(启动时读取配置中心或 DB,构建所有租户的
HikariDataSource) - 也可按需懒加载(首次访问某租户时创建并缓存,避免启动慢、内存占用高)
-
determineCurrentLookupKey()中优先取TenantContext.getTenantId(),未命中则 fallback 到默认租户或抛异常
确保事务、MyBatis、JPA 等组件感知租户上下文
动态数据源只解决连接获取,还需保障整个请求链路使用同一租户数据源。
-
事务管理器:使用自定义的
DataSourceTransactionManager,其dataSource属性指向你的动态数据源 Bean -
MyBatis:确认
SqlSessionFactory使用的是动态数据源;Mapper 接口无需改动,自动走路由 -
JPA/Hibernate:若用
AbstractRoutingDataSource,需确保LocalContainerEntityManagerFactoryBean的dataSource指向它;注意二级缓存需按租户隔离(如用CacheManager+ 租户前缀)
不复杂但容易忽略:System 属性只是起点,真正隔离靠 ThreadLocal 绑定 + 动态数据源路由 + 全链路组件适配。租户切换本质是“上下文传播”,而非配置替换。











