AbstractRoutingDataSource 必须配 ThreadLocal,因其不保存数据源标识,每次 getConnection() 时依赖 determineCurrentLookupKey() 动态获取键,而该键需通过 ThreadLocal 在线程上下文中传递,否则多线程下会因共享状态导致路由错乱、连接异常或跨库查询。

为什么 AbstractRoutingDataSource 必须配 ThreadLocal
因为 AbstractRoutingDataSource 本身不保存数据源标识,它只在每次 getConnection() 时调用 determineCurrentLookupKey() 查一次键——这个键得靠外部线程上下文传进来。不用 ThreadLocal,多线程下会互相覆盖、路由错乱。
常见错误现象:Connection closed 或查到别的库的表,尤其在 Spring MVC 异步请求、定时任务、CompletableFuture 场景下高频复现。
- 必须在业务入口(如 Controller 方法开始)就用
DataSourceContextHolder.set("slave1")显式设键,不能依赖“上一个请求留下的值” -
ThreadLocal需手动remove(),否则线程复用(如 Tomcat 线程池)会导致脏数据;建议用try-finally包裹 - Spring AOP 切面里设键时,注意切点是否覆盖了异步方法(
@Async默认新开线程,原ThreadLocal不继承)
AbstractRoutingDataSource 的 determineCurrentLookupKey 返回 null 怎么办
返回 null 会触发 NoSuchBeanDefinitionException: No bean named 'null' available,本质是 Spring 尝试从 targetDataSources Map 中找 key 为 null 的数据源。
使用场景:切换逻辑有分支未覆盖(比如没处理默认主库)、或 ThreadLocal 取值前没初始化。
- 务必在
determineCurrentLookupKey()开头加空值兜底:return DataSourceContextHolder.get() != null ? DataSourceContextHolder.get() : "master" - 不要在
determineCurrentLookupKey()里做耗时操作(如查 DB、远程调用),它在每次获取连接时都执行,直接影响 SQL 性能 - 如果用枚举管理数据源名,确保
toString()或name()和targetDataSources的 key 完全一致(大小写敏感)
多数据源事务失效的典型原因
Spring 原生 @Transactional 只绑定单个 DataSource,跨数据源时事务无法传播——不是配置问题,是底层 JDBC/XA 限制。
分析 Java Spring Boot 后端 Controller 代码,自动生成 Markdown 格式的接口文档。当用户说“生成 Java 接口文档”、“生成 API 文档”、“从 Spring 代码生成文档”时触发。
错误现象:主库插入成功、从库更新失败,但主库未回滚;或抛出 Cannot change transaction isolation level after connection has been obtained。
- 同一个事务内禁止切换数据源(即不能在
@Transactional方法中调用两次不同set()) - 需要强一致性时,改用
JtaTransactionManager+ XA 数据源(如Atomikos),但性能损耗明显,且 MySQL 8.0.30+ 才完整支持 XA PREPARE - 多数业务用最终一致性:主库写后发 MQ,下游服务消费并更新其他库,通过本地事务表+定时补偿
动态增删数据源时如何避免 ConcurrentModificationException
AbstractRoutingDataSource 的 targetDataSources 是 Map,直接 put 或 remove 会触发 ConcurrentModificationException,因为 afterPropertiesSet() 内部会遍历该 Map 构建缓存。
性能影响:频繁修改会导致路由逻辑锁竞争,吞吐量断崖下跌。
- 必须用线程安全容器替代:
ConcurrentHashMap,并在构造时传入,而非用父类默认的LinkedHashMap - 修改
targetDataSources后,需显式调用super.setTargetDataSources(...)和afterPropertiesSet()刷新内部缓存 - 生产环境慎用动态增删——DNS 切换、数据库扩缩容等应走发布流程,而非运行时热插拔
最易被忽略的一点:自定义的 DataSource Bean 如果用了 HikariCP,它的 setJdbcUrl() 等方法不是线程安全的,动态改配置前必须先 close() 原连接池。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










