@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() { returnDBContext.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>
參考連結 :