MyBatis源码阅读(十一) --- MyBatis事务管理机制

2024-01-10 10:15:10 浏览数 (2)

一、概述

事务的概念,大家都不会陌生。在我们写增删改的时候,我们肯定都需要加上事务,来保证数据的一致性。MyBatis作为Java语言的数据库框架,对数据库的事务管理是其非常重要的一个方面。在Mybatis中,同样提供了事务的功能,所以我们有必要了解一下MyBatis的事务管理的实现机制。

二、Mybatis事务管理机制

Mybatis也提供了事务的机制,MyBatis将事务抽象成了Transaction接口。Transaction接口定义如下:

代码语言:javascript复制
//包装数据库连接, 处理connection连接的生命周期,包括:它的创建、准备、提交/回滚和关闭。
public interface Transaction {
 
  /**
   * 获取数据库连接
   */
  Connection getConnection() throws SQLException;
 
  /**
   * 提交
   */
  void commit() throws SQLException;
 
  /**
   * 回滚
   */
  void rollback() throws SQLException;
 
  /**
   * 关闭数据库连接
   */
  void close() throws SQLException;
 
  /**
   * 获取事务超时时间
   */
  Integer getTimeout() throws SQLException;
 
}

Transaction的继承关系图:

如上图,Transaction有两个实现子类,对应着MyBatis的事务管理的两种形式:

  • JdbcTransaction:直接使用JDBC提交和回滚功能,依赖于从数据源获取到的连接【java.sql.Connection对象】来管理事务;
  • ManagedTransaction:这种机制MyBatis自身不会去实现事务管理,而是让程序的容器如(JBOSS,Weblogic)来实现对事务的管理;

事务的配置,在Mybatis全局配置文件中,我们进行了事务管理器相关的配置,它的作用就是生成一个事务工厂,然后事务工厂用于创建事务对象。

代码语言:javascript复制
<environments default="development">
    <environment id="development">
        //指定事务管理器是Jdbc类型
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="com.mysql.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://127.0.0.1:3306/user_mybatis?serverTimezone=UTC"/>
            <property name="username" value="root"/>
            <property name="password" value="root"/>
        </dataSource>
    </environment>
</environments>

<environment节点定义了连接某个数据库的信息,其子节点<transactionManager 的type 会决定具体使用什么类型的事务管理机制。

配置完后,在解析XML的时候,也就是在XMLConfigBuilder类的parseConfiguration方法里面调用了environmentsElement方法解析environment标签内配置的信息,包括事务管理器和数据源的解析:

代码语言:javascript复制
//解析environments
private void environmentsElement(XNode context) throws Exception {
  if (context != null) {
    if (environment == null) {
      //development  
      environment = context.getStringAttribute("default");
    }
    //循环遍历XML各个节点
    for (XNode child : context.getChildren()) {
       //获取ID属性,对应<environment id="development">的ID属性, 即id=development
      String id = child.getStringAttribute("id");
      if (isSpecifiedEnvironment(id)) {
        //获取transactionManager事务管理器相关配置
        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对象    
        configuration.setEnvironment(environmentBuilder.build());
      }
    }
  }
}

到这里,事务相关的配置已经解析完成,下一步就要看一下事务是在哪里创建的。

如果看了前面总结的sqlSession获取流程的小伙伴一下就能想到,事务其实是在获取sqlSession的会话中创建的,我们来看看:

代码语言:javascript复制
// 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();
    }
  }

如上我们可以看到,在配置文件中我们将<transactionManager的type配置为"JDBC",所以在MyBatis初始化解析<environment节点时,会根据type="JDBC"创建一个JdbcTransactionFactory工厂,然后根据事务工厂创建一个Transaction事务对象,接着又根据这个事务对象,创建具体的Executor,最终创建出创建SqlSession对象。

下面我们看一下TransactionFactory的源码:

代码语言:javascript复制
public interface TransactionFactory {
 
  /**
   * 设置事务工厂相关属性
   */
  void setProperties(Properties props);
 
  /**
   * 从已有的连接中创建Transaction对象
   */
  Transaction newTransaction(Connection conn);
 
  /**
   * 根据数据源,数据库隔离级别,自动提交创建Transaction对象
   * @param dataSource 数据源信息
   * @param 数据库隔离级别
   * @param 是否自动提交
   * @return Transaction
   * @since 3.1.0
   */
  Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);
 
}

TransactionFactory默认有两个实现子类:

  • JdbcTransactionFactory:创建JdbcTransaction事务对象;
  • ManagedTransactionFactory:创建ManagedTransaction事务对象;

通过事务工厂TransactionFactory很容易获取到Transaction对象实例,我们以JdbcTransaction为例,看一下JdbcTransactionFactory是怎样生成JdbcTransaction的,代码如下:

代码语言:javascript复制
public class JdbcTransactionFactory implements TransactionFactory {
 
  @Override
  public void setProperties(Properties props) {
  }
 
  @Override
  public Transaction newTransaction(Connection conn) {
     //创建JdbcTransaction事务
    return new JdbcTransaction(conn);
  }
 
  @Override
  public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
     //创建JdbcTransaction事务,传入数据源、数据库隔离级别、是否自动提交属性
    return new JdbcTransaction(ds, level, autoCommit);
  }
}

简单理解,JdbcTransaction只是相当于对java.sql.Connection事务处理进行了一次包装,利用connection的commit()、rollback()、close()来实现对事务的管理。

代码语言:javascript复制
//直接使用JDBC提交和回滚功能。它依赖于从数据源获取到的连接来管理事务的范围
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 autoCommmit;
  
  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()
      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()
      connection.rollback();
    }
  }
 
  @Override
  public void close() throws SQLException {
    if (connection != null) {
      resetAutoCommit();
      if (log.isDebugEnabled()) {
        log.debug("Closing JDBC Connection ["   connection   "]");
      }
      //使用connection的close()
      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提交和回滚功能,依赖于从数据源获取到的连接来管理事务的范围。设置为自动提交时会忽略提交或回滚请求。
  • ManagedTransaction:MyBatis不会管理事务,这使得程序的运行容器能够管理事务的整个生命周期。这种情况下它会忽略所有提交或回滚请求。

Mybatis的事务管理这一块相对来说没有其他复杂,基本上都是封装了原生JDBC的connection连接来实现事务的管理。当然,如果我们集成到Spring以后,基本上也是使用Spring提供的事务,更加强大更加好用。

鉴于笔者水平有限,如果文章有什么错误或者需要补充的,希望小伙伴们指出来,希望这篇文章对大家有帮助。

0 人点赞