1. 程式人生 > >Spring資料庫事務的實現機制

Spring資料庫事務的實現機制

事務控制的核心——Connection

在開始之前,先讓我們回憶一下資料庫較原始的JDBC是怎麼管理事務的:


    //僅做演示,程式碼不完整,不完全規範
    try {
        con.setAutoCommit(false);
        statement1 = con.prepareStatement(sql);
        statement1.executeUpdate();
        statement2 = con.prepareStatement(sql1);
        statement2.executeUpdate();
        con.commit();
    } catch
(SQLException e) { try { con.rollback(); } catch (SQLException e1) { } }

可以很明顯的看到,JDBC框架下的事務控制是由connection完成的。因為不論是MyBatis還是MyBatis-Spring都是在JDBC框架基礎上的高層框架,所以他們的原理仍然應該是一致的,也就是說想控制事務,必須要控制Connection。

我們常說事務要切在Service層,所以連線需要在整個Service請求中都是同一個,不能變。

用AOP技術保持當前的Connection

Spring的事務管理就是使用AOP技術,通過對Service層設定切面,注入事務管理的邏輯。

Spring的事務管理切面配置採用了宣告式事務,最常用的兩種方法是 tx:Advicetx:annotation-driven 兩種方式。兩種方式的配置檔案解析器分別是: org.springframework.transaction.config.TxAdviceBeanDefinitionParserorg.springframework.transaction.config.AnnotationDrivenBeanDefinitionParser

細看其中的程式碼和配置內容,就會發現,不論哪種方式都會建立包含事務處理功能的動態代理。代理關聯的切面(Advice)類是 TransactionInterceptor

一起看下關鍵程式碼:

    protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
            throws Throwable {
        //......
        if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
            TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);//-----1.開啟事務
            Object retVal = null;
            try {
                retVal = invocation.proceedWithInvocation();//...2.執行被代理的請求
            }
            catch (Throwable ex) {
completeTransactionAfterThrowing(txInfo, ex);//...3.異常回滾
                throw ex;
            }
            finally {
                cleanupTransactionInfo(txInfo);
            }
            commitTransactionAfterReturning(txInfo);//...4.提交事務
            return retVal;
        }

        //......

上邊的程式碼裡是不是沒有看到TransactionManger和Connection?那麼這兩個東西的作用在哪裡呢?

TransactionManger是上述TransactionInterceptor的一個屬性(不嚴的說),主要作用是用來建立connection,建立之後的Connection會被儲存在TransactionInfo裡面。

TransactionInfo在上述程式碼片段中被後續傳遞給事務提交和事務回滾的程式碼。

Service層和Dao層共享Connection

我們都知道事務要切在Service層,也就是說上一節的切面只是在Service層有效,那麼Dao層怎麼獲取到Connection連線呢?

如果Service層和Dao層的連線不是一個連線那麼回滾和提交操作就等同於無效了!

這裡只用MyBatis來說明,其他的ORM框架實現原理基本也是一樣的。

要明白這一點需要先弄明白MyBatis本身的事務管理機制,可以參考唯一浩哥的博文MyBatis原始碼解析(三)——Transaction事務模組。MyBatis提供了兩種事務管理機制一種是自己內部用的JDBC模式,一種是支援代理給外部控制的MANAGED模式。

第二種模式下會把事務的交給外部控制,外部只需要提供一個實現了 org.apache.ibatis.transaction.Transaction 介面的控制類即可。一起來看一下Transaction需要提供哪些方法:

public interface Transaction {
  Connection getConnection() throws SQLException;
  void commit() throws SQLException;
  void rollback() throws SQLException;
  void close() throws SQLException;
}

注意裡邊的getConnection方法,也就是說MyBatis的連線也是交給外部來獲取的!!那麼只需要想辦法把Service層的Connection存起來,然後讓自己實現Transaction獲取到即可。

Spring採用的是ThreadLocal本地執行緒變數的技術來做到的,我們可以看下mybatis-spring的 org.mybatis.spring.transaction.SpringManagedTransaction 中getConnection的實現就明白了:

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

  private void openConnection() throws SQLException {
    this.connection = DataSourceUtils.getConnection(this.dataSource);//...1.關鍵點在這裡!!
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug(
          "JDBC Connection ["
              + this.connection
              + "] will"
              + (this.isConnectionTransactional ? " " : " not ")
              + "be managed by Spring");
    }
  }

其中 this.connection = DataSourceUtils.getConnection(this.dataSource); 一行就會從ThreadLocal中拿到Connection物件。

事務為什麼要切在Service層的理由

對於這個常識,有一點個人的理解:

事務的ACID要求事務要有原子性,也就是一個事務裡邊的多項DB操作要同時成功,同時失敗,成功一半的情況是不允許的。也就是說,一般需要事務的時候,都是包含多個功能單元的。那麼我們都放在一個Dao裡面就顯得不那麼職能分明,也就是不那麼符合設計原則的單一職責原則。