上一篇介绍了如何配置并使用动态数据源切换,这边主要梳理下源码原理和遇到的坑。
1、首先就是我们发现有事务的方法里面数据源切换是失败的,并且都是用的主数据源。
这里我们猜想是事务aop要比我们数据源aop先执行了,拿到默认数据源,并不再改变。然后我把配置文件的默认数据源设置为读数据源,发现代码拿到的就是读数据源,导致写操作失败,然后我开始debug源码。
首先是aop执行顺序问题,这里可以根据order指定,但是不指定顺序时,spring的声明式事务的aop要比我们自定义的数据源aop先执行。
我看了几个人的文章,并自己debug了下,梳理了一下事务aop的动作
(1)访问到方法时,进入代理类CglibAopProxy的静态内部类DynamicAdvisedInterceptor的intercept()方法,通过493行拿到一个这个方法的代理链,然后在499行执行proceed()方法处理代理。
(2)进入proceed()方法,我们看到这是个递归,这里循环的处理了代理链的每个代理,当处理事务时,73行的invoke()方法进入TransactionInterceptor的invoke()方法。
(3)TransactionInterceptor的invoke()方法调用TransactionAspectSupport的invokeWithinTransaction方法
(4)invokeWithinTransaction方法里面150行调用createTransactionIfNecessary方法开始创建事务,这里会根据事务属性判断是否需要开启一个事务,事务的传递性也是在这里判断的。
(5)createTransactionIfNecessary方法219行,调用getTransaction方法调用到AbstractPlatformTransactionManager类的getTransaction方法,然后进入到doBegin方法。
(6)终于doBegin方法就来到了我们配置文件配置的DataSourceTransactionManager,79行这里开始获取connection了,这个dataSource就是我们初始化设置进去动态数据源。然后就调用了AbstractRoutingDataSource的getConnection()方法了,注意这里txObject设置了一个ConnectionHolder,下面分析会用到。
(7)看到这里就明白了,getConnection()调用determineTargetDataSource()方法,这个方法里面调用了我们重写过的determineCurrentLookupKey方法,然而这时我们还没走到数据源aop设置数据源,所以这里拿到的是null,然后按照他这个逻辑就拿了默认数据源,即主数据源。所以开启事务的方法获取到的是主数据源,在不指定order的前提下,一般也不会指定order。
然而有一个坑会导致这里获取到读数据源,导致失败,这个以后会分析。我继续梳理下拿到写数据源后,为什么切换数据源会失败。
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里面拿数据源,这就会走到我们通过数据源标签设置的数据源。
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掉。