@Transactional导致无法动态数据源切换

公司目前数据源为主从模式:主库可读写,从库只负责读。使用spring-jdbc提供的AbstractRoutingDataSource结合ThreadLocal存储key,实现数据源动态切换。

最近项目加入数据源切换后,偶尔会报出read-only异常,百思不得其解......

<!--数据源-->
<bean id="dsCrm" class="cn.mwee.framework.commons.utils.datasource.RoutingDataSource">
        <property name="targetDataSources">
            <map key-type="java.lang.String">
                <entry key="master" value-ref="dsCrm_master"/>
                <entry key="slave1" value-ref="dsCrm_slave1"/>
                <entry key="slave2" value-ref="dsCrm_slave2"/>
            </map>
        </property>
        <!--默认走主库-->
        <property name="defaultTargetDataSource" ref="dsCrm_master"/>
</bean>

RoutingDataSource类是对AbstractRoutingDataSource轻量封装实现determineCurrentLookupKey :

public class RoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DBContext.getDBKey();
    }
}
对应的业务代码如下,数据源切换在其他项目使用正常,代码迁移过来之后偶发报出read-only异常,数据库处于只读模式。写方法需要事物默认走主库,在该方法前也没有数据源的切换。 
@Transactional(rollbackFor = Exception.class)
public DataResult settingMarketMsg(SettingMarketMsgRequest request) {
        .....
}
@Slave
public DataResult detailMarketMsg(DetailMarketMsgRequest request) {
        ......
}  

因为aop切面只会切入打上@Slave注解的方法并切为从库,方法返回会清除key。所以臆想着肯定不会有问题?思考N久。。。

最后在aop的配置中看到破绽:

 1 @Component("dynamicDataSourceAspect")
 2 public class DynamicDataSourceAspect {
 3 public void setCrmDataSource(JoinPoint joinPoint) {
 4         MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
 5         Method method = methodSignature.getMethod();
 6         // 默认 master 优先
 7         DBContext.setDBKey(DbKeyConstant.DS_MASTER);
 8         if (method.isAnnotationPresent(Slave.class)) {
 9             DBContext.setDBKey(DbKeyConstant.DS_SLAVE_1);
10         }
11         logger.info("Revert DataSource : {} > {}", DBContext.getDBKey(), joinPoint.getSignature());
12     }
13     public void clearCrmDataSource(JoinPoint joinPoint) {
14         logger.info("Clear DataSource : {} > {}", DBContext.getDBKey(), joinPoint.getSignature());
15         DBContext.clearDBKey();
16     }
17 }

aop的xml配置 :

1 <aop:aspectj-autoproxy proxy-target-class="true"/>
2 <aop:config>
3    <aop:aspect id="dynamicDataSourceAspect" ref="dynamicDataSourceAspect" order="3">
4             <aop:pointcut id="dsAspect"
5                           expression="@annotation(cn.mwee.framework.commons.utils.datasource.Slave)"/>
6             <aop:before pointcut-ref="dsAspect" method="setCrmDataSource"/>
7             <aop:after-returning pointcut-ref="dsAspect" method="clearCrmDataSource"/>
8    </aop:aspect>
9 </aop:config>   

问题出在after-returning 即 方法正常返回之后执行,一旦执行异常就不会执行。此时线程如果没有被回收将一直持有该key。那线程持有该key,怎么跟上述【read-only异常】联系起来呢?

先看事物管理器配置:

<!--事务管理器-->
<bean id="crmTxManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
     <property name="dataSource" ref="dsCrm"/>
</bean>
<tx:annotation-driven transaction-manager="crmTxManager"/>

DataSourceTransactionManager的源码事物开始doBegin部分:

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

AbstractRoutingDataSource获取数据源源码:

 1 /**
 2      * Retrieve the current target DataSource. Determines the
 3      * {@link #determineCurrentLookupKey() current lookup key}, performs
 4      * a lookup in the {@link #setTargetDataSources targetDataSources} map,
 5      * falls back to the specified
 6      * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
 7      * @see #determineCurrentLookupKey()
 8      */
 9     protected DataSource determineTargetDataSource() {
10         // 根据线程绑定的key获取数据源, 如果不存在则获取默认数据源
11         Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
12         Object lookupKey = determineCurrentLookupKey();
13         DataSource dataSource = this.resolvedDataSources.get(lookupKey);
14         if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
15             dataSource = this.resolvedDefaultDataSource;
16         }
17         if (dataSource == null) {
18             throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
19         }
20         return dataSource;
21     }

这样可以解释偶发报出read-only异常了。项目使用tomcat,其中tomcat工作线程池是复用。当tomcat工作线程响应请求,访问带有@Slave方法,数据源切换至从库。由于某种原因异常导致未进入after-returning线程绑定的key未清除。此时访问写方法并tomcat使用同一个工作线程响应请求,通过AbstractRoutingDataSource将获得只读库的数据源,因而会产生报出read-only异常。

问题偶发原因在于必须满足两点:

  1、只读请求异常未切数据源

  2、复用相同tomcat工作线程池。

找到问题症结之后,自然容易解决:将after-returning 修改为 after(不管是否异常,都执行),每次必清空数据源即可。

