ORM - MyBatis事务管理机制详解

2022年10月20日
大约 5 分钟

ORM - MyBatis事务管理机制详解

事务这个词我们多次遇到,在Executor执行器里面,在SqlSession里面,在二级缓存里面。和事务打交道避免不了事务的管理,那么MyBatis是如何进行事务管理的,这篇文章将带领大家一解疑惑。

初识事务

数据库事务的定义就是将多个执行单元看作一个整体,要么全部执行成功,要么执行失败。事务具有ACID属性,事务的隔离级别有读未提交,读提交,可重复读,串行等等,这些涉及到数据库的知识我们在此不做展开,希望同学们有这方面的基础。

MyBatis 事务管理的接口是Transaction,它其实包装的就是一个数据库连接,处理这个连接的生命周期,它的方法如下

/**
 * 包装一个数据库连接,处理一个数据库连接的生命周期,包括创建,准备,提交,回滚,关闭
 * @author Clinton Begin
 */
public interface Transaction {

  /**
   * 获取内部的数据库连接
   */
  Connection getConnection() throws SQLException;

  /**
   * 提交
   */
  void commit() throws SQLException;

  /**
   * 回滚
   */
  void rollback() throws SQLException;

  /**
   * 关闭连接
   */
  void close() throws SQLException;

  /**
   *  获取超时时间
   */
  Integer getTimeout() throws SQLException;

}

Transaction有两个实现JdbcTransaction和ManagedTransaction,提供的这两种管理机制来管理事务

  • JdbcTransaction:使用Jdbc中的java.sql.Connection来管理事务,包括提交回滚
  • ManagedTransaction:在这种机制下,MyBatis不会管理事务,而是交由程序的运行容器(weblogic,tomcat)来进行管理。

图

创建事务

在mybatis-config.xml中可以配置事务管理的类型,其中transactionManager的类型配置为JDBC,将会以JdbcTransaction管理机制来管理事务。

<environments default="development">
	<environment id="development">
		<transactionManager type="JDBC"/>
		<dataSource type="POOLED">
			<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
			<property name="url" value="jdbc:mysql://xxxx:3306/xxxx?useUnicode=true"/>
			<property name="username" value="xxxx"/>
			<property name="password" value="xxxx"/>
		</dataSource>
	</environment>
</environments>

XMLConfigBuilder 中的environmentsElement会解析transactionManager类型,并且会创建一个TransactionFactory类型的事务工厂,这个工厂的作用就是来创建Transaction事务对象。

// XMLConfigBuilder
  private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        if (isSpecifiedEnvironment(id)) {
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }

TransactionFactory 作为对象工厂,其功能就是创建一个Transaction对象。创建方式有两种

  • 从已有的连接中创建Transaction对象
  • 根据数据源,数据库隔离级别,自动提交创建Transaction对象
public interface TransactionFactory {

  /**
   * 设置事务工厂私有属性
   */
  default void setProperties(Properties props) {
    // NOP
  }

  /**
   * 从已有的连接中创建Transaction对象
   */
  Transaction newTransaction(Connection conn);

  /**
   * 根据数据源,数据库隔离级别,自动提交创建Transaction对象
   */
  Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);

}

TransactionFactory 有两个实现,一个是JdbcTransactionFactory,另一个是ManagedTransactionFactory,他们分别创建出来的就是JdbcTransaction和ManagedTransaction对象。以JdbcTransactionFactory为例,其实现也是很简单的,没有什么逻辑。

public class JdbcTransactionFactory implements TransactionFactory {

  @Override
  public Transaction newTransaction(Connection conn) {
    return new JdbcTransaction(conn);
  }

  @Override
  public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
    return new JdbcTransaction(ds, level, autoCommit);
  }
}

上面创建完TransactionFactory后,就会被Environment引用。接下来在获取SqlSession的时候,会根据事务工厂创建一个Transaction事务对象,根据这个对象,再创建具体的Executor,最终创建出创建SqlSession对象。

// DefaultSqlSessionFactory
  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      // 获取事务工厂
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      // 创建事务对象
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 创建执行器
      final Executor executor = configuration.newExecutor(tx, execType);
      // 创建SqlSession对象
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

JdbcTransaction 内容如下,也是没有什么复杂的逻辑,基本都是对JDBC connection的包装

