• @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 }
    View Code

     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>   
    View Code

    问题出在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     }
    View Code

     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     }
    View Code

    这样可以解释偶发报出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 的连接数与线程池

  • 相关阅读:
    我的知识库(4) java获取页面编码(Z)
    知识库(3)JAVA 正则表达式 (超详细)
    The Struts dispatcher cannot be found. This is usually caused by using Struts tags without the associated filter. Struts
    某人总结的《英语听力的技巧 》,挺搞的
    我的知识库(5)java单例模式详解
    构建可扩展程序
    SerialPort (RS232 Serial COM Port) in C# .NET
    Python学习笔记——String、Sequences
    UI题目我的答案
    jQuery学习系列学会操纵Form表单元素(1)
  • 原文地址:https://www.cnblogs.com/xiaoxing/p/10224617.html
Copyright © 2020-2023  润新知