核心就三件事:线程级租户标识绑定、路由键动态返回、数据源map预加载;需通过过滤器提取x-tenant-id或jwt tenant字段存入threadlocal,并在finally中清除,determinecurrentlookupkey()须返回与targetdatasources注册key完全一致的字符串,且不可为null。

直接结论:用 AbstractRoutingDataSource 做多租户数据源切换,核心就三件事——线程级租户标识绑定、路由键动态返回、数据源 Map 预加载。漏掉任一环,要么切换失效,要么内存泄漏。
怎么把租户 ID 绑定到当前线程
多租户场景下,每次请求都带租户上下文(比如 HTTP Header 里的 X-Tenant-ID 或 JWT 中的 tenant 字段),你得在进入业务逻辑前把它存进 ThreadLocal,且必须确保每次请求结束时清除。
常见错误现象:
- 没清
ThreadLocal→ 后续请求复用前一个租户的数据源,查错库 - 用完不
remove()→ 线程池复用导致内存泄漏(ThreadLocal的 key 是弱引用,value 不会自动回收)
实操建议:
- 用过滤器(
OncePerRequestFilter)或网关统一提取租户 ID,调用DynamicDataSourceContextHolder.setTenantId("tenant_001") - 务必在
finally块里调用DynamicDataSourceContextHolder.clearTenantId() - 不要在异步线程(如
@Async)里直接复用该上下文,需手动传递并重设
determineCurrentLookupKey() 返回什么才对
这个方法不是返回数据库连接,也不是返回 URL,它只负责返回一个 Object 类型的 lookup key,这个 key 必须和你配置时注册进 targetDataSources 的 key 完全一致(包括大小写、空格)。
使用场景:
- 租户 ID 直接当 key:如
"tenant_a"对应数据库db_tenant_a - 带前缀的 key:如
"ds-tenant_b",此时targetDataSources里也得用同样前缀注册
容易踩的坑:
- 返回
null→ Spring 报No qualifying bean of type 'javax.sql.DataSource',因为找不到默认数据源 - 返回未注册的 key(如
"tenant_x"但没配)→ 报IllegalStateException: Cannot determine target DataSource for lookup key [tenant_x] - 返回 Integer 但配置用 String 当 key → 类型不匹配,路由失败(建议统一用
String)
示例片段:
@Override
protected Object determineCurrentLookupKey() {
String tenantId = DynamicDataSourceContextHolder.getTenantId();
if (tenantId == null) {
return "default"; // 必须有兜底,不能返回 null
}
return tenantId;
}
targetDataSources 怎么初始化才安全
Spring 初始化 AbstractRoutingDataSource 时,会调用 afterPropertiesSet(),此时 targetDataSources 必须已加载完成,否则启动报错或后续路由为空。
性能与兼容性影响:
- 如果租户数上千,别在启动时一次性全加载;改用懒加载 + 缓存(比如
ConcurrentHashMap+computeIfAbsent) - Druid 连接池要为每个租户单独配置(maxActive、minIdle 等),避免共用连接池引发跨租户连接污染
- MyBatis 的
SqlSessionFactory只需一个,因为它依赖的是DataSource接口,不感知底层是哪个物理库
实操建议:
- 用
@ConfigurationProperties(prefix = "multi.tenant")加载租户配置,结构类似:multi.tenant.tenant_001.url=... - 在
DynamicDataSource构造器里遍历配置,构建Map<object datasource></object>,再调用setTargetDataSources(...) - 设置
setDefaultTargetDataSource(...)指向一个“默认租户”或监控库,防止 lookup key 缺失时崩掉
AOP 切面要不要加?什么时候加
纯多租户场景(租户 ID 由请求头/Token 决定),不需要 AOP 注解驱动切换——过滤器就够了。AOP 只在以下情况有用:
- 某些后台任务需要临时指定租户(如定时同步
tenant_c的数据) - 同一接口内部分逻辑要切到另一个租户库(极少见,通常说明设计有问题)
容易被忽略的地方:
- AOP 切面必须在事务开启前执行,否则
@Transactional会拿不到正确的数据源 - 切面里设了
tenantId,但没配@Order(Ordered.HIGHEST_PRECEDENCE)→ 可能被其他 AOP 干扰 - 切面没处理异常路径:try/catch 后忘记
clear(),导致后续请求沿用错误租户
一句话收尾:真正难的不是写通路由,而是让租户上下文在过滤器、AOP、异步线程、事务传播、连接池销毁这些环节里始终干净、准确、可预测。










