能实现读写分离,但需手动控制路由逻辑和线程上下文;AbstractRoutingDataSource仅依据ThreadLocal中的key路由,不解析SQL或方法名,且必须用枚举保证类型安全、避免拼写错误导致null key异常。

能实现,但必须自己控制路由逻辑和线程上下文,Spring 不会自动识别“读”还是“写”。AbstractRoutingDataSource 只负责路由,不分析 SQL 或方法名;它只认你塞进 ThreadLocal 的 key,且这个 key 一旦设错或没清,就会导致主从串用、脏读甚至事务失效。
为什么 determineCurrentLookupKey() 必须返回枚举而不是字符串
返回字符串看似灵活,实则埋雷:拼写错误、大小写不一致、空格干扰都会让 resolvedDataSources.get(lookupKey) 返回 null,最终抛出 IllegalStateException: Cannot determine target DataSource for lookup key [...]。用枚举能靠编译器兜底。
-
DataSourceType.MASTER和DataSourceType.SLAVE是类型安全的 key,不会因 typo 失效 - Spring 初始化
AbstractRoutingDataSource时,会把所有@Bean数据源按 key 注入resolvedDataSourcesMap,key 类型必须严格匹配determineCurrentLookupKey()的返回类型 - 如果返回
String,你还得确保每个数据源@Bean名字(如masterDataSource)和你塞进ThreadLocal的字符串(如"master")完全一致——这在多模块或配置中心场景下极难维护
DataSourceContextHolder.clear() 漏调会导致什么
不清理 ThreadLocal,下一个请求可能复用上一个请求遗留的 DataSourceType.SLAVE,结果本该写主库的操作被发到从库,直接报错 Connection is read-only,或者静默失败(取决于数据库驱动是否校验 readOnly 状态)。
- 必须在每次请求结束时清理,推荐用
HandlerInterceptor.afterCompletion()或@AroundAOP 切面包裹 service 方法 - 不要依赖
finally块——如果 service 抛异常且未被捕获,finally可能不执行 - 异步线程(如
@Async)会丢失父线程的ThreadLocal,需手动传递并清理,否则子线程永远走默认数据源(通常是主库)
事务方法里强制走主库的坑怎么绕
Spring 的 @Transactional 在开启事务时就已绑定连接,此时再调用 DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER) 无效。事务内的所有操作仍走事务最初获取的数据源。
- 读写分离必须在事务开启前决定数据源,即:AOP 切面要在
@Transactional之前生效(调整@Order值,比如@Order(Ordered.HIGHEST_PRECEDENCE + 1)) - 不能在
@Transactional方法内部临时切从库——除非你手动TransactionTemplate开新事务,并显式指定TransactionDefinition.PROPAGATION_REQUIRES_NEW - 查询类方法(如命名含
find/get/list)可加自定义注解@ReadOnly,由 AOP 解析后提前设SLAVE;但注意:若该方法被@Transactional包裹且传播行为是REQUIRED,仍会沿用已有事务的主库连接
配置多个从库时,determineCurrentLookupKey 怎么返回具体实例
AbstractRoutingDataSource 的 resolvedDataSources 是 Map<object datasource></object>,key 可以是任意对象。要支持多个从库负载均衡,不能只用 DataSourceType.SLAVE 这个固定枚举值作 key。
- 定义新 key 类型,比如
SlaveKey,包含从库编号和权重信息:new SlaveKey("slave-1", 5) -
determineCurrentLookupKey()改为返回SlaveKey实例,配合轮询或随机策略选择具体从库 - 所有从库
@Bean必须按SlaveKey实例注册进resolvedDataSources,不能只注册名字字符串 - 注意:这种设计会让
DataSourceContextHolder持有更复杂的状态,建议封装成SlaveRouter类统一管理选取逻辑,避免业务层感知
最易被忽略的一点:AbstractRoutingDataSource 不做连接池管理,它只是路由壳。主从数据源各自要用 HikariCP 或 Druid 封装好再注入,否则高并发下连接泄漏或超时问题会掩盖路由本身的问题。

















