天天看點

mybatis原理分析(二)---深入了解Executor

文章目錄

      • 1.概述
      • 2.Executor相關概念
        • 2.1 SimpleExecutor
        • 2.2 ReuseExecutor
        • 2.4 BatchExecutor
          • 2.4.1 批處理的效率
          • 2.4.2 批處理查詢
          • 2.4.3 源碼實作
      • 3. 總結
      • 4. 後續

1.概述

《mybatis源碼分析(一)—JDBC》中回顧了JDBC的使用和特點,本篇部落格将介紹mybatis中一個重要的元件Executor。

可以簡單的将mybatis的執行過程分成4個階段:接口代理、sql會話、執行器、JDBC處理器。各自的作用如下:

  1. 接口代理:是為了簡化對Mybatis的使用,底層使用基于接口的動态代理實作。
  2. sql會話:提供了增删改查的基本API,業務邏輯交給執行器處理。
  3. 執行器:處理SQL請求、事務管理、批處理和維護緩存等。決定如何執行sql請求,然後交給JDBC處理器執行具體的sql。
  4. JDBC處理器:上篇部落格中說明了JDBC用于處理和執行sql語句。在會話中每調用一次增删改查,都會生成一個執行個體與之對應,除非命中緩存。

在一次會話中,這四個元件的執行個體比例是1:1:1:n

并且這些元件都不是線程安全的,不能跨線程使用。

當一個SQL請求通過會話到達執行器後,然後交給對應的JDBC處理器進行處理。

2.Executor相關概念

Executor是Mybatis執行者接口,他包含的功能有:

  • 基本功能:改、查,沒有增删是因為所有的增删操作都可以歸結為改。
  • 緩存維護:包括建立緩存Key、清理緩存、判斷緩存是否存在。
  • 事務管理:送出、復原、關閉、批處理重新整理。

Executor有6個實作類,這裡先介紹三個重要的實作子類。分别是:SimpleExecutor(簡單執行器)、ReuseExecutor(重用執行器)、BatchExecutor(批處理執行器)。

2.1 SimpleExecutor

是mybatis預設的執行器,它每處理一次會話當中的sql請求都會通過StatementHandler建構一個新的statment。例如下面的例子:

@Before
public void init() {
  // 1.擷取建構器
  SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
  // 2.擷取配置檔案的流資訊
  InputStream resourceAsStream = ExecutorTest.class.getResourceAsStream("/mybatis-config.xml");
  // 3.解析XML 并構造會話工廠
  sqlSessionFactory = factoryBuilder.build(resourceAsStream);
  // 4.擷取工廠配置
  configuration = sqlSessionFactory.getConfiguration();
  // 5.建構jdbc事務
  jdbcTransaction = new JdbcTransaction(sqlSessionFactory.openSession().getConnection());
  // 6.擷取Mapper映射
  mappedStatement = configuration.getMappedStatement("com.gongsenlin.executor.dao.UserMapper.selectByid");
}

@Test//簡單執行器
public void simpleTest() throws SQLException {
  SimpleExecutor simpleExecutor = new SimpleExecutor(configuration, jdbcTransaction);
  // 就算是兩個一樣的sql語句,但每次執行都會進行編譯
  List<Object> list = simpleExecutor.doQuery(mappedStatement, 10, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, mappedStatement.getBoundSql(10));
  simpleExecutor.doQuery(mappedStatement, 10, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, mappedStatement.getBoundSql(10));
  System.out.println(list.get(0));
}
           

simpleTest執行的結果如下,可以看到相同的sql語句,每執行一次都會編譯一次。

mybatis原理分析(二)---深入了解Executor

接下來點進源碼中看看。看下doQuery是如何實作的。首先擷取配置資訊,根據配置資訊建構一個StatementHandler。然後調用prepareStatement來預編譯sql,建構新的statement。

mybatis原理分析(二)---深入了解Executor

跟着源碼再看看這做了些什麼。主要就是建構一個statement 然後給statementHandler設定參數。

mybatis原理分析(二)---深入了解Executor

點進prepare方法,底層是使用上篇部落格中jdbc中預編譯sql的代碼。connection.prepareStatement()來得到statement,然後設定逾時間和資料庫傳回行數。

mybatis原理分析(二)---深入了解Executor
mybatis原理分析(二)---深入了解Executor

而在parameterize方法中,可以看到這裡将Statement強制轉成了PreparedStatement。是以預設是使用的PreparedStatement來執行sql。這也是比較安全的,可以防止sql注入。

mybatis原理分析(二)---深入了解Executor

建構好了handler就會執行handler的query方法。這裡就先不細看handler是如何工作的了,之後再另寫一篇部落格來詳細的介紹StatementHandler。

綜上的源碼分析,可以驗證之前得出的結論,每處理一次會話當中的sql請求都會通過StatementHandler建構一個新的statment。

2.2 ReuseExecutor

看名字就知道這是一個重用執行器,那麼重用的是什麼東西呢?

我們将上面例子中的簡單執行器換成重用執行器,再執行一次看看有什麼差別。統一是執行兩個一樣的sql語句。結果如下

可以發現這裡隻預編譯了一次sql。也就是說在同一個會話中第二次執行相同sql會使用之前建構好的statement。

mybatis原理分析(二)---深入了解Executor

讓我們來看看源碼是如何實作的。debug調試進入doQuery方法。

結構上和SimpleExecutor沒什麼差別。那麼來看看裡面的方法有什麼差别。

mybatis原理分析(二)---深入了解Executor

下面是ReuseExecutor中的prepareStatement,它是如何獲得一個statement的呢?

