核心是ThreadLocal存键、取键、清键:因AbstractRoutingDataSource为单例,必须用ThreadLocal隔离线程上下文,否则线程复用会导致串库;determineCurrentLookupKey()需兜底返回默认key,remove()须在finally中调用防污染。

直接用原生 ThreadLocal 实现多数据源动态路由,核心就三点:存键、取键、清键。AbstractRoutingDataSource 本身不记状态,每次 getConnection() 都靠 determineCurrentLookupKey() 返回一个 key 去查 map,这个 key 必须来自当前线程上下文——ThreadLocal 就是唯一可靠的方式。
为什么非得用 ThreadLocal
因为 Spring 的 DataSource 是单例 Bean,所有请求共用同一个 AbstractRoutingDataSource 实例。如果不隔离线程状态,A 请求刚设了 "slave1",B 请求立刻覆盖成 "master",下一行 getConnection() 就可能连错库。尤其在 Tomcat 线程池、@Async、CompletableFuture 场景下,线程复用频繁,不用 ThreadLocal 必然串库或报 Connection closed。
ThreadLocal 的标准写法
定义一个静态 ThreadLocal
- set("slave1") 在业务入口(如 Controller 方法开头)显式设置,不能依赖上一次残留值
- get() 在 determineCurrentLookupKey() 中直接返回,务必加空值兜底:return DataSourceContextHolder.get() != null ? DataSourceContextHolder.get() : "master"
- remove() 必须手动调用,建议用 try-finally 包裹,否则线程被复用时会带着旧 key 查错库
AOP 自动切换的注意事项
用注解 + 切面省去手动 set/remove,但要注意:
- @Async 方法默认新开线程,原 ThreadLocal 不继承,需配合 TaskDecorator 或自定义 AsyncConfigurer 透传 key
- 切点要覆盖到真正执行 DAO 的方法,不能只切 Service 接口声明
- 事务方法内切换无效:Spring 事务一旦开启连接,Hibernate/JPA 就会绑定死,此时切库只影响后续非事务操作
常见报错和对应处理
遇到 No bean named 'null' available,说明 determineCurrentLookupKey() 返回了 null,本质是 ThreadLocal.get() 拿到空值后没兜底;Connection closed 多因 remove 漏掉,导致下一个请求复用脏 key;查到别的库的表,基本就是异步或线程池场景下 ThreadLocal 未正确透传。


















