spring配置主库从库_spring下的数据库主从分离(下)
上一篇介紹了如何配置并使用動態數據源切換,這邊主要梳理下源碼原理和遇到的坑。
1、首先就是我們發現有事務的方法里面數據源切換是失敗的,并且都是用的主數據源。
這里我們猜想是事務aop要比我們數據源aop先執行了,拿到默認數據源,并不再改變。然后我把配置文件的默認數據源設置為讀數據源,發現代碼拿到的就是讀數據源,導致寫操作失敗,然后我開始debug源碼。
首先是aop執行順序問題,這里可以根據order指定,但是不指定順序時,spring的聲明式事務的aop要比我們自定義的數據源aop先執行。
我看了幾個人的文章,并自己debug了下,梳理了一下事務aop的動作
(1)訪問到方法時,進入代理類CglibAopProxy的靜態內部類DynamicAdvisedInterceptor的intercept()方法,通過493行拿到一個這個方法的代理鏈,然后在499行執行proceed()方法處理代理。
image.png
(2)進入proceed()方法,我們看到這是個遞歸,這里循環的處理了代理鏈的每個代理,當處理事務時,73行的invoke()方法進入TransactionInterceptor的invoke()方法。
image.png
(3)TransactionInterceptor的invoke()方法調用TransactionAspectSupport的invokeWithinTransaction方法
image.png
(4)invokeWithinTransaction方法里面150行調用createTransactionIfNecessary方法開始創建事務,這里會根據事務屬性判斷是否需要開啟一個事務,事務的傳遞性也是在這里判斷的。
image.png
(5)createTransactionIfNecessary方法219行,調用getTransaction方法調用到AbstractPlatformTransactionManager類的getTransaction方法,然后進入到doBegin方法。
image.png
image.png
(6)終于doBegin方法就來到了我們配置文件配置的DataSourceTransactionManager,79行這里開始獲取connection了,這個dataSource就是我們初始化設置進去動態數據源。然后就調用了AbstractRoutingDataSource的getConnection()方法了,注意這里txObject設置了一個ConnectionHolder,下面分析會用到。
image.png
(7)看到這里就明白了,getConnection()調用determineTargetDataSource()方法,這個方法里面調用了我們重寫過的determineCurrentLookupKey方法,然而這時我們還沒走到數據源aop設置數據源,所以這里拿到的是null,然后按照他這個邏輯就拿了默認數據源,即主數據源。所以開啟事務的方法獲取到的是主數據源,在不指定order的前提下,一般也不會指定order。
image.png
然而有一個坑會導致這里獲取到讀數據源,導致失敗,這個以后會分析。我繼續梳理下拿到寫數據源后,為什么切換數據源會失敗。
2、事務開啟后切換數據源為什么失敗
spring聲明式事務是面向連接connection的,如果connection變化,那么將導致事務無法進行、提交。所以事務一旦獲取connection就不會變化,這也是為什么事務開啟后切換數據源失敗,因為已經不再從我們的數據源aop里面拿connection了。因為會從上文中提到的ConnectionHolder里面獲取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;
}
}
執行我們的業務代碼時,當遇到數據庫dao執行時,mybatis會走一些列流程,將我們配置好的configuration用來開啟一個openSession,然后調用SpringManagedTransaction的構造方法,將datasource初始化進去。然后各種用于執行sql的BaseExecutor之類的調用SpringManagedTransaction類中的getConnection()方法,這個方法會首先判斷當前類實例中的connection是否為空,不為空返回,為空進行獲取,這里如果釋放過close()方法,或沒加載過會為空。獲取進入openConnection()方法,通過DataSourceUtils的getConnection獲取,這個方法比較重要,首先會判斷我們之前開啟事務設置的ConnectionHolder是否為空,不為空就返回ConnectionHolder的connection,就是我們一開始處理
代理時候的xml默認數據源,主數據源。如果沒開啟事務,這個ConnectionHolder是空的,就執行Connection con = dataSource.getConnection();,從我們配置的AbstractRoutingDataSource里面拿數據源,這就會走到我們通過數據源標簽設置的數據源。
image.png
SpringManagedTransaction里面還有個close()方法,他是釋放connection的方法,發現沒有事務的時候,他會每次執行一個dao會釋放一次,如果在事務內,他不會釋放掉,直到事物提交才會執行。這樣,如果在事務內有多條dao操作的時候,在第一個dao操作從ConnectionHolder拿到connection后不釋放,后面的dao操作拿connection時進入的getConnection()的判空就會返回非空,直接返回connection,這就是事務保證唯一connection的方式。
3、ThreadLocal使用時遇到tomcat線程池需要注意的坑。
上面說道,事務aop先執行會獲取到默認數據源,因為我們的數據源標簽還沒有加載,但是在調試過程中,事務開啟時的determineCurrentLookupKey不為null,而是從庫。這是一次新的請求,理論上應該是空,因為還沒有任何時機將ThreadLocal設置值,然后注意到使用tomcat7時的bio時,只有我一個人請求時,會分配給我同一個線程,猜想是線程池復用線程,所以沒有將ThreadLocal里面的值銷毀,導致這次請求拿到了上一個請求的在線程里面ThreadLocal的值,這樣還是比較危險的,所以在切面類里面將最初的before切入改成了around切入,在執行后將ThreadLocal的值remove掉。
總結
以上是生活随笔為你收集整理的spring配置主库从库_spring下的数据库主从分离(下)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 盛大游戏杯第十五届上海大学程序设计联赛暨
- 下一篇: python ju_如何使用jupy设置