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)来进行管理。

引用资料