這裡比簡單執行器多了一步判斷目前的sql語句是否在緩存中出現了,并且是在同一個會話下。若有則從緩存中擷取對應的statement不用再預編譯sql來獲得statement。沒有的話,則和簡單執行器一樣的方式建構。然後放入statementMap緩存中。sql語句作為key,statement作為value。

mybatis原理分析(二)---深入了解Executor

之後的邏輯就和簡單執行器一樣了。從源碼中也可以看出這樣做的效率會高一點.

綜上ReuseExecutor 差別在于他會将在會話期間内的Statement進行緩存(Map<String, Statement> statementMap),并使用SQL語句作為Key。是以當執行下一請求的時候,不在重複建構Statement,而是從緩存中取出并設定參數,然後執行。

就算是兩個不同的方法,對應的兩個MapperedStatement不一樣,但是sql語句一樣的話,不在重複建構Statement而是使用同一個jdbc中的statement。這也說明了為什麼不能跨線程使用,因為多個線程可能會給同一個statement設定參數。

2.4 BatchExecutor

BatchExecutor 顧名思議,它就是用來作批處理的。但會将所有SQL請求集中起來,最後調用Executor.flushStatements() 方法時一次性将所有請求發送至資料庫。

這裡它是利用了Statement中的addBath機制嗎?

不一定,因為隻有連續相同的SQL語句并且相同的SQL映射聲明,才會重用Statement,并利用其批處理功能。否則會建構一個新的Satement然後在flushStatements() 時一起執行。這麼做的原因是它要保證執行順序。跟調用順序一至。

能進行批處理的條件有3個

  1. 相同的sql映射聲明,即MappedStatement相同
  2. 必須是連續的sql
  3. 相同的sql語句

看如下的測試代碼

  1. 驗證相同的MappedStatement

    setName和setName2有相同的sql語句,但是沒有相同的MappedStatement。

    mybatis原理分析(二)---深入了解Executor
    mybatis原理分析(二)---深入了解Executor
    執行前的資料庫如下:
    mybatis原理分析(二)---深入了解Executor

    執行之後的控制台輸出和資料庫結果如下:

    可以看到sql預編譯進行了兩次,前兩次滿足條件是以共用一個statement進行批處理。而第三個因為MappedStatement不相同是以無法進行批處理。

    mybatis原理分析(二)---深入了解Executor
  2. 驗證必須連續

    修改了上面的測試代碼,将兩個相同的sql和有相同的MappedStatement的代碼分割開了

    mybatis原理分析(二)---深入了解Executor

    執行的結果如下

    可以看到進行了3次的預編譯,是以驗證了必須連續的才可以進行批處理。

    mybatis原理分析(二)---深入了解Executor
  3. 嚴重sql必須相同

    測試代碼如下:

    ​ setName和addUser執行不同的sql語句。這也是最好了解的必然是無法批處理的。

    mybatis原理分析(二)---深入了解Executor
    mybatis原理分析(二)---深入了解Executor
    結果如下
    mybatis原理分析(二)---深入了解Executor
2.4.1 批處理的效率

分别使用批處理執行器和重用執行器去執行添加100個新使用者,記錄時間,代碼如下

mybatis原理分析(二)---深入了解Executor

批處理用時326毫秒

mybatis原理分析(二)---深入了解Executor

對照實驗

mybatis原理分析(二)---深入了解Executor

多次單條執行用時588毫秒

mybatis原理分析(二)---深入了解Executor

可以看出批處理的效率更高。

2.4.2 批處理查詢

批處理提高效率僅對增删改有效果,對查詢沒效果。将剛才的兩組對照實驗修改for循環中的addUser方法改成mapper.selectByid(10);

執行的結果如下,幾乎沒有差别。

mybatis原理分析(二)---深入了解Executor
mybatis原理分析(二)---深入了解Executor
2.4.3 源碼實作

編寫如下測試代碼debug調試來看看源碼是如何實作的批處理

mybatis原理分析(二)---深入了解Executor

首先setName會執行到BatchExecutor中的doUpdate方法,在這裡打一斷點。

mybatis原理分析(二)---深入了解Executor

這裡有一個if判斷,就是判斷能否批處理的三個條件。

currentSql和currentStatement記錄的是上一條sql的資訊。

而現在是第一次進來是以這兩個變量都是null。必定是走else的邏輯。

else的邏輯會建構一個新的statament 然後并記錄下來現在的sql和statement。并将statement添加到statement隊尾,添加一個批處理結果集到結果集隊尾。

然後執行handler的batch

mybatis原理分析(二)---深入了解Executor

而這裡就是使用的jdbc的addBatch。第二條addUser代碼 也會走else的邏輯。

第三條addUser,因為滿足批處理的三個條件那麼會走if的邏輯。

if的邏輯中直接從statement隊列中拿出隊尾的statement,和結果集隊列中的隊尾的BatchResult。設定參數即可。

執行完所有的5次doUpdate方法後,有三個statement和三個batchResult

mybatis原理分析(二)---深入了解Executor

執行flushStatements進行批處理。真正的執行邏輯在BatchExecutor中的doFlushStatements,依次的拿出statement,執行批處理。

3. 總結

詳細介紹了三種Executor的特點和實作原理,做個簡單的總結。

  1. SimpleExecutor

    每處理一次會話當中的sql請求都會通過StatementHandler建構一個新的statment。

  2. ReuseExecutor

    在同一個會話中第二次執行相同sql會使用之前建構好的statement。就算是statement不一樣隻要在同一個會話中,sql語句相同即可。

  3. BatchExecutor

    批處理執行器,連續相同的SQL語句并且相同的SQL映射聲明會重用statement,執行批處理。

    批處理僅對增删改有效,對查無效。

  4. 底層預設使用的PreparedStatement

4. 後續

關于Executor一級、二級緩存和事務相關的知識,下一篇部落格中介紹。