关于Spring+Mybatis事务管理中数据源的思考
之前被同事问了一个问题:在我们的工程里,事务的开启跟关闭是由Spring负责的,但具体的SQL语句却是由Mybatis执行的。那么问题来了,Mybatis怎么保证自己执行的SQL语句是处在Spring的事务上下文中?
注:这篇文章重点不是分析Spring事务的实现原理,但却需要读者提前了解Spring事务原理的一些知识点,这样读起来才会容易些
现在公司主流的开发框架大部分是使用spring+mybatis来操作数据库,所有的事务操作都是交给spring去管理。当我们需要一个有事务上下文的数据库操作时,我们的做法就是写一个操作数据库的方法,并在该方法上面加上@Transactional注解就可以了。
仔细思考一下这个过程,@Transactional是由spring进行处理的,spring做的事情是从数据源(一般为数据库连接池,比如说druid,c3p0等)获取一个数据库连接,然后在进入方法逻辑前执行setAutoCommit(false)操作,最后在处理成功或者出现异常的时候分别执行commit或者rollback操作。
那么问题来了,开启跟结束事务是由spring获取到数据库连接以后进行操作的,但我们实际执行的update或者insert语句却是由mybatis获取数据库连接进行操作的,可以想到如果想让事务生效,那么spring跟mybatis使用的必须是同一个连接,真实情况是什么样呢?它们之间如何进行无缝衔接?让我们通过源码来分析一下。
首先如果想在spring中使用mybatis,我们除了引入mybatis依赖以外,还需要引入一个包:mybatis-spring。
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>x.x.x</version>
</dependency>
可以猜测这个依赖包应该就是Spring跟Mybatis进行无缝连接的关键。
一般来说,我们在工程中的配置文件往往是这样:
<!--会话工厂 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
</bean>
<!--spring事务管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!--使用注释事务 -->
<tx:annotation-driven transaction-manager="transactionManager" />
注:
1.会话工厂sqlSessionFactory跟Spring事务管理器transactionManager所使用的数据源dataSource必须是同一个。
2.这里的sqlSessionFactory类型是org.mybatis.spring.SqlSessionFactoryBean,该类是由我们引入的包mybatis-spring提供的。
看名字就知道SqlSessionFactoryBean是一个工厂bean,也就是说它交给Spring的真正实例是由getObject()方法提供的,那么我们去看下它真正实例初始化源码:
@Override
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
//可以看出逻辑都在这里面
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
@Override
public void afterPropertiesSet() throws Exception {
//此处省略一些校验逻辑
//...
this.sqlSessionFactory = buildSqlSessionFactory();
}
//最后来看这个最核心的方法
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
//...
//省略一些其他初始化信息,我们重点关注事务处理逻辑
if (this.transactionFactory == null) {
//可以看出,mybatis中把事务操作交给了SpringManagedTransactionFactory去做
this.transactionFactory = new SpringManagedTransactionFactory();
}
configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));
//省略后续逻辑
//...
}
下面我们再去看看SpringManagedTransactionFactory类的源码:
public class SpringManagedTransactionFactory implements TransactionFactory {
/**
* {@inheritDoc}
*/
@Override
public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
return new SpringManagedTransaction(dataSource);
}
/**
* {@inheritDoc}
*/
@Override
public Transaction newTransaction(Connection conn) {
throw new UnsupportedOperationException("New Spring transactions require a DataSource");
}
/**
* {@inheritDoc}
*/
@Override
public void setProperties(Properties props) {
// not needed in this version
}
}
代码很少,且只有一个方法是有效的,看来离成功越来越近了,继续跟进去看看SpringManagedTransaction的源码:
@Override
public Connection getConnection() throws SQLException {
if (this.connection == null) {
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);
}
省略该类中其他部分,我们重点看获取连接的地方,这里最关键的地方就在this.connection = DataSourceUtils.getConnection(this.dataSource);,
DataSourceUtils全名是org.springframework.jdbc.datasource.DataSourceUtils,没错,它是由Spring提供的类,根据我们之前的猜测,Spring开启事务以后,Mybatis要想让自己的SQL语句处在这个事务上下文中操作,那必须拿到跟Spring开启事务同一个数据库连接才行,由于DataSourceUtils类是由Spring提供的,看来跟我们开始猜测的结果类似,我们接下来看看DataSourceUtils源码验证一下:
//获取数据库连接最终落在该方法上,我删除一些不重要的代码
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
//TransactionSynchronizationManager重点!!!有没有很熟悉的感觉??
ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);
if (conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) {
Connection con = fetchConnection(dataSource);
if (TransactionSynchronizationManager.isSynchronizationActive()) {
ConnectionHolder holderToUse = conHolder;
if (conHolder == null) {
holderToUse = new ConnectionHolder(con);
} else {
conHolder.setConnection(con);
}
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}
return con;
} else {
conHolder.requested();
if (!conHolder.hasConnection()) {
conHolder.setConnection(fetchConnection(dataSource));
}
return conHolder.getConnection();
}
}
看到TransactionSynchronizationManager有没有很亲切的感觉?对Spring事务管理源码熟悉的同学会马上联想到Spring开启事务以后,就是把相应的数据库连接放在这里,我截取源码看一下:
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}
这段代码具体就是在我们上面配置的org.springframework.jdbc.datasource.DataSourceTransactionManager类中的doBegin方法里。至于TransactionSynchronizationManager类的实现原理其实我觉得你已经猜到了,没错,就是Java中经典类库ThreadLocal类!!!
最后补上一张图来说明spring+mybatis事务过程数据源获取逻辑:
Spring-Mybatis事务处理过程