天天看點

mybatis原理分析(三)---一級緩存和二級緩存

文章目錄

      • 1.概述
        • 1.1 BaseExecutor
        • 1.2 CachingExecutor
      • 2.一級緩存
        • 2.1 一級緩存的命中場景
        • 2.2 觸發清空一級緩存
        • 2.3 一級緩存源碼分析
      • 3 二級緩存
        • 3.1 二級緩存的設計
        • 3.2 二級緩存的使用
        • 3.3 二級緩存的命中場景
        • 3.4 二級緩存源碼分析
          • 3.4.1 query查詢操作。
          • 3.4.2 commit送出操作。
          • 3.4.3 update操作
        • 3.5 為什麼隻有會話送出成功才會更新或清空二級緩存
      • 4. 後續

1.概述

上一篇部落格《mybatis原理分析(二)—深入了解Executor》中介紹了mybatis中的一個重要的元件Executor,還有兩個該元件的實作類沒有介紹,那就是BaseExecutor和CachingExecutor,它們分别去處理一級緩存的邏輯和二級緩存的邏輯。這篇部落客要就是介紹這兩種緩存的差别和命中場景,并且分析源碼來看看mybatis是如何實作的一級緩存和二級緩存。

1.1 BaseExecutor

BaseExecutor是上一篇部落格中介紹的三個Executor的父類,主要的功能就是維護一級緩存和對事務的管理。事務是通過會話中調用commit、rollback來管理。重點在于緩存這塊是如何處理的。它提供基本的api如query和update,在query方法中處理一級緩存的邏輯,即根據sql以及參數等條件來判斷緩存中是否有資料,有的話就走緩存,沒有的話就會調用子類的doQuery方法來執行查詢。而在update方法中,對于緩存方面的參與就是清空緩存。

mybatis原理分析(三)---一級緩存和二級緩存

1.2 CachingExecutor

CachingExecutor,處理二級緩存的邏輯,二級緩存預設是關閉的,而一級緩存預設是打開的且必須是打開的,之後會說明為什麼。

二級緩存可以根據參數來控制開啟或者關閉,它的實作是采用了裝飾者的方式,在CachingExecutor中聚合了一個BaseExecutor。處理完二級緩存的邏輯後,剩餘的邏輯就交給裡面的Executor來實作。看下面的測試代碼

mybatis原理分析(三)---一級緩存和二級緩存

将簡單執行器聚合到緩存執行器中,來執行兩次查詢,控制台的輸出内容如下:

mybatis原理分析(三)---一級緩存和二級緩存

第一次查詢緩存中沒有是以命中率為0.而第二次查詢的時候可以看到命中率變成了0.5 說明第二次查詢命中了緩存,兩次查詢中有一次查詢命中了緩存,算出命中率為0.5

若直接使用簡單緩存執行器的結果如下:

mybatis原理分析(三)---一級緩存和二級緩存

sql預編譯了兩次,并且執行了兩次sql查詢。不會走二級緩存。

2.一級緩存

一級緩存也叫做會話級緩存,生命周期僅存在于目前會話,不可以直接關關閉。但可以通過flushCache和localCacheScope對其做相應控制。一級緩存預設打開。

2.1 一級緩存的命中場景

因為緩存中的key是由以下參數組成的,是以緩存的命中條件就是以下參數構成的key在緩存中可以找到。

  • SQL與參數相同
  • 同一個會話
  • 相同的MapperStatement ID
  • RowBounds行範圍相同

2.2 觸發清空一級緩存

  1. 手動調用clearLocalCache,在BaseExecutor中有這個方法可以清空一級緩存
    mybatis原理分析(三)---一級緩存和二級緩存
  2. 執行送出復原,都會去調用上面的這個清空一級緩存的方法
    mybatis原理分析(三)---一級緩存和二級緩存
  3. 執行update操作
    mybatis原理分析(三)---一級緩存和二級緩存
  4. 配置flushCache=true 在子查詢的時候不會清空 清空緩存不能發生在子查詢裡面。
    mybatis原理分析(三)---一級緩存和二級緩存
    在query方法中有如下判斷,說明當flushCache設為true的時候,并且不是子查詢的時候,會清空一級緩存。queryStack表示子查詢遞歸的層數。
    mybatis原理分析(三)---一級緩存和二級緩存
  5. 緩存作用域改為Statement 同理 子查詢的時候不會清空

    同樣在query方法中有另一個判斷,如下:判斷緩存的作用與是不是statement否則,就會将其清空。

    mybatis原理分析(三)---一級緩存和二級緩存
    可以在mybatis的配置檔案中修改一級緩存的作用域 如下:
    mybatis原理分析(三)---一級緩存和二級緩存

