最可控方案是AbstractRoutingDataSource+ThreadLocal+自定义注解,HikariCP需分层配置Bean;DataSourceBuilder会绕过Hikari参数导致连接耗尽;路由key须与ThreadLocal值严格一致;@Transactional不自动切库,需配合@TargetDataSource;跨库事务需Seata或补偿;主从延迟需@Master兜底。

直接上结论:用 AbstractRoutingDataSource + ThreadLocal + 自定义注解是最可控、最易调试的方案,HikariCP 配置必须分层写,否则连接池参数全失效。
为什么不能只靠 DataSourceBuilder.create().build()
这是新手最常踩的坑——它会绕过 HikariCP 的所有配置项(比如 maximum-pool-size、connection-timeout),最终用的是默认连接池,压测时立刻暴露连接耗尽问题。
- 正确做法是为每个数据源单独声明
HikariDataSourceBean,并显式 set 各项参数 -
spring.datasource.master.hikari.*这类 yml 路径只有配合@ConfigurationProperties("spring.datasource.master")才生效 - 别图省事用
DataSourceBuilder,它不认hikari下级配置
AbstractRoutingDataSource 的 key 必须和 ThreadLocal 值严格一致
路由逻辑靠 determineCurrentLookupKey() 返回的字符串匹配数据源 Map 的 key,大小写、下划线、空格错一个就 fallback 到默认数据源(甚至报 IllegalStateException: No target DataSource set)。
- 建议统一用小写字符串,如
"master"和"slave" -
DataSourceContextHolder.setDataSourceType("slave")和routingDataSource.setTargetDataSources(Map.of("slave", slaveDataSource))中的"slave"必须完全相同 - 切面里执行完一定要
clear(),否则线程复用时会沿用上一次的 key
事务方法里读操作也会走主库,但得手动控制
@Transactional 默认绑定到主事务管理器,即使你写了 @ReadOnly 注解,Spring 也不会自动切从库——它只影响事务的 readOnly 属性,不触发数据源切换。
- 必须在 service 方法上同时加自定义注解(如
@TargetDataSource("slave"))和@Transactional(readOnly = true) - 如果方法既读又写,别指望自动分离;要么拆成两个方法,要么显式调用
DataSourceContextHolder.setDataSourceType("master") - 跨库事务(比如主库写 A 表、从库查 B 表)无法用 Spring 原生事务保证一致性,得用 Seata 或业务层补偿
最容易被忽略的是主从延迟场景下的脏读:从库同步有 lag,但代码里没做任何兜底。上线前必须确认是否允许“写后即读不到”,否则得加 @Master 强制读主库,或者引入缓存兜底。


