/**
 * {@link Transaction} that makes use of the JDBC commit and rollback facilities directly.
 * It relies on the connection retrieved from the dataSource to manage the scope of the transaction.
 * Delays connection retrieval until getConnection() is called.
 * Ignores commit or rollback requests when autocommit is on.
 *
 * @author Clinton Begin
 *
 * @see JdbcTransactionFactory
 *
 *
 * Transaction 完全直接利用JDBC的提交和回滚管理机制。它依赖于从数据源获取到的连接connection来管理事务transaction的生命周期。
 * 如果开启了自动提交,那么提交和回滚将会被忽略。
 */
public class JdbcTransaction implements Transaction {

  private static final Log log = LogFactory.getLog(JdbcTransaction.class);

  protected Connection connection;
  protected DataSource dataSource;
  protected TransactionIsolationLevel level;
  protected boolean autoCommit;

  public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) {
    dataSource = ds;
    level = desiredLevel;
    autoCommit = desiredAutoCommit;
  }

  public JdbcTransaction(Connection connection) {
    this.connection = connection;
  }

  @Override
  public Connection getConnection() throws SQLException {
    if (connection == null) {
      openConnection();
    }
    return connection;
  }

  @Override
  public void commit() throws SQLException {
    if (connection != null && !connection.getAutoCommit()) {
      if (log.isDebugEnabled()) {
        log.debug("Committing JDBC Connection [" + connection + "]");
      }
      connection.commit();
    }
  }

  @Override
  public void rollback() throws SQLException {
    if (connection != null && !connection.getAutoCommit()) {
      if (log.isDebugEnabled()) {
        log.debug("Rolling back JDBC Connection [" + connection + "]");
      }
      connection.rollback();
    }
  }

  @Override
  public void close() throws SQLException {
    if (connection != null) {
      resetAutoCommit();
      if (log.isDebugEnabled()) {
        log.debug("Closing JDBC Connection [" + connection + "]");
      }
      connection.close();
    }
  }

  protected void setDesiredAutoCommit(boolean desiredAutoCommit) {
    try {
      if (connection.getAutoCommit() != desiredAutoCommit) {
        if (log.isDebugEnabled()) {
          log.debug("Setting autocommit to " + desiredAutoCommit + " on JDBC Connection [" + connection + "]");
        }
        connection.setAutoCommit(desiredAutoCommit);
      }
    } catch (SQLException e) {
      // Only a very poorly implemented driver would fail here,
      // and there's not much we can do about that.
      throw new TransactionException("Error configuring AutoCommit.  "
          + "Your driver may not support getAutoCommit() or setAutoCommit(). "
          + "Requested setting: " + desiredAutoCommit + ".  Cause: " + e, e);
    }
  }

  protected void resetAutoCommit() {
    try {
      if (!connection.getAutoCommit()) {
        // MyBatis does not call commit/rollback on a connection if just selects were performed.
        // Some databases start transactions with select statements
        // and they mandate a commit/rollback before closing the connection.
        // A workaround is setting the autocommit to true before closing the connection.
        // Sybase throws an exception here.
        if (log.isDebugEnabled()) {
          log.debug("Resetting autocommit to true on JDBC Connection [" + connection + "]");
        }
        connection.setAutoCommit(true);
      }
    } catch (SQLException e) {
      if (log.isDebugEnabled()) {
        log.debug("Error resetting autocommit to true "
            + "before closing the connection.  Cause: " + e);
      }
    }
  }

  protected void openConnection() throws SQLException {
    if (log.isDebugEnabled()) {
      log.debug("Opening JDBC Connection");
    }
    connection = dataSource.getConnection();
    if (level != null) {
      connection.setTransactionIsolation(level.getLevel());
    }
    setDesiredAutoCommit(autoCommit);
  }

  @Override
  public Integer getTimeout() throws SQLException {
    return null;
  }

}

个人总结

MyBatis提供的这两种管理机制来管理事务

  • JdbcTransaction:使用Jdbc中的java.sql.Connection来管理事务,包括提交回滚
  • ManagedTransaction:在这种机制下,MyBatis不会管理事务,而是交由程序的运行容器(weblogic,tomcat)来进行管理。

引用资料

  • https://www.jianshu.com/p/0cb1591d50ba