@Transactional导致AbstractRoutingDataSource动态数据源无法切换的解决办法

简介: 上午花了大半天排查一个多数据源主从切换的问题,记录一下: 背景: 项目的数据库采用了读写分离多数据源,采用AOP进行拦截,利用ThreadLocal及AbstractRoutingDataSource进行数据源切换,数据源代码如下: public class RoutingDataSour...

上午花了大半天排查一个多数据源主从切换的问题,记录一下:

背景:

项目的数据库采用了读写分离多数据源,采用AOP进行拦截,利用ThreadLocal及AbstractRoutingDataSource进行数据源切换,数据源代码如下:

public class RoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DBContext.getDBKey();
    }
}

AOP细节就不讲了,大致是拦截mybatis的Mapper层,约定对方法前缀,比如update/delete/insert/save开头的认为是写方法,切换到主库,其它方法切换到从库。spring的xml配置如下:

数据源:

 1     <bean id="dsAlfred" class="cn.mwee.utils.datasource.RoutingDataSource">
 2         <property name="targetDataSources">
 3             <map key-type="java.lang.String">
 4                 <entry key="master" value-ref="dsAlfred_master"/>
 5                 <entry key="slave1" value-ref="dsAlfred_slave1"/>
 6                 <entry key="slave2" value-ref="dsAlfred_slave2"/>
 7                 <entry key="history" value-ref="dsAlfred_history"/>
 8             </map>
 9         </property>
10         <property name="defaultTargetDataSource" ref="dsAlfred_master"/>
11     </bean>

事务部分:

1     <bean id="alfredTxManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
2         <property name="dataSource" ref="dsAlfred"/>
3     </bean>
4     <tx:annotation-driven transaction-manager="alfredTxManager"/>

一直用了很久,都很正常(不管是事务方法,还是非事务方法),最近几天发现有一个服务,更新数据库时,一直报read-only异常,当时判断应该是连接到从库上了(注:从库是只读权限,无法更新数据),方法伪代码如下:

1 @Transactional
2 void doSomeThing(){
3   xxxMapper.select(...);
4    yyyMapper.update(...);
5    ...
6 } 

执行到第4行的时候,死活切换不到master主库上来,哪怕在doSomeThing方法的首行,设置DBContext.setDBKey("master") 都不好使,而其它类似的方法都正常。于是对比了代码,发现这个方法被调用的地方,最近加了几行代码,伪代码如下:

    public void method1(){
        xxxMapper.select(...);
... doSomeThing(); }

即:在调用doSomeThing()方法前,最近因为需求变更,前面加了一行查询操作(大家不用纠结为啥加这一行,产品需要~_~),把这个查询去掉,再执行,就ok了,然后... 然后就开始思考人生了...

各种百度,google后,最后在org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin 这个类的源代码中找到了答案:

 1 @Override
 2     protected void doBegin(Object transaction, TransactionDefinition definition) {
 3         DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
 4         Connection con = null;
 5 
 6         try {
 7             if (txObject.getConnectionHolder() == null ||
 8                     txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
 9                 Connection newCon = this.dataSource.getConnection();
10                 if (logger.isDebugEnabled()) {
11                     logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
12                 }
13                 txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
14             }
15 
16             txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
17             con = txObject.getConnectionHolder().getConnection();
18 
19             Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
20             txObject.setPreviousIsolationLevel(previousIsolationLevel);
21 
22             // Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
23             // so we don't want to do it unnecessarily (for example if we've explicitly
24             // configured the connection pool to set it already).
25             if (con.getAutoCommit()) {
26                 txObject.setMustRestoreAutoCommit(true);
27                 if (logger.isDebugEnabled()) {
28                     logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
29                 }
30                 con.setAutoCommit(false);
31             }
32 
33             prepareTransactionalConnection(con, definition);
34             txObject.getConnectionHolder().setTransactionActive(true);
35 
36             int timeout = determineTimeout(definition);
37             if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
38                 txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
39             }
40 
41             // Bind the connection holder to the thread.
42             if (txObject.isNewConnectionHolder()) {
43                 TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
44             }
45         }
46 
47         catch (Throwable ex) {
48             if (txObject.isNewConnectionHolder()) {
49                 DataSourceUtils.releaseConnection(con, this.dataSource);
50                 txObject.setConnectionHolder(null, false);
51             }
52             throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
53         }
54     }

注意:第7-16行,在开始一个事务前,如果当前上下文的连接对象为空,获取一个连接对象,然后保存起来,下次doBegin再调用时,就直接用这个连接了,根本不做任何切换(类似于缓存命中!)

这样就解释得通了: doSomeThing()方法被调用前,加了一段select方法,相当于已经切换到了slave从库,然后再进入doBegin方法时,就直接拿这个库的链接了,不再进行切换。那为啥其它同样启用事务的方法,又能正常连到主库呢?同样的解释,因为这类方法前面,没有任何其它操作,而xml中的动态数据源配置,默认连接的就是master主库,因此没有问题。

弄明白了之后,解决办法自然就有了:

    public void method1(){
        DBContext.setDBKey("master");//先切换到主库
        xxxMapper.select(...);
        ...
        doSomeThing();
    }

先切到主库上来,这样后面再调用有事务的方法时,就仍然保持在主库的连接上。

目录
相关文章
|
6月前
|
Java 数据库
SpringBoot手动取消接口执行方案
实际开发中经常会遇到比较耗时的接口操作,但页面强制刷新或主动取消接口调用后后台还是会继续运行,特别是有大量数据库操作时会增加服务器压力,所以进行研究测试后总结了一套主动取消接口调用的解决方案
101 0
|
负载均衡 算法 druid
【Java多数据源实现教程】实现动态数据源、多数据源切换方式(下)
【Java多数据源实现教程】实现动态数据源、多数据源切换方式(下)
912 0
|
20天前
|
Java
SpringBoot 内部方法调用,事务不起作用的原因及解决办法
在做业务开发时,遇到了一个事务不起作用的问题。大概流程是这样的,方法内部的定时任务调用了一个带事务的方法,失败后事务没有回滚。查阅资料后,问题得到解决,记录下来分享给大家。
16 4
|
6月前
|
Java 关系型数据库 MySQL
【mybatis-plus】自定义多数据源,动态切换数据源事务失效问题
【mybatis-plus】自定义多数据源,动态切换数据源事务失效问题
【mybatis-plus】自定义多数据源,动态切换数据源事务失效问题
|
6月前
|
XML Java 关系型数据库
注解驱动事务:Spring中基于注解的事务属性配置详解
注解驱动事务:Spring中基于注解的事务属性配置详解
113 0
注解驱动事务:Spring中基于注解的事务属性配置详解
|
Cloud Native Java Go
解决Nacos配置刷新问题: 如何启用配置刷新功能以及与`@RefreshScope`注解的关联问题
解决Nacos配置刷新问题: 如何启用配置刷新功能以及与`@RefreshScope`注解的关联问题
1188 0
|
6月前
|
Java
SpringBoot开启配置全局异常
SpringBoot开启配置全局异常
29 0
|
6月前
|
存储 Java 关系型数据库
springboot整合多数据源的配置以及动态切换数据源,注解切换数据源
springboot整合多数据源的配置以及动态切换数据源,注解切换数据源
655 0
ssm-事务--使用配置文件方式进行配置,使用注解进行使用
ssm-事务--使用配置文件方式进行配置,使用注解进行使用
|
SQL 消息中间件 JavaScript
我在项目里用@Transactional注解控制事务,结果完全不生效,纳尼?
我在项目里用@Transactional注解控制事务,结果完全不生效,纳尼?