Mybatis 源碼分析(9)—— 事物管理

Mybatis 提供了事物的頂層接口:

public interface Transaction {

  /**
   * Retrieve inner database connection
   * @return DataBase connection
   * @throws SQLException
   */
  Connection getConnection() throws SQLException;

  /**
   * Commit inner database connection.
   * @throws SQLException
   */
  void commit() throws SQLException;

  /**
   * Rollback inner database connection.
   * @throws SQLException
   */
  void rollback() throws SQLException;

  /**
   * Close inner database connection.
   * @throws SQLException
   */
  void close() throws SQLException;

}

還有一個事物工廠:

public interface TransactionFactory {

  /**
   * Sets transaction factory custom properties.
   * @param props
   */
  void setProperties(Properties props);

  /**
   * Creates a {@link Transaction} out of an existing connection.
   * @param conn Existing database connection
   * @return Transaction
   * @since 3.1.0
   */
  Transaction newTransaction(Connection conn);

  /**
   * Creates a {@link Transaction} out of a datasource.
   * @param dataSource DataSource to take the connection from
   * @param level Desired isolation level
   * @param autoCommit Desired autocommit
   * @return Transaction
   * @since 3.1.0
   */
  Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);

}

對於這兩個接口我們一般是不直接操作的,但是它的影響是實實在在的。畢竟作爲一個 ORM 框架,事物的管理是少不了的。它的實現大致可以分爲兩類,非 Spring 相關的事物和基於 Spring 管理的事物。

非 Spring 相關

關於 JdbcTransaction 和 ManagedTransaction 這兩個實現就不多說了,實際上 getConnection 和 close 方法都是直接操作的 Connection。ManagedTransaction 的提交和回滾是個空的實現,交給容器了。

Mybatis對外提供的統一接口是SqlSession,通常情況下我們可以這樣使用:

public void doSomethingWithTemplate(){
    SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
    try {
        sqlSession = sqlSessionFactory.openSession();
        doSomething();
        sqlSession.commit();
    } catch (Exception e) {
        e.printStackTrace();
        sqlSession.rollback();
    } finally {
        if (sqlSession != null) {
            sqlSession.close();
        }
    }
}

DefaultSqlSession 持有 Executor,將事物相關的操作做了簡單的封裝,Executor 又在此基礎上加入了一級緩存等相關操作,如 commit 方法:

public void commit(boolean required) throws SQLException {
    if (closed) throw new ExecutorException("Cannot commit, transaction is already closed");
    clearLocalCache();
    flushStatements();
    if (required) {
       transaction.commit();
    }
}

最終都調用了Mybatis提供的事物接口相關方法,以 JdbcTransaction 爲例:

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

這個提交就是判斷連接不爲 null,而且不是自動提交的事物,那麼就調用 JDBC 連接的 commit 方法,回滾也是類似。

結合 Spring

在mybatis-spring中,提供了一個實現:

public class SpringManagedTransactionFactory implements TransactionFactory {

  public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
    return new SpringManagedTransaction(dataSource);
  }

  public Transaction newTransaction(Connection conn) {
    throw new UnsupportedOperationException("New Spring transactions require a DataSource");
  }

  public void setProperties(Properties props) {
    // not needed in this version
  }

}

在SqlSessionFactoryBean 中的 buildSqlSessionFactory 方法中指定了事物工廠爲 SpringManagedTransactionFactory,然後將其放到 Environment 實例中:

if (this.transactionFactory == null) {
    this.transactionFactory = new SpringManagedTransactionFactory();
}

Environment environment = new Environment(this.environment, this.transactionFactory, this.dataSource);
configuration.setEnvironment(environment);

//......

return this.sqlSessionFactoryBuilder.build(configuration);

最後返回了sqlSessionFactoryBuilder 的 build 方法構建的 SqlSessionFactory:

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

DefaultSqlSessionFactory#openSessionFromDataSource:

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);
      return new DefaultSqlSession(configuration, executor);
    } 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();
    }
}

這時候 DataSource 是 Spring 管理的,事物是 SpringManagedTransaction。連接是找 Spring 要的,跟蹤一下發現事物由 Spring 直接處理了。

管理與操作

不管有沒有 Spring,Mybatis 都要作爲一個獨立的 ORM 框架存在,所以事物管理是免不了的。Mybatis 定義了 Transaction 接口,對外提供的 SqlSession 間接操作了事物相關的接口。底層可以有不同的實現,處理起來更靈活。

JdbcTransaction 中的實現就是直接操作 JDBC Connection,ManagedTransaction 的實現就是爲空,不做任何處理。但是作爲和 Spring 結合使用的 SpringManagedTransaction,這個就有點複雜了。