4和5的差别是 4是在查詢前清空,5是在查詢後清空。

2.3 一級緩存源碼分析

如下的測試代碼,debug調試

mybatis原理分析(三)---一級緩存和二級緩存

首先進來的是BaseExecutor的方法,第一行代碼是動态綁定sql,這裡先不考慮是如何實作的,後面會當作一個專題寫一篇部落格來介紹。

第二行就是做的建立緩存key,點進去一探究進

mybatis原理分析(三)---一級緩存和二級緩存

就是将MapperStatement的id,分頁參數,sql語句,傳入的參數都儲存在cachekey中,這也說明了為什麼隻有當這些條件都一樣的時候,才會命中緩存。

mybatis原理分析(三)---一級緩存和二級緩存

建立好了緩存key 會去調用BaseExecutor中的另一個重載的query方法

mybatis原理分析(三)---一級緩存和二級緩存

看紅色的框框,這裡根據緩存key從localCache中擷取對象,這個localCache也就是一級緩存。localCache是一個PerpetualCache對象,裡面有一個cache Map集合。

mybatis原理分析(三)---一級緩存和二級緩存

緩存key作為鍵,查詢的結果作為value。

如果從緩存中拿到了結果 則将結果傳回,如果沒有則将執行queryFromDatabase去查詢資料庫。會将緩存key和結果集放入一級緩存中。

mybatis原理分析(三)---一級緩存和二級緩存

BaseExecutor中的query和queryFromDatabase 也是解決嵌套子查詢中循環依賴問題的關鍵。後面會單獨寫一篇部落格來講這個知識點。現在我們隻要關注于一級緩存是如何實作的即可。

3 二級緩存

二級緩存也稱作是應用級緩存,與一級緩存不同的,是它的作用範圍是整個應用,而且可以跨線程使用。是以二級緩存有更高的命中率,适合緩存一些修改較少的資料。在流程上是先通路二級緩存,在通路一級緩存。

3.1 二級緩存的設計

一個應用級别的緩存,需要考慮如何存儲緩存,溢出淘汰機制的設計等。

mybatis中,使用了責任鍊的設計模式,将這一串的邏輯,設計成可拔插,自由組裝的元件。責任鍊模式顧名思義就是每一個階段處理一個階段的責任,将責任細化,友善擴充和使用。

涉及到的元件如下:都是一個一個套娃的形式,内部聚合了下一個元件。這樣執行完這一層的邏輯可以交給下一個元件執行

事務元件TransactionalCache->同步元件SynchronizedCache->日志元件LoggingCache->序列化元件SerializedCache->移除淘汰元件LruCache->存儲政策元件PerpetualCache

mybatis原理分析(三)---一級緩存和二級緩存

從這一串預設的二級緩存的執行元件中可以看出,mybatis使用的預設的溢出淘汰機制是最近最少使用(LRU),使用的存儲政策是記憶體存儲。

