1. 程式人生 > >@Transactional導致無法動態資料來源切換

@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 的連線數與執行緒池