文章目錄
-
-
- 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處理器。各自的作用如下:
- 接口代理:是為了簡化對Mybatis的使用,底層使用基于接口的動态代理實作。
- sql會話:提供了增删改查的基本API,業務邏輯交給執行器處理。
- 執行器:處理SQL請求、事務管理、批處理和維護緩存等。決定如何執行sql請求,然後交給JDBC處理器執行具體的sql。
- 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語句,每執行一次都會編譯一次。
接下來點進源碼中看看。看下doQuery是如何實作的。首先擷取配置資訊,根據配置資訊建構一個StatementHandler。然後調用prepareStatement來預編譯sql,建構新的statement。
跟着源碼再看看這做了些什麼。主要就是建構一個statement 然後給statementHandler設定參數。
點進prepare方法,底層是使用上篇部落格中jdbc中預編譯sql的代碼。connection.prepareStatement()來得到statement,然後設定逾時間和資料庫傳回行數。
而在parameterize方法中,可以看到這裡将Statement強制轉成了PreparedStatement。是以預設是使用的PreparedStatement來執行sql。這也是比較安全的,可以防止sql注入。
建構好了handler就會執行handler的query方法。這裡就先不細看handler是如何工作的了,之後再另寫一篇部落格來詳細的介紹StatementHandler。
綜上的源碼分析,可以驗證之前得出的結論,每處理一次會話當中的sql請求都會通過StatementHandler建構一個新的statment。
2.2 ReuseExecutor
看名字就知道這是一個重用執行器,那麼重用的是什麼東西呢?
我們将上面例子中的簡單執行器換成重用執行器,再執行一次看看有什麼差別。統一是執行兩個一樣的sql語句。結果如下
可以發現這裡隻預編譯了一次sql。也就是說在同一個會話中第二次執行相同sql會使用之前建構好的statement。
讓我們來看看源碼是如何實作的。debug調試進入doQuery方法。
結構上和SimpleExecutor沒什麼差別。那麼來看看裡面的方法有什麼差别。
下面是ReuseExecutor中的prepareStatement,它是如何獲得一個statement的呢?
這裡比簡單執行器多了一步判斷目前的sql語句是否在緩存中出現了,并且是在同一個會話下。若有則從緩存中擷取對應的statement不用再預編譯sql來獲得statement。沒有的話,則和簡單執行器一樣的方式建構。然後放入statementMap緩存中。sql語句作為key,statement作為value。
之後的邏輯就和簡單執行器一樣了。從源碼中也可以看出這樣做的效率會高一點.
綜上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個
- 相同的sql映射聲明,即MappedStatement相同
- 必須是連續的sql
- 相同的sql語句
看如下的測試代碼
-
驗證相同的MappedStatement
setName和setName2有相同的sql語句,但是沒有相同的MappedStatement。
執行前的資料庫如下:執行之後的控制台輸出和資料庫結果如下:
可以看到sql預編譯進行了兩次,前兩次滿足條件是以共用一個statement進行批處理。而第三個因為MappedStatement不相同是以無法進行批處理。
-
驗證必須連續
修改了上面的測試代碼,将兩個相同的sql和有相同的MappedStatement的代碼分割開了
執行的結果如下
可以看到進行了3次的預編譯,是以驗證了必須連續的才可以進行批處理。
-
嚴重sql必須相同
測試代碼如下:
setName和addUser執行不同的sql語句。這也是最好了解的必然是無法批處理的。
結果如下
2.4.1 批處理的效率
分别使用批處理執行器和重用執行器去執行添加100個新使用者,記錄時間,代碼如下
批處理用時326毫秒
對照實驗
多次單條執行用時588毫秒
可以看出批處理的效率更高。
2.4.2 批處理查詢
批處理提高效率僅對增删改有效果,對查詢沒效果。将剛才的兩組對照實驗修改for循環中的addUser方法改成mapper.selectByid(10);
執行的結果如下,幾乎沒有差别。
2.4.3 源碼實作
編寫如下測試代碼debug調試來看看源碼是如何實作的批處理
首先setName會執行到BatchExecutor中的doUpdate方法,在這裡打一斷點。
這裡有一個if判斷,就是判斷能否批處理的三個條件。
currentSql和currentStatement記錄的是上一條sql的資訊。
而現在是第一次進來是以這兩個變量都是null。必定是走else的邏輯。
else的邏輯會建構一個新的statament 然後并記錄下來現在的sql和statement。并将statement添加到statement隊尾,添加一個批處理結果集到結果集隊尾。
然後執行handler的batch
而這裡就是使用的jdbc的addBatch。第二條addUser代碼 也會走else的邏輯。
第三條addUser,因為滿足批處理的三個條件那麼會走if的邏輯。
if的邏輯中直接從statement隊列中拿出隊尾的statement,和結果集隊列中的隊尾的BatchResult。設定參數即可。
執行完所有的5次doUpdate方法後,有三個statement和三個batchResult
執行flushStatements進行批處理。真正的執行邏輯在BatchExecutor中的doFlushStatements,依次的拿出statement,執行批處理。
3. 總結
詳細介紹了三種Executor的特點和實作原理,做個簡單的總結。
-
SimpleExecutor
每處理一次會話當中的sql請求都會通過StatementHandler建構一個新的statment。
-
ReuseExecutor
在同一個會話中第二次執行相同sql會使用之前建構好的statement。就算是statement不一樣隻要在同一個會話中,sql語句相同即可。
-
BatchExecutor
批處理執行器,連續相同的SQL語句并且相同的SQL映射聲明會重用statement,執行批處理。
批處理僅對增删改有效,對查無效。
- 底層預設使用的PreparedStatement
4. 後續
關于Executor一級、二級緩存和事務相關的知識,下一篇部落格中介紹。