仔細看看源碼裡每一個元件都幹了些什麼

  1. 調試源碼,可以發現上面這一串的邏輯是在執行了commit的時候進行的。commit處打個斷點。
    mybatis原理分析(三)---一級緩存和二級緩存
  2. CachingExecutor

    由CachingExecutor内部聚合的Executor去執行會話送出,然後送出事務緩存管理器的暫存區,進行更新二級緩存的操作。

    mybatis原理分析(三)---一級緩存和二級緩存
  3. TransactionalCacheManager

    管理二級緩存空間和對應的暫存區的關系,一一對應。有多少個二級緩存空間,每個線程内就有多少個暫存區。

    周遊暫存區,執行事務元件的commit方法

    mybatis原理分析(三)---一級緩存和二級緩存
  4. TransactionalCache

    判斷是否設定了送出後清空。然後執行flushPendingEntries,周遊緩存在事務緩存管理器的暫存區裡的對象。一個一個的執行下一個元件的putObject方法,也就是将暫存區裡的鍵值對都更新到二級緩存空間中。

    mybatis原理分析(三)---一級緩存和二級緩存
    mybatis原理分析(三)---一級緩存和二級緩存
  5. SynchronizedCache

    這個元件隻做一件事情,為每個方法做同步處理,加上了synchronized。因為二級緩存是線程共享的,是以需要做同步處理

    mybatis原理分析(三)---一級緩存和二級緩存
  6. LoggingCache

    這是日志元件,僅對getObject方法做了修改,列印出日志,計算輸出二級緩存的命中率

    mybatis原理分析(三)---一級緩存和二級緩存
  7. SerializedCache

    序列化元件,put的時候将查詢結果進行序列化,get的時候進行反序列化。因為二級緩存是跨線程使用的。若儲存在緩存中的對象不進行序列化的話,之後多個線程拿到的對象是同一個對象 這樣會有線程安全的問題。

    mybatis原理分析(三)---一級緩存和二級緩存
  8. LruCache

    溢出淘汰機制元件,預設是最近最少使用的淘汰掉。

    mybatis原理分析(三)---一級緩存和二級緩存
    mybatis原理分析(三)---一級緩存和二級緩存
  9. PerpetualCache

    存儲政策元件,采用記憶體存儲。

    mybatis原理分析(三)---一級緩存和二級緩存

3.2 二級緩存的使用

二級緩存預設是不開啟的,需要為其聲明緩存空間才可以使用,通過在mapper上設定注解

mybatis原理分析(三)---一級緩存和二級緩存

或者在mybatis的配置檔案中,設定打開二級緩存的支援

mybatis原理分析(三)---一級緩存和二級緩存

然後在mapper中設定

< cache/> 表示開啟這個mapper的二級緩存空間

值得注意的是,如果UserMapper中加了二級緩存的注解 并且UserMapper.xml中同時設定了< cache/> 将會報錯。需要将其中之一改成二級緩存的空間引用。例如在xml中将< cache/> 改成 指向注解聲明的緩存空間。

mybatis原理分析(三)---一級緩存和二級緩存

@CacheNamespace 注解詳細的配置資訊見下圖,說明可以對這個二級緩存做自定義。實作自定義的相關實作類。例如自定義的存儲實作類,自定義的緩存溢出淘汰機制。預設是最近最少使用。

mybatis原理分析(三)---一級緩存和二級緩存

3.3 二級緩存的命中場景

  1. 相同sql和參數
  2. 相同的statement的id
  3. RowBounds行範圍相同
  4. 會話送出之後

3.4 二級緩存源碼分析

3.4.1 query查詢操作。
  1. 首先執行的CachingExecutor的query方法,擷取動态綁定sql,建立緩存key然後執行另一個重載的query方法,緩存key和一級緩存中的一樣。
    mybatis原理分析(三)---一級緩存和二級緩存
  2. 這裡做的事情就比較多了。從statement中得到二級緩存空間。判斷二級緩存空間是否存在,也就是有沒有開啟二級緩存的支援。如果有,則進入第一個if。沒有的話就交給裡面BaseExecutor執行它的query邏輯。
    mybatis原理分析(三)---一級緩存和二級緩存
  3. flushCacheIfRequired(ms) 檢查是否設定了清空二級緩存的參數,如果是true,則執行清空二級緩存。預設的查詢相關的statement中的flushCacheRequired屬性是false,可以通過手動的将其設定為true,那麼在執行查詢的時候也會清空二級緩存。
  4. ensureNoOutParams 如果是callcabelStatement 不支援對出參的緩存。抛出異常
  5. tcm.getObject 從二級緩存中找是否存在緩存key對應的結果。如果此時還沒有建立二級緩存空間對應的暫存區,則執行建立。事務緩存管理器裡面維護了一個暫存區Map。二級緩存空間作為key,暫存區作為value。一一對應。并且将這個key,記錄下來,表明它沒有命中。

    TransactionalCache 中的 private final Set entriesMissedInCache;就是用來記錄沒有命中的緩存key

    mybatis原理分析(三)---一級緩存和二級緩存
  6. 如果此時二級緩存中沒有,則執行查詢操作。
  7. 将結果集,放入到事務緩存管理器的暫存區中。
    mybatis原理分析(三)---一級緩存和二級緩存
    mybatis原理分析(三)---一級緩存和二級緩存
    TransactionalCache 中的 private final Map<Object, Object> entriesToAddOnCommit //暫存區,key是之前建立出來的緩存key,value是對應的查詢結果。