<aop:config>
        <aop:aspect id="dynamicDataSourceAspect" ref="dynamicDataSourceAspect" order="3">
            <aop:pointcut id="dsAspect"
                          expression="@annotation(cn.mwee.framework.commons.utils.datasource.Slave)"/>
            <aop:before pointcut-ref="dsAspect" method="setCrmDataSource"/>
            <aop:after pointcut-ref="dsAspect" method="clearCrmDataSource"/>
        </aop:aspect>
</aop:config> 

参考链接 :

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

Tomcat线程池策略

详解 Tomcat 的连接数与线程池

原文地址:https://www.cnblogs.com/xiaoxing/p/10224617.html

时间: 2024-08-30 12:00:24

@Transactional导致无法动态数据源切换的相关文章

Transaction事务注解和DynamicDataSource动态数据源切换问题解决

问题描述: 写主库开事务的情况下会导致时不时的将更新/插入操作写入到从库上, 导致mysqlException 问题原因: jetty的工作队列会重用处理线程, 导致threadLocal被重用, 然而transaction注解在service层, 他会在DynamicDataSourceSwitch被设置之前直接去threadlocal拿数据, 本应拿到null, 但是拿到了之前线程的值 一般代码调用链: [email protected](AOP)-->DefaultSqlSession--

Spring主从数据库的配置和动态数据源切换原理

原文:https://www.liaoxuefeng.com/article/00151054582348974482c20f7d8431ead5bc32b30354705000 在大型应用程序中,配置主从数据库并使用读写分离是常见的设计模式.在Spring应用程序中,要实现读写分离,最好不要对现有代码进行改动,而是在底层透明地支持. Spring内置了一个AbstractRoutingDataSource,它可以把多个数据源配置成一个Map,然后,根据不同的key返回不同的数据源.因为Abst

spring AbstractRoutingDataSource实现动态数据源切换

使用Spring 提供的 AbstractRoutingDataSource 实现 创建 AbstractRoutingDataSource 实现类,负责保存所有数据源与切换数据源策略:public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.ge

Spring(AbstractRoutingDataSource)实现动态数据源切换

一.前言 近期一项目A需实现数据同步到另一项目B数据库中,在不改变B项目的情况下,只好选择项目A中切换数据源,直接把数据写入项目B的数据库中.这种需求,在数据同步与定时任务中经常需要. 那么问题来了,该如何解决多数据源问题呢?不光是要配置多个数据源,还得能灵活动态的切换数据源.以spring+hibernate框架项目为例(引用:http://blog.csdn.net/wangpeng047/article/details/8866239博客的图片): 单个数据源绑定给sessionFacto

Spring(AbstractRoutingDataSource)实现动态数据源切换--转载

原始出处:http://linhongyu.blog.51cto.com/6373370/1615895 一.前言 近期一项目A需实现数据同步到另一项目B数据库中,在不改变B项目的情况下,只好选择项目A中切换数据源,直接把数据写入项目B的数据库中.这种需求,在数据同步与定时任务中经常需要. 那么问题来了,该如何解决多数据源问题呢?不光是要配置多个数据源,还得能灵活动态的切换数据源.以spring+hibernate框架项目为例(引用:http://blog.csdn.net/wangpeng04

AbstractRoutingDataSource动态数据源切换

操作数据一般都是在DAO层进行处理,可以选择直接使用JDBC进行编程(http://blog.csdn.net/yanzi1225627/article/details/26950615/) 或者是使用多个DataSource 然后创建多个SessionFactory,在使用Dao层的时候通过不同的SessionFactory进行处理,不过这样的入侵性比较明显,一般的情况下我们都是使用继承HibernateSupportDao进行封装了的处理,如果多个SessionFactory这样处理就是比较

springboot2.0动态多数据源切换

摘要:springboot1.x到springboot2.0配置变化有一点变化,网上关于springboot2.0配置多数据源的资料也比较少,为了让大家配置多数据源从springboot1.x升级到springboot2.0少踩写坑,博主在此介绍用springboot2.0来进行动态数据源切换.(在博客的最后会给大家提供源码的下载地址) 一.引入依赖 <?xml version="1.0" encoding="UTF-8"?> <project x

spring动态数据源+事务

今天在尝试配置spring的动态数据源和事务管理的时候,遇到了几处配置上的问题,在此记录下: 1.使用了spring的aop思想,实现了动态数据源的切换. 2.spring的事务管理,是基于数据源的,也就是说Transaction是基于SessionFactory的, 所以如果要实现动态数据源切换,而且在同一个数据源中保证事务是起作用的话,就需要注意二者的顺序问题,即:在事务起作用之前就要把数据源切换回来. 3.application-mvc.xml文件扫描时不需要扫描services,避免事务

spring 动态数据源

1.动态数据源:  在一个项目中,有时候需要用到多个数据库,比如读写分离,数据库的分布式存储等等,这时我们要在项目中配置多个数据库. 2.原理:   (1).spring 单数据源获取数据连接过程: DataSource --> SessionFactory --> Session  DataSouce   实现javax.sql.DateSource接口的数据源,  DataSource  注入SessionFactory, 从sessionFactory 获取 Session,实现数据库的