Spring的事务管理与MyBatis事物管理结合的原理
- 前言
- 正文
- 原生的 MyBatis 的事务处理
- SqlSession
- Transaction
- MyBatis 事务管理 与 Spring 事务管理的融合
- SpringManagedTransaction
- SqlSessionTemplate
- SqlSessionInterceptor
- mybatis-spring 融合的原理
- 连接获取 & 开启事务
- 连接的关闭/释放:并不是真正的关闭
- MyBatis 中使用的 DB 连接 与 spring-tx 中使用的 DB 连接应该是同步的
- 补充: Spring 与 MyBatis 结合后,为什么要调用两次 TransactionSynchronizationManager#bindSource()?
- 小结
前言
spring-tx 是一套事务管理框架。
Mybatis 是一个 ORM 框架,它会操作 DB 连接来执行 sql,所以,也会涉及到事务的处理。
在 MyBatis 中,是通过 SqlSession 来执行 sql 的,也是通过它来管理事务的。默认情况下,MyBatis 是开启事务的,即: connection.setAutoCommit(false);
那么,spring-tx 的事务管理是如何与 mybatis 的事务管理进行整合的呢?
正文
通过 前面文章 的分析,我们知道 spring-tx 是通过 PlatformTransactionManager
来管理事务的。
查看 PlatformTransactionManager
源码,我们可以发现,它在底层是通过 org.springframework.jdbc.datasource.ConnectionHolder
中持有的连接来控制事务的。
下面我们先来研究一下 MyBatis 是如何进行事务管理的?
原生的 MyBatis 的事务处理
MyBatis 有两种方式来处理事务: 一是,SqlSession;二是,Transaction;
SqlSession
org.apache.ibatis.session.SqlSession
:
The primary Java interface for working with MyBatis. Through this interface you can execute commands, get mappers and manage transactions.
SqlSession
是使用 MyBatis 时的关键接口。通过此接口,可以执行 sql 命令、获取 mappers 映射器和管理事务。
SqlSession 提供了 commit()/rollback()
、getConnection()/close()
方法来操作事物和管理连接。
SqlSession 操作事物的例子:
SqlSession session = sqlSessionFactory.openSession();
try {
int affected_rows = session.insert("com.kvn.mapper.UserMapper.insert", user);
} catch (Exception e) {
// 捕获到异常,将操作回滚
session.rollback();
}
// 正常执行,提交事务
session.commit();
session.close();
查看
DefaultSqlSession
的源码,可以发现,MyBatis 的事务和连接管理最终是交给org.apache.ibatis.transaction.Transaction
来处理的。
Transaction
org.apache.ibatis.transaction.Transaction
:
Wraps a database connection. Handles the connection lifecycle that comprises: its creation, preparation, commit/rollback and close.
Transaction
会包装数据库的连接,处理数据库连接的生命周期,包括:连接的创建、准备、提交、回滚和关闭。
可以说,SqlSession 是一个偏向于 sql 执行和应用的接口,它既可以执行 sql,又可以对事物进行管理。而 SqlSession 操作事物时,底层是通过 Transaction
来实现的。
Transaction 操作事物的例子:
public void doBiz(){
TransactionFactory transactionFactory = new JdbcTransactionFactory();
userMapper userDao=getSession().getMapper(UserMapper.class);
Transaction newTransaction=transactionFactory.newTransaction(getSession().getConnection());
try {
userDao.insert(xxx);
userDao.update(xxx);
} catch (Exception e) {
newTransaction.rollback();
e.printStackTrace();
} finally {
newTransaction.close();
}
}
综上,MyBatis 的事务管理,统一是通过 org.apache.ibatis.transaction.Transaction
来管理的。
MyBatis 事务管理 与 Spring 事务管理的融合
MyBatis 的事务和连接的管理是通过 mybatis-spring-2.0.3.jar
这个 jar 包来完成与 spring-tx 体系下的事务和连接管理整合的。
SpringManagedTransaction
SpringManagedTransaction 实现了 org.apache.ibatis.transaction.Transaction
接口。
SpringManagedTransaction
打通了 MyBatis 的事物管理、连接管理 和 spring-tx 的 事物管理、连接管理,使得 MyBatis 与 Spring 可以使用统一的方式来管理连接的生命周期 和 事务处理。
这里要格外的注意一下:
MyBatis 与 Spring 通过 mybatis-spring-2.0.3.jar
结合使用之后,事物的管理的逻辑就分为了两种情况:
-
- 在一个非
@Transactional
标记的方法中执行 sql 命令,则事物的管理会通过SpringManagedTransaction
来执行。
- 在一个非
-
- 在一个
@Transactional
标记的事物方法中执行 sql 命令,则SpringManagedTransaction
的commit()/rollback()
方法不会执行任何动作,而事物的管理会走 Spring 的 AOP 事物管理,即通过org.springframework.transaction.interceptor.TransactionInterceptor
来进行拦截处理。
- 在一个
MyBatis 的事务管理是通过 SqlSession 来进行管理的,底层又是通过
org.apache.ibatis.transaction.Transaction
来进行管理的。
Spring 与 MyBatis 整合时,通过SpringManagedTransaction
扩展了Transaction
接口。这样,MyBatis 的事务管理就和 Spring 的事务管理结合在一起了。
SqlSessionTemplate
MyBatis 执行 sql 都是通过 SqlSession
接口来执行的。
MyBatis 与 Spring 结合之后,sql 的执行具体会通过实现类 org.mybatis.spring.SqlSessionTemplate
来完成。
SqlSessionTemplate 的构造函数如下:
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
// 创建 SqlSession 的代理对象
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
可以看出,SqlSessionTemplate 持有一个 sqlSessionProxy 属性。SqlSessionTemplate 相当于是 SqlSession 接口的一个代理,它将所有的 SqlSession 的方法执行委托给 sqlSessionProxy 去执行。
所有的方法执行都通过 SqlSessionInterceptor
去进行拦截。
SqlSessionInterceptor
SqlSessionInterceptor
会对每次执行 Mapper 查询时进行拦截,然后通过 Spring 的事务同步器获取到当前的 SqlSession 去执行 sql 操作。
这里是 MyBatis 的 SqlSession 与 spring-tx 结合的一个非常关键的点!
SqlSessionInterceptor 保证了 MyBatis 的 SqlSession 在执行 sql 时使用的连接与 Spring 事物管理操作使用的连接是同一个连接。具体就是通过 Spring 的事务同步器 TransactionSynchronizationManager
来保证的。
详细见下面的源码:
Spring 事务同步器
TransactionSynchronizationManager
会将连接资源绑定到 ThreadLocal 变量中,如果是在同一个事务当中的话,就可以通过TransactionSynchronizationManager
中的 ThreadLocal 变量来获取到同一个连接资源。
mybatis-spring 融合的原理
针对上面一些核心类的分析可知:
- mybatis-spring.jar 是通过
SqlSessionTemplate
来创建SqlSession
的代理 sqlSessionProxy; - sqlSessionProxy 会通过
SqlSessionInterceptor
来对 SqlSession 中的每个 sql 操作进行拦截,从而使用 spring-tx 的事务同步器TransactionSynchronizationManager
中管理的 SqlSession 来执行 sql。 - 在执行 sql 前,是通过
SpringManagedTransaction
来获取连接和管理事物的。 - 如果是 @Transactional 标记的事物方法,
SpringManagedTransaction
就会放弃事物的管理,交由 spring-tx 的TransactionInterceptor
来进行 aop 拦截,从而管理事物。
SpringManagedTransaction 中连接的获取是从 Spring 管理的 DataSource 中获取的,这样,数据库连接池也就和 spring 整合在一起了。
连接获取 & 开启事务
mybatis 通过 org.apache.ibatis.transaction.Transaction
接口来获取连接和进行事务管理。
与 Spring 结合时,通过实现类 org.mybatis.spring.transaction.SpringManagedTransaction
来获取连接
org.mybatis.spring.transaction.SpringManagedTransaction#getConnection()
的代码如下:
public Connection getConnection() throws SQLException {
if (this.connection == null) {
openConnection();
}
return this.connection;
}
/**
* Gets a connection from Spring transaction manager and discovers if this {@code Transaction} should manage
* connection or let it to Spring.
* It also reads autocommit setting because when using Spring Transaction MyBatis thinks that autocommit is always
* false and will always call commit/rollback so we need to no-op that calls.
*/
private void openConnection() throws SQLException {
this.connection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.connection.getAutoCommit();
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
LOGGER.debug(() -> "JDBC Connection [" + this.connection + "] will"
+ (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring");
}
连接的关闭/释放:并不是真正的关闭
org.mybatis.spring.transaction.SpringManagedTransaction#close()
/**
* 关闭连接。最终也是委托给 DataSourceUtils#releaseConnection()
*/
public void close() {
DataSourceUtils.releaseConnection(this.connection, this.dataSource);
}
详细的调用过程如下:
MyBatis 中使用的 DB 连接 与 spring-tx 中使用的 DB 连接应该是同步的
MyBatis SqlSession 在执行 sql 时使用的 DB 连接,与 spring-tx 管理事物时使用的 DB 连接应该是同一个,这样才能达到事物管理的效果。
连接资源同步的处理流程如下:
-
org.mybatis.spring.SqlSessionUtils#registerSessionHolder()
SqlSessionUtils#registerSessionHolder()
这个方法里面会调用TransactionSynchronizationManager.bindResource(SqlSessionFactory, SqlSessionHolder);
将 DB 连接资源SqlSessionHolder
绑定到当前线程。
SqlSessionHolder 中获取 DB 连接最终会调用SpringManagedTransaction#getConnection()
来获取,最终还是会优先从当前线程中绑定的ConnectinHolder
当中来获取 DB 连接。 -
org.springframework.jdbc.datasource.DataSourceUtils#doGetConnection()
spring-tx 在通过DataSourceUtils#doGetConnection()
获取到连接之后,会调用TransactionSynchronizationManager.bindResource(dataSource, holderToUse)
,将连接包装成ConnectinHolder
并绑定到当前线程中。
补充: Spring 与 MyBatis 结合后,为什么要调用两次 TransactionSynchronizationManager#bindSource()?
现象:
我们可以在代码中打印 TransactionSynchronizationManager.getResourceMap()
,可以发现里面有两个值:
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory@168b41a=org.mybatis.spring.SqlSessionHolder@79b62800
HikariDataSource (HikariPool-1)=org.springframework.jdbc.datasource.ConnectionHolder@7c6e897d
这说明,spring-tx 执行了两次 bindSource() 操作。
疑问:spring 与 mybatis 结合使用时,为什么要调用两次 bindSource() 呢?
答:spring-tx 管理事物时,连接是通过 org.springframework.jdbc.datasource.ConnectionHolder
来持有的,为了方便获取连接,ConnectionHolder
是通过 bindSource() 操作绑定在当前线程中的。
MyBatis 的 连接获取 和 sql 操作是通过 SqlSession
来完成的,为了方便获取 SqlSession, mybatis-spring.jar 中通过 org.mybatis.spring.SqlSessionUtils#registerSessionHolder()
调用 bindSource(),将 SqlSessionHolder
绑定在了当前线程中。
SqlSession
在获取连接时,是通过 SpringManagedTransaction
来获取的,底层最终还是优先获取当前线程中绑定的 ConnectionHolder
。
这样, MyBatis 的事物管理在与 Spring 进行整合后,在处理 @Transactional 事物方法时,使用的就是同一个 Connection
了。
TransactionSynchronizationManager#bindSource() 将事务资源绑定在了线程变量
ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
上
- Spring 绑定事物资源的 key=DataSource实现类, value=ConnectionHolder
- MyBatis 绑定事物资源的 key=DefaultSqlSessionFactory, value=SqlSessionHolder
参考: https://www.cnblogs.com/timfruit/p/11508873.html
要想将sql语句的执行由mybatis执行, 事务的提交或者回滚操作由Spring控制, 两者需要关联使用同一个connection, 在不同的方法中调用connection的相关方法操作, (所以, Spring并没有直接使用mybatis sqlSession中提供的提交或者回滚方法) . 如何安全的获取同一个connection?这就需要使用TransactionSynchronizationManager
Spring 没有直接使用 MyBatis 的 Transaction 中的事务管理的 begin/commit/rollback 方法,而是通过 SpringManagedTransaction
类中持有的 java.sql.Connection
对象直接进行事务管理的。
小结
JDBC 连接的生命周期分为: 连接的创建、提交、回滚和关闭
我们将连接的创建/关闭
分为一组,叫连接的管理;将连接的 提交/回滚
分为一组,叫事物的管理。
MyBatis 原生的 连接管理 和 事物管理 是交给 org.apache.ibatis.transaction.Transaction
来管理的。
Spring-tx 主要封装的是事物管理,事物管理操作是通过 DataSourceTransactionManager
来实现的。而连接的管理是通过 org.springframework.jdbc.datasource.DataSourceUtils
来操作具体的 DataSource
来实现的。
MyBatis 与 Spring-tx 的事物管理的整合是通过 mybatis-spring-2.0.3.jar
中的 SpringManagedTransaction
来完成的。
SpringManagedTransaction
打通了 MyBatis 的事物管理、连接管理 和 spring-tx 的 事物管理、连接管理,使得 MyBatis 与 Spring 可以使用统一的方式来管理连接的生命周期 和 事务处理。
MyBatis 与 Spring 结合之后,sql 的执行具体会通过实现类 org.mybatis.spring.SqlSessionTemplate
来完成。
SqlSessionTemplate 每次在执行 sql 时,都会被 SqlSessionInterceptor
进行拦截,拦截后会通过 Spring 的事务同步器 TransactionSynchronizationManager
获取到当前的 SqlSession 去执行 sql 操作。
SqlSessionInterceptor 保证了 MyBatis 的 SqlSession 在执行 sql 时使用的连接与 Spring 事物管理操作使用的连接是同一个连接。