ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring 声明式事务在多数据源切换时的失效排查与 DynamicDataSource 实践

Spring 声明式事务在多数据源切换时的失效排查与 DynamicDataSource 实践 Spring 声明式事务在多数据源切换时的失效排查与 DynamicDataSource 实践在读写分离、分库分表或多租户业务架构中单应用连接多个数据库实例如 Master 主库写、Slave 从库读是非常常见的场景。很多团队基于 Spring 提供的AbstractRoutingDataSource配合自定义注解TargetDataSource(slave)与 AOP 切面实现动态数据源切换。然而一旦在业务方法上同时使用了TargetDataSource(slave)和Transactional往往会出现极其诡异的 Bug“明明注解显式指定了切换到从库Slave执行只读查询但日志里打印出的 SQL 却依然在主库Master上被执行或者在事务内切换数据源完全失效”这个经典问题的根源在于Spring 声明式事务与动态数据源路由切面在 AOP 执行顺序以及数据库连接绑定时机上的脱节。今天我们把这套底层调用链彻底理顺并给出生产可用的多数据源高可用方案。事故复现为什么加了Transactional数据源就切不动了来看典型的业务切面代码Aspect Component Order(1) // 很多人以为把数据源切面 Order 调高就能解决问题 public class DynamicDataSourceAspect { Before(annotation(targetDataSource)) public void switchDataSource(JoinPoint point, TargetDataSource targetDataSource) { String dsKey targetDataSource.value(); DynamicDataSourceHolder.setDataSourceKey(dsKey); // 将数据源 Key 存入 ThreadLocal } After(annotation(targetDataSource)) public void restoreDataSource(JoinPoint point, TargetDataSource targetDataSource) { DynamicDataSourceHolder.clear(); } }Service public class ReportService { // 踩坑点同时使用了事务注解与切换从库注解 Transactional(readOnly true) TargetDataSource(slave) public ReportData generateReport(Long tenantId) { // 期望从从库读取海量报表数据但实际依然连接到了主库 return reportMapper.queryLargeStats(tenantId); } }底层时序崩塌过程Spring 声明式事务切面TransactionInterceptor默认是由InfrastructureAdvisor注册的其底层优先级非常高当调用generateReport时事务切面率先介入触发PlatformTransactionManager.getTransaction()事务管理器立即调用当前配置的DataSource.getConnection()从数据源获取数据库连接Connection此时由于ReportService.generateReport()的方法体还没正式执行数据源切面的Before甚至还没被触发ThreadLocal中记录的依然是默认的 Master 主库 KeyAbstractRoutingDataSource.determineCurrentLookupKey()读取到 Master从主库连接池中获取了 Connection并将该 Connection 绑定到了当前线程的TransactionSynchronizationManager紧接着数据源切面虽然执行了setDataSourceKey(slave)但后续 MyBatis 或 Hibernate 执行 SQL 时发现当前线程已经绑定了活跃的事务连接直接复用了刚才的主库 Connection核心解法引入LazyConnectionDataSourceProxy延迟获取连接根本破局之道不是去死磕 AOP 的Order排序而是改变获取物理数据库连接的时机。Spring 官方提供了LazyConnectionDataSourceProxy。它的核心思想是在事务开启或从数据源获取连接时先返回一个动态代理连接Proxy Connection此时根本不去真正的底层物理连接池拿连接只有当业务代码真正执行到第一条 SQL调用Statement.execute()或PreparedStatement.executeQuery()时代理连接才会根据此时 ThreadLocal 中的最新数据源 Key去对应的物理连接池索要真实连接Configuration public class DataSourceConfig { Bean public DataSource dynamicDataSource() { DynamicRoutingDataSource routingDataSource new DynamicRoutingDataSource(); MapObject, Object targetDataSources new HashMap(); targetDataSources.put(master, masterDataSource()); targetDataSources.put(slave, slaveDataSource()); routingDataSource.setTargetDataSources(targetDataSources); routingDataSource.setDefaultTargetDataSource(masterDataSource()); // 关键点用 LazyConnectionDataSourceProxy 包装动态路由数据源 return new LazyConnectionDataSourceProxy(routingDataSource); } Bean public PlatformTransactionManager transactionManager(DataSource dynamicDataSource) { return new DataSourceTransactionManager(dynamicDataSource); } }多数据源架构的生产避坑指南单个事务内严禁跨数据源物理操作LazyConnectionDataSourceProxy解决了“方法入口确定数据源”的问题但如果你试图在同一个Transactional方法内先往 Master 写、后又动态切换到 Slave 读依然是不可能的。因为一个 Spring 事务在单线程内只能绑定一个固定的物理 Connection。跨数据源一致性必须升级分布式事务如果业务确实需要在一次请求中同时修改两个物理隔离的数据库实例必须引入SeataAT 模式或基于消息队列的可靠消息最终一致性方案单机的DataSourceTransactionManager无法保证跨库原子性。ThreadLocal 必须在finally块中清理由于 Tomcat / 线程池中的工作线程是复用的如果不在切面的finally或afterCompletion中显式调用remove()该线程下一次处理请求时会产生极其严重的数据源污染隐患。
返回列表