【Spring事物三千问】Spring的事务管理与MyBatis事物管理结合的原理

前言

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.png

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.png

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 体系下的事务和连接管理整合的。

mybatisspring.png

SpringManagedTransaction

SpringManagedTransaction 实现了 org.apache.ibatis.transaction.Transaction 接口。
SpringManagedTransaction 打通了 MyBatis 的事物管理、连接管理 和 spring-tx 的 事物管理、连接管理,使得 MyBatis 与 Spring 可以使用统一的方式来管理连接的生命周期 和 事务处理。

这里要格外的注意一下
MyBatis 与 Spring 通过 mybatis-spring-2.0.3.jar 结合使用之后,事物的管理的逻辑就分为了两种情况:

    1. 在一个非 @Transactional 标记的方法中执行 sql 命令,则事物的管理会通过 SpringManagedTransaction 来执行。
    1. 在一个 @Transactional 标记的事物方法中执行 sql 命令,则 SpringManagedTransactioncommit()/rollback() 方法不会执行任何动作,而事物的管理会走 Spring 的 AOP 事物管理,即通过 org.springframework.transaction.interceptor.TransactionInterceptor 来进行拦截处理。

SpringManagedTransactioncode.png

SpringManagedTransaction.png

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 来保证的。
详细见下面的源码:
SqlSessionInterceptor.png

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 整合在一起了。

doGetConnection.png

连接获取 & 开启事务

mybatis 通过 org.apache.ibatis.transaction.Transaction 接口来获取连接和进行事务管理。
与 Spring 结合时,通过实现类 org.mybatis.spring.transaction.SpringManagedTransaction 来获取连接

mybatisTransaction.png

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);
}

详细的调用过程如下:
closeConnection.png

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 事物管理操作使用的连接是同一个连接。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老王学源码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值