上一篇介紹了如何配置并使用動態(tài)數(shù)據(jù)源切換搞乏,這邊主要梳理下源碼原理和遇到的坑咬最。
1蕴轨、首先就是我們發(fā)現(xiàn)有事務(wù)的方法里面數(shù)據(jù)源切換是失敗的,并且都是用的主數(shù)據(jù)源贸营。
這里我們猜想是事務(wù)aop要比我們數(shù)據(jù)源aop先執(zhí)行了吨述,拿到默認(rèn)數(shù)據(jù)源,并不再改變钞脂。然后我把配置文件的默認(rèn)數(shù)據(jù)源設(shè)置為讀數(shù)據(jù)源揣云,發(fā)現(xiàn)代碼拿到的就是讀數(shù)據(jù)源,導(dǎo)致寫操作失敗冰啃,然后我開始debug源碼邓夕。
首先是aop執(zhí)行順序問題,這里可以根據(jù)order指定阎毅,但是不指定順序時焚刚,spring的聲明式事務(wù)的aop要比我們自定義的數(shù)據(jù)源aop先執(zhí)行。
我看了幾個人的文章扇调,并自己debug了下矿咕,梳理了一下事務(wù)aop的動作
(1)訪問到方法時,進(jìn)入代理類CglibAopProxy的靜態(tài)內(nèi)部類DynamicAdvisedInterceptor的intercept()方法狼钮,通過493行拿到一個這個方法的代理鏈碳柱,然后在499行執(zhí)行proceed()方法處理代理。
(2)進(jìn)入proceed()方法熬芜,我們看到這是個遞歸莲镣,這里循環(huán)的處理了代理鏈的每個代理,當(dāng)處理事務(wù)時涎拉,73行的invoke()方法進(jìn)入TransactionInterceptor的invoke()方法瑞侮。
(3)TransactionInterceptor的invoke()方法調(diào)用TransactionAspectSupport的invokeWithinTransaction方法
(4)invokeWithinTransaction方法里面150行調(diào)用createTransactionIfNecessary方法開始創(chuàng)建事務(wù),這里會根據(jù)事務(wù)屬性判斷是否需要開啟一個事務(wù)曼库,事務(wù)的傳遞性也是在這里判斷的区岗。
(5)createTransactionIfNecessary方法219行略板,調(diào)用getTransaction方法調(diào)用到AbstractPlatformTransactionManager類的getTransaction方法毁枯,然后進(jìn)入到doBegin方法。
(6)終于doBegin方法就來到了我們配置文件配置的DataSourceTransactionManager叮称,79行這里開始獲取connection了种玛,這個dataSource就是我們初始化設(shè)置進(jìn)去動態(tài)數(shù)據(jù)源藐鹤。然后就調(diào)用了AbstractRoutingDataSource的getConnection()方法了,注意這里txObject設(shè)置了一個ConnectionHolder赂韵,下面分析會用到娱节。
(7)看到這里就明白了,getConnection()調(diào)用determineTargetDataSource()方法祭示,這個方法里面調(diào)用了我們重寫過的determineCurrentLookupKey方法肄满,然而這時我們還沒走到數(shù)據(jù)源aop設(shè)置數(shù)據(jù)源,所以這里拿到的是null质涛,然后按照他這個邏輯就拿了默認(rèn)數(shù)據(jù)源稠歉,即主數(shù)據(jù)源。所以開啟事務(wù)的方法獲取到的是主數(shù)據(jù)源汇陆,在不指定order的前提下怒炸,一般也不會指定order。
然而有一個坑會導(dǎo)致這里獲取到讀數(shù)據(jù)源毡代,導(dǎo)致失敗阅羹,這個以后會分析。我繼續(xù)梳理下拿到寫數(shù)據(jù)源后教寂,為什么切換數(shù)據(jù)源會失敗捏鱼。
2、事務(wù)開啟后切換數(shù)據(jù)源為什么失敗
spring聲明式事務(wù)是面向連接connection的酪耕,如果connection變化穷躁,那么將導(dǎo)致事務(wù)無法進(jìn)行、提交因妇。所以事務(wù)一旦獲取connection就不會變化问潭,這也是為什么事務(wù)開啟后切換數(shù)據(jù)源失敗,因?yàn)橐呀?jīng)不再從我們的數(shù)據(jù)源aop里面拿connection了婚被。因?yàn)闀纳衔闹刑岬降腃onnectionHolder里面獲取connection狡忙。看下SpringManagedTransaction類址芯。
package org.mybatis.spring.transaction;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.transaction.Transaction;
import org.springframework.jdbc.datasource.ConnectionHolder;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.Assert;
public class SpringManagedTransaction implements Transaction {
private static final Log LOGGER = LogFactory.getLog(SpringManagedTransaction.class);
private final DataSource dataSource;
private Connection connection;
private boolean isConnectionTransactional;
private boolean autoCommit;
public SpringManagedTransaction(DataSource dataSource) {
Assert.notNull(dataSource, "No DataSource specified");
this.dataSource = dataSource;
}
public Connection getConnection() throws SQLException {
if(this.connection == null) {
this.openConnection();
}
return this.connection;
}
private void openConnection() throws SQLException {
this.connection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.connection.getAutoCommit();
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
if(LOGGER.isDebugEnabled()) {
LOGGER.debug("JDBC Connection [" + this.connection + "] will" + (this.isConnectionTransactional?" ":" not ") + "be managed by Spring");
}
}
public void commit() throws SQLException {
if(this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
if(LOGGER.isDebugEnabled()) {
LOGGER.debug("Committing JDBC Connection [" + this.connection + "]");
}
this.connection.commit();
}
}
public void rollback() throws SQLException {
if(this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
if(LOGGER.isDebugEnabled()) {
LOGGER.debug("Rolling back JDBC Connection [" + this.connection + "]");
}
this.connection.rollback();
}
}
public void close() throws SQLException {
DataSourceUtils.releaseConnection(this.connection, this.dataSource);
}
public Integer getTimeout() throws SQLException {
ConnectionHolder holder = (ConnectionHolder)TransactionSynchronizationManager.getResource(this.dataSource);
return holder != null && holder.hasTimeout()?Integer.valueOf(holder.getTimeToLiveInSeconds()):null;
}
}
執(zhí)行我們的業(yè)務(wù)代碼時灾茁,當(dāng)遇到數(shù)據(jù)庫dao執(zhí)行時,mybatis會走一些列流程谷炸,將我們配置好的configuration用來開啟一個openSession北专,然后調(diào)用SpringManagedTransaction的構(gòu)造方法,將datasource初始化進(jìn)去旬陡。然后各種用于執(zhí)行sql的BaseExecutor之類的調(diào)用SpringManagedTransaction類中的getConnection()方法拓颓,這個方法會首先判斷當(dāng)前類實(shí)例中的connection是否為空,不為空返回描孟,為空進(jìn)行獲取驶睦,這里如果釋放過close()方法砰左,或沒加載過會為空。獲取進(jìn)入openConnection()方法场航,通過DataSourceUtils的getConnection獲取缠导,這個方法比較重要,首先會判斷我們之前開啟事務(wù)設(shè)置的ConnectionHolder是否為空溉痢,不為空就返回ConnectionHolder的connection僻造,就是我們一開始處理
代理時候的xml默認(rèn)數(shù)據(jù)源,主數(shù)據(jù)源孩饼。如果沒開啟事務(wù)嫡意,這個ConnectionHolder是空的,就執(zhí)行Connection con = dataSource.getConnection();捣辆,從我們配置的AbstractRoutingDataSource里面拿數(shù)據(jù)源蔬螟,這就會走到我們通過數(shù)據(jù)源標(biāo)簽設(shè)置的數(shù)據(jù)源。
SpringManagedTransaction里面還有個close()方法汽畴,他是釋放connection的方法旧巾,發(fā)現(xiàn)沒有事務(wù)的時候,他會每次執(zhí)行一個dao會釋放一次忍些,如果在事務(wù)內(nèi)鲁猩,他不會釋放掉,直到事物提交才會執(zhí)行罢坝。這樣廓握,如果在事務(wù)內(nèi)有多條dao操作的時候,在第一個dao操作從ConnectionHolder拿到connection后不釋放嘁酿,后面的dao操作拿connection時進(jìn)入的getConnection()的判空就會返回非空隙券,直接返回connection,這就是事務(wù)保證唯一connection的方式闹司。
3娱仔、ThreadLocal使用時遇到tomcat線程池需要注意的坑。
上面說道游桩,事務(wù)aop先執(zhí)行會獲取到默認(rèn)數(shù)據(jù)源牲迫,因?yàn)槲覀兊臄?shù)據(jù)源標(biāo)簽還沒有加載,但是在調(diào)試過程中借卧,事務(wù)開啟時的determineCurrentLookupKey不為null盹憎,而是從庫。這是一次新的請求铐刘,理論上應(yīng)該是空陪每,因?yàn)檫€沒有任何時機(jī)將ThreadLocal設(shè)置值,然后注意到使用tomcat7時的bio時,只有我一個人請求時奶稠,會分配給我同一個線程,猜想是線程池復(fù)用線程捡遍,所以沒有將ThreadLocal里面的值銷毀锌订,導(dǎo)致這次請求拿到了上一個請求的在線程里面ThreadLocal的值,這樣還是比較危險的画株,所以在切面類里面將最初的before切入改成了around切入辆飘,在執(zhí)行后將ThreadLocal的值remove掉。