關於Spring+Mybatis事務管理中資料來源的思考

凌風郎少發表於2019-02-26

之前被同事問了一個問題:在我們的工程裡,事務的開啟跟關閉是由Spring負責的,但具體的SQL語句卻是由Mybatis執行的。那麼問題來了,Mybatis怎麼保證自己執行的SQL語句是處在Spring的事務上下文中?

原文地址:www.jianshu.com/p/6a880d20a…

注:這篇文章重點不是分析Spring事務的實現原理,但卻需要讀者提前瞭解Spring事務原理的一些知識點,這樣讀起來才會容易些

現在公司主流的開發框架大部分是使用spring+mybatis來運算元據庫,所有的事務操作都是交給spring去管理。當我們需要一個有事務上下文的資料庫操作時,我們的做法就是寫一個運算元據庫的方法,並在該方法上面加上@Transactional註解就可以了。

仔細思考一下這個過程,@Transactional是由spring進行處理的,spring做的事情是從資料來源(一般為資料庫連線池,比如說druid,c3p0等)獲取一個資料庫連線,然後在進入方法邏輯前執行setAutoCommit(false)操作,最後在處理成功或者出現異常的時候分別執行commit或者rollback操作。

那麼問題來了,開啟跟結束事務是由spring獲取到資料庫連線以後進行操作的,但我們實際執行的update或者insert語句卻是由mybatis獲取資料庫連線進行操作的,可以想到如果想讓事務生效,那麼spring跟mybatis使用的必須是同一個連線,真實情況是什麼樣呢?它們之間如何進行無縫銜接?讓我們通過原始碼來分析一下。

首先如果想在spring中使用mybatis,我們除了引入mybatis依賴以外,還需要引入一個包:mybatis-spring。

<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis-spring</artifactId>
  <version>x.x.x</version>
</dependency>複製程式碼

可以猜測這個依賴包應該就是Spring跟Mybatis進行無縫連線的關鍵。

一般來說,我們在工程中的配置檔案往往是這樣:

<!--會話工廠 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
</bean>

<!--spring事務管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource" />
</bean>

<!--使用註釋事務 -->
<tx:annotation-driven  transaction-manager="transactionManager" />複製程式碼

注:
1.會話工廠sqlSessionFactory跟Spring事務管理器transactionManager所使用的資料來源dataSource必須是同一個。
2.這裡的sqlSessionFactory型別是org.mybatis.spring.SqlSessionFactoryBean,該類是由我們引入的包mybatis-spring提供的。

看名字就知道SqlSessionFactoryBean是一個工廠bean,也就是說它交給Spring的真正例項是由getObject()方法提供的,那麼我們去看下它真正例項初始化原始碼:

@Override
public SqlSessionFactory getObject() throws Exception {
    if (this.sqlSessionFactory == null) {
      //可以看出邏輯都在這裡面
      afterPropertiesSet();
    }
    return this.sqlSessionFactory;
}

@Override
public void afterPropertiesSet() throws Exception {
    //此處省略一些校驗邏輯
    //...
    this.sqlSessionFactory = buildSqlSessionFactory();
}

//最後來看這個最核心的方法
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
    //...
    //省略一些其他初始化資訊,我們重點關注事務處理邏輯

    if (this.transactionFactory == null) {
      //可以看出,mybatis中把事務操作交給了SpringManagedTransactionFactory去做
      this.transactionFactory = new SpringManagedTransactionFactory();
    }

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

   //省略後續邏輯
   //...
}複製程式碼

下面我們再去看看SpringManagedTransactionFactory類的原始碼:

public class SpringManagedTransactionFactory implements TransactionFactory {

  /**
   * {@inheritDoc}
   */
  @Override
  public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
    return new SpringManagedTransaction(dataSource);
  }

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

  /**
   * {@inheritDoc}
   */
  @Override
  public void setProperties(Properties props) {
    // not needed in this version
  }

}複製程式碼

程式碼很少,且只有一個方法是有效的,看來離成功越來越近了,繼續跟進去看看SpringManagedTransaction的原始碼:

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

  private void openConnection() throws SQLException {
    this.connection = DataSourceUtils.getConnection(this.dataSource);
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
  }複製程式碼

省略該類中其他部分,我們重點看獲取連線的地方,這裡最關鍵的地方就在this.connection = DataSourceUtils.getConnection(this.dataSource);
DataSourceUtils全名是org.springframework.jdbc.datasource.DataSourceUtils,沒錯,它是由Spring提供的類,根據我們之前的猜測,Spring開啟事務以後,Mybatis要想讓自己的SQL語句處在這個事務上下文中操作,那必須拿到跟Spring開啟事務同一個資料庫連線才行,由於DataSourceUtils類是由Spring提供的,看來跟我們開始猜測的結果類似,我們接下來看看DataSourceUtils原始碼驗證一下:

//獲取資料庫連線最終落在該方法上,我刪除一些不重要的程式碼
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
        //TransactionSynchronizationManager重點!!!有沒有很熟悉的感覺??
        ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);
        if (conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) {
            Connection con = fetchConnection(dataSource);
            if (TransactionSynchronizationManager.isSynchronizationActive()) {
                ConnectionHolder holderToUse = conHolder;
                if (conHolder == null) {
                    holderToUse = new ConnectionHolder(con);
                } else {
                    conHolder.setConnection(con);
                }

                holderToUse.requested();
                TransactionSynchronizationManager.registerSynchronization(new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource));
                holderToUse.setSynchronizedWithTransaction(true);
                if (holderToUse != conHolder) {
                    TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
                }
            }
            return con;
        } else {
            conHolder.requested();
            if (!conHolder.hasConnection()) {
                conHolder.setConnection(fetchConnection(dataSource));
            }

            return conHolder.getConnection();
        }
    }複製程式碼

看到TransactionSynchronizationManager有沒有很親切的感覺?對Spring事務管理原始碼熟悉的同學會馬上聯想到Spring開啟事務以後,就是把相應的資料庫連線放在這裡,我擷取原始碼看一下:

if (txObject.isNewConnectionHolder()) {
    TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}複製程式碼

這段程式碼具體就是在我們上面配置的org.springframework.jdbc.datasource.DataSourceTransactionManager類中的doBegin方法裡。至於TransactionSynchronizationManager類的實現原理其實我覺得你已經猜到了,沒錯,就是Java中經典類庫ThreadLocal類!!!

最後補上一張圖來說明spring+mybatis事務過程資料來源獲取邏輯:

Spring-Mybatis事務處理過程
Spring-Mybatis事務處理過程

相關文章