3.4.2 commit送出操作。
  1. 首先來到CachingExecutor 中的comit方法 主要做兩件事情,一是将會話送出,清空一級緩存。二是送出事務緩存管理器中的所有暫存區,進行更新二級緩存的操作。
    mybatis原理分析(三)---一級緩存和二級緩存
  2. TransactionalCacheManager 中的commit

    會話送出這裡不考慮,看看事務緩存管理器中的commit方法

    周遊暫存區,對每一個暫存區執行送出操作。

    mybatis原理分析(三)---一級緩存和二級緩存
  3. TransactionalCache 中的commit

    執行flushPendingEntries 和 reset

    mybatis原理分析(三)---一級緩存和二級緩存
  4. flushPendingEntries

    周遊暫存區所有的鍵值對,全部放到二級緩存空間中。然後周遊之前存放的沒有命中二級緩存中的緩存key。如果這個鍵在暫存區沒有出現過。則将這個鍵和null值送出給二級緩存空間。這樣做的目的是為了避免無效的查詢,多次的去查詢資料庫,而造成資料庫很大的壓力負擔。也就是解決緩存穿透的問題。

    mybatis原理分析(三)---一級緩存和二級緩存
3.4.3 update操作
  1. 首先來到的是CachingExecutor中的update方法,做兩件事情,清空暫存區并設定送出清空标記為true。交給baseExecutor執行它的update邏輯。
    mybatis原理分析(三)---一級緩存和二級緩存
  2. 檢查清空緩存标記是否為true,對于update相關的statement中的flushCacheRequired屬性預設是true,是以對于update操作必然會清空二級緩存。而Select相關的statement中的flushCacheRequired屬性預設是false。如果将這個屬性手動的設定為true,則也會執行清空二級緩存的操作。讓我們看看緩存事務管理器中的clear是做了哪些事情
    mybatis原理分析(三)---一級緩存和二級緩存
  3. 根據二級緩存空間拿到對應的暫存區,執行clear
    mybatis原理分析(三)---一級緩存和二級緩存
  4. 設定送出時清空标記為true,然後清空暫存區。可以看到此時并沒有真正的對二級緩存空間進行清空,而僅僅是設定了送出時清空标記,等到執行commit的時候,才會清空二級緩存空間。
    mybatis原理分析(三)---一級緩存和二級緩存

3.5 為什麼隻有會話送出成功才會更新或清空二級緩存

假設現在資料庫中有一行資料,name=gongsenlin,age=18

此時第一個線程,執行了update操作,将age更新成了16,并且執行了select方法,若不是按照mybatis的設計思路送出後才更新二級緩存,而是直接在查詢後就更新二級緩存。

那麼此時第二個線程執行select方法,會看到二級緩存中有資料,則直接從二級緩存中擷取,會查詢到name=gongsenlin,age=16。

但是此時第一個線程突然出現了錯誤,復原了,之前的更新操作失效了。那麼此時資料庫中的資料又回到了name=gongsenlin,age=18。

但是第二個線程卻讀到了age=16,造成了髒讀。是以mybatis是為了避免多線程髒讀的出現,才設計成送出後更新或清空二級緩存。

4. 後續

下一篇部落格介紹mybatis中的Jdbc處理器—StatementHandler。