記一次 Mybatis 一級緩存清理無效引發的源碼走讀

今天對象在學習 Mybatis 時發現 org.apache.ibatis.session.SqlSession 對象的 clearCache() 方法並不能清理一級緩存, 同一 session 下相同查詢條件返回的結果仍是舊值。測試代碼以下html

clipboard.png

上網搜索

網上搜索找到了相同問題, 並無人解答。例如:https://www.iqismart.com/topi...java

查看官方文檔

http://www.mybatis.org/mybati...mysql

SqlSession 實例有一個本地緩存在執行 update,commit,rollback 和 close 時被清理。要明確地關閉它(獲取打算作更多的工做) ,你能夠調用 clearCache()。

看起來, 沒什麼問題, 方法也沒有被標記成廢棄.sql

打印詳細日誌

先把日誌配上, 看看有沒有打印什麼有用的信息, 添加 slf4j、logback 依賴,添加 logback.xml , 日誌級別設置爲 DEBUG 運行後未看到跟清理緩存有關的信息, 調整日誌級別爲 TRACE 後依舊沒有.數據庫

<configuration>
    <contextName>mybatis</contextName>
    <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger %msg%n</pattern>
        </encoder>
    </appender>
    <appender name="file" class="ch.qos.logback.core.FileAppender">
        <file>mybatis.log</file>
        <encoder>UTF-8</encoder>
        <encoder>
            <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="TRACE">
        <appender-ref ref="stdout"/>
        <appender-ref ref="file"/>
    </root>
</configuration>

看 clearCache() 源碼

上面方法都沒有收穫, 只能看源碼了.第一步, 先看一下 clearCache() 作了什麼, 下面會大規模貼圖apache

clipboard.png

clipboard.png

clipboard.png

注意 PerpetualCache 類的 cache 變量api

clipboard.png

org.apache.ibatis.cache.impl.PerpetualCache#cache 就是一個 HashMap緩存

到此, clearCache() 已經完結, 最終就是調用一個 HashMap 的 clear() 方法服務器

看 selectOne() 源碼

clipboard.png
這一步沒有什麼好看的, 就是封一層 selectList session

clipboard.png

第一個方法會間接調用第二個, 只是少了一個分頁相關的 RowBounds
把傳入的 statement 值變成 MappedStatement, 因爲不是咱們查看源碼的重點, 能夠直接跳過.

不過能夠學習到 Mybatis 實際上是把咱們寫的 xml 文件抽象成 MappedStatement , 在執行 sql 時須要先使用 statement (也就是咱們 xml 中 select 標籤中的 id) 去配置中 get 出整個 MappedStatement, MappedStatement 包含了 resultMaps 之類的, 一下子 sql 返回時映射結果集極可能要用到.

clipboard.png

這一步把 MappedStatement 變成 BoundSql, BoundSql 應該就是每條 SQL 的抽象.

還會根據 MappedStatement (xml 文件)、parameter (sql 參數)、rowBounds (分頁信息)、BoundSql (SQL) 生成一個 CacheKey (緩存 key) .

已經跟咱們想要了解的東西沾點邊了.

clipboard.png

這一步是取 MappedStatement 對象的 Cache , 暫時不知道是什麼緩存(多是二級緩存), 能夠知道的是和剛纔看 clearCache() 清理的不是同一個東西. 調試發現返回值是 null, 不關心繼續往下看

clipboard.png

這裏到了 BaseExecutor 類, 152 行會根據 CacheKey 從 localCache 獲取結果.
並且和 clearCache() 方法清理的是同一個緩存對象.
基本能夠肯定 Mybatis 就是在這裏從一級緩存獲取結果後返回, 須要重點關注.

階段性成果

反覆運行發現以下規律:

  • 若是第二次查詢前不加 sqlSession.clearCache(); 能夠從 localCache get 出結果
  • 若是第二次查詢前加 sqlSession.clearCache(); localCache get 結果爲空

由此能夠得出結論:

sqlSession.clearCache() 方法是有效的, 清理一級緩存後第二次查詢結果依然和第一次相同, 和 Mybatis 一級緩存並沒有關係.

既然如此, 要想知道結果, 只能繼續往下跟蹤, 看一級緩存爲空後, Mybatis 是怎麼處理的.

clipboard.png

能夠看出, 爲空後調用了 queryFromDatabase 方法,從方法名能夠理解, 會去數據庫查詢

clipboard.png

第 322 行先往一級緩存設置一個佔位符, 並沒有實際含義
第 324 行執行查詢動做, 須要重點關注
第 326 行根據緩存 key 清理一級緩存
第 328 行從新設置一級緩存
第 330 行看到一個面熟的東西, 在 clearCache() 時會同時清理 localCache 和 localOutputParameterCache, 若是執行的是存儲過程, 會把參數緩存起來

繼續跟蹤 doQuery 方法

clipboard.png

先是獲取 MappedStatement 的配置, 建立一個 StatementHandler, 加工成 JDBC 標準的 Statement , 這中間隱藏了無數細節, 仍是那句話, 不是咱們關心的重點, 繼續跟蹤 Query 方法

clipboard.png

通過 RoutingStatementHandler 路由分發, 到達 SimpleStatementHandler

clipboard.png

方法體只有三行
第一行拿出具體 SQL
第二行調用 statement.execute() 方法, 這裏已經到了 JDBC 驅動層, mysql 驅動包會幫咱們封裝請求包發送給 mysql 服務器並把響應結果映射到 jdbc 規範的對象中
第三行處理返回結果集

其實執行完 execute 方法, 就能夠從 PreparedStatement 對象 get 出想要的結果集, 但貿然 get 會影響 Mybatis 處理, 仍是繼續跟蹤 handleResultSets 方法吧

clipboard.png

方法一開始聲明瞭一個 multipleResults , 這個就是最終的結果集.
接着分別處理 ResultMap 和 ResultSet, 把 mysql 返回的結果按照 xml 中的規則映射成指定對象
因爲 xml 中的 select 並無定義 resultSets , 只關注上半部分便可, 斷點設在 198 行

clipboard.png

能夠看出 mysql 服務器返回的確實是舊值,

階段性成果

至此能夠肯定一級緩存清理無效的問題和應用沒有關係.

還能是什麼問題呢, 難道是事務的隔離級別致使的, 應用只是簡單的查詢, 連事務管理器都沒有配置, 要有問題也只能懷疑 mysql 服務器.

查詢數據庫的默認隔離級別

mysql> SELECT @@global.tx_isolation;
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ       |
+-----------------------+
1 row in set, 1 warning (0.00 sec)

mysql> SELECT @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ        |
+------------------------+
1 row in set, 1 warning (0.00 sec)

mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)

居然是"可重複讀", 好了, 緣由找到, 此貼終結.

解決

解決辦法就是把事務的默認隔離級別設置成 "讀已提交".

mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)

mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)
相關文章
相關標籤/搜索