都交給了 Spring,那麼它怎麼辦,Mybatis 底層也有依賴於事物的操作,如緩存。

SqlSessionUtils 中的 SqlSessionSynchronization 內部類繼承了 TransactionSynchronizationAdapter,當 Spring 提交或者回滾時就會通過這個來回調。具體可以在事物提交前和提交後做一些操作,來彌補事物不在 Mybatis 這一方帶來的缺憾。

誰來管理,誰來操作

看一下 SpringManagedTransaction 是怎麼獲取連接的:

private void openConnection() throws SQLException {
    this.connection = DataSourceUtils.getConnection(this.dataSource);
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional = isConnectionTransactional(this.connection, this.dataSource);

    if (this.logger.isDebugEnabled()) {
      this.logger.debug(
          "JDBC Connection ["
              + this.connection
              + "] will"
              + (this.isConnectionTransactional ? " " : " not ")
              + "be managed by Spring");
    }
}

再看下提交和回滾的實現:

public void commit() throws SQLException {
    if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
      if (this.logger.isDebugEnabled()) {
        this.logger.debug("Committing JDBC Connection [" + this.connection + "]");
      }
      this.connection.commit();
    }
}


public void rollback() throws SQLException {
    if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
      if (this.logger.isDebugEnabled()) {
        this.logger.debug("Rolling back JDBC Connection [" + this.connection + "]");
      }
      this.connection.rollback();
    }
}

最後是否提交和回滾還得依賴一系列判斷,其中 isConnectionTransactional 是這樣判斷的:

public static boolean isConnectionTransactional(Connection con, DataSource dataSource) {
    if (dataSource == null) {
        return false;
    }
    ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
    return (conHolder != null && connectionEquals(conHolder, con));
}

就是當前的JDBC連接是否是事務性的。如果是 Spring 管理的事物,這裏就返回 true。如果不是,還能留一手,在這裏補上一腳。

狀態的維護

既然事物是 Spring 管理,它如何管理呢?

就拿基於 AOP 的事物管理來說,切面都在 Service 層,考慮這樣一種情況,一個線程中可能同時存在多個事物:把一個基於 Servlet 的請求看作一個線程,調用了一個 Service ,而這個 Service 包含了多個相關 Mapper 的操作,這些操作必須存在於同一個事物中。

Mapper 方法的執行模板位於 SqlSessionTemplate:

private class SqlSessionInterceptor implements InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      final SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      try {
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
      }
    }
}

這個和我們客戶端的方法執行模板還是有區別的,第一個需要考慮的問題是:這個方法往上層追溯,必然是位於 Spring 的事物執行模板之中。那麼這裏的 SqlSession 的獲取和關閉就不能隨隨便便,必須跟着“上面”走。

查看源碼可以看到,不論是 SqlSession 的獲取還是關閉,都是基於當前事物的狀態判斷,而不是直接在 ThreadLocal 中拿或者直接創建一個新的,體現在代碼中就是:

SqlSessionHolder holder = (SqlSessionHolder) getResource(sessionFactory);

if (holder != null && holder.isSynchronizedWithTransaction()) {
   if (holder.getExecutorType() != executorType) {
       throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction");
   }

   holder.requested();

   if (logger.isDebugEnabled()) {
       logger.debug("Fetched SqlSession [" + holder.getSqlSession() + "] from current transaction");
   }

   return holder.getSqlSession();
}

if (logger.isDebugEnabled()) {
   logger.debug("Creating a new SqlSession");
}

SqlSession session = sessionFactory.openSession(executorType);

第一個就是 holder 不爲 null, 第二是要求在同一個事物中,這些判斷依賴於 ResourceHolderSupport 和 TransactionSynchronizationManager,這是比線程更細粒度的控制,來自 Spring 管理的事物狀態。

試想一下,作爲底層的框架,事物由 Spring 管理,Mybatis 如何知道事物是否開啓,如何判斷是否在同一個事物,而這些它不能不知道,畢竟 SqlSession 是它管理的,而 SqlSession 的生命週期又和事物息息相關。

上面舉了一個例子,如果多個 Mapper 存在於同一個事物中,那麼每次獲取的 SqlSession 必然是同一個,不會創建新的,這樣一級緩存也會發揮出功效。

如果多個請求最終調用同一個 Mapper 呢?這時候 Mapper 持有的 SqlSession 是同一個(SqlSessionTemplate),但實際在 invoke 方法中獲取 SqlSession 卻各不相同。

最後對 Mybatis 事物做一個總結:

image

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章