文章目錄
-
-
- 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方法中,對于緩存方面的參與就是清空緩存。
1.2 CachingExecutor
CachingExecutor,處理二級緩存的邏輯,二級緩存預設是關閉的,而一級緩存預設是打開的且必須是打開的,之後會說明為什麼。
二級緩存可以根據參數來控制開啟或者關閉,它的實作是采用了裝飾者的方式,在CachingExecutor中聚合了一個BaseExecutor。處理完二級緩存的邏輯後,剩餘的邏輯就交給裡面的Executor來實作。看下面的測試代碼
将簡單執行器聚合到緩存執行器中,來執行兩次查詢,控制台的輸出内容如下:
第一次查詢緩存中沒有是以命中率為0.而第二次查詢的時候可以看到命中率變成了0.5 說明第二次查詢命中了緩存,兩次查詢中有一次查詢命中了緩存,算出命中率為0.5
若直接使用簡單緩存執行器的結果如下:
sql預編譯了兩次,并且執行了兩次sql查詢。不會走二級緩存。
2.一級緩存
一級緩存也叫做會話級緩存,生命周期僅存在于目前會話,不可以直接關關閉。但可以通過flushCache和localCacheScope對其做相應控制。一級緩存預設打開。
2.1 一級緩存的命中場景
因為緩存中的key是由以下參數組成的,是以緩存的命中條件就是以下參數構成的key在緩存中可以找到。
- SQL與參數相同
- 同一個會話
- 相同的MapperStatement ID
- RowBounds行範圍相同
2.2 觸發清空一級緩存
- 手動調用clearLocalCache,在BaseExecutor中有這個方法可以清空一級緩存
- 執行送出復原,都會去調用上面的這個清空一級緩存的方法
- 執行update操作
- 配置flushCache=true 在子查詢的時候不會清空 清空緩存不能發生在子查詢裡面。 在query方法中有如下判斷,說明當flushCache設為true的時候,并且不是子查詢的時候,會清空一級緩存。queryStack表示子查詢遞歸的層數。
-
緩存作用域改為Statement 同理 子查詢的時候不會清空
同樣在query方法中有另一個判斷,如下:判斷緩存的作用與是不是statement否則,就會将其清空。
可以在mybatis的配置檔案中修改一級緩存的作用域 如下:
4和5的差别是 4是在查詢前清空,5是在查詢後清空。
2.3 一級緩存源碼分析
如下的測試代碼,debug調試
首先進來的是BaseExecutor的方法,第一行代碼是動态綁定sql,這裡先不考慮是如何實作的,後面會當作一個專題寫一篇部落格來介紹。
第二行就是做的建立緩存key,點進去一探究進
就是将MapperStatement的id,分頁參數,sql語句,傳入的參數都儲存在cachekey中,這也說明了為什麼隻有當這些條件都一樣的時候,才會命中緩存。
建立好了緩存key 會去調用BaseExecutor中的另一個重載的query方法
看紅色的框框,這裡根據緩存key從localCache中擷取對象,這個localCache也就是一級緩存。localCache是一個PerpetualCache對象,裡面有一個cache Map集合。
緩存key作為鍵,查詢的結果作為value。
如果從緩存中拿到了結果 則将結果傳回,如果沒有則将執行queryFromDatabase去查詢資料庫。會将緩存key和結果集放入一級緩存中。
BaseExecutor中的query和queryFromDatabase 也是解決嵌套子查詢中循環依賴問題的關鍵。後面會單獨寫一篇部落格來講這個知識點。現在我們隻要關注于一級緩存是如何實作的即可。
3 二級緩存
二級緩存也稱作是應用級緩存,與一級緩存不同的,是它的作用範圍是整個應用,而且可以跨線程使用。是以二級緩存有更高的命中率,适合緩存一些修改較少的資料。在流程上是先通路二級緩存,在通路一級緩存。
3.1 二級緩存的設計
一個應用級别的緩存,需要考慮如何存儲緩存,溢出淘汰機制的設計等。
mybatis中,使用了責任鍊的設計模式,将這一串的邏輯,設計成可拔插,自由組裝的元件。責任鍊模式顧名思義就是每一個階段處理一個階段的責任,将責任細化,友善擴充和使用。
涉及到的元件如下:都是一個一個套娃的形式,内部聚合了下一個元件。這樣執行完這一層的邏輯可以交給下一個元件執行
事務元件TransactionalCache->同步元件SynchronizedCache->日志元件LoggingCache->序列化元件SerializedCache->移除淘汰元件LruCache->存儲政策元件PerpetualCache
從這一串預設的二級緩存的執行元件中可以看出,mybatis使用的預設的溢出淘汰機制是最近最少使用(LRU),使用的存儲政策是記憶體存儲。
仔細看看源碼裡每一個元件都幹了些什麼
- 調試源碼,可以發現上面這一串的邏輯是在執行了commit的時候進行的。commit處打個斷點。
-
CachingExecutor
由CachingExecutor内部聚合的Executor去執行會話送出,然後送出事務緩存管理器的暫存區,進行更新二級緩存的操作。
-
TransactionalCacheManager
管理二級緩存空間和對應的暫存區的關系,一一對應。有多少個二級緩存空間,每個線程内就有多少個暫存區。
周遊暫存區,執行事務元件的commit方法
-
TransactionalCache
判斷是否設定了送出後清空。然後執行flushPendingEntries,周遊緩存在事務緩存管理器的暫存區裡的對象。一個一個的執行下一個元件的putObject方法,也就是将暫存區裡的鍵值對都更新到二級緩存空間中。
-
SynchronizedCache
這個元件隻做一件事情,為每個方法做同步處理,加上了synchronized。因為二級緩存是線程共享的,是以需要做同步處理
-
LoggingCache
這是日志元件,僅對getObject方法做了修改,列印出日志,計算輸出二級緩存的命中率
-
SerializedCache
序列化元件,put的時候将查詢結果進行序列化,get的時候進行反序列化。因為二級緩存是跨線程使用的。若儲存在緩存中的對象不進行序列化的話,之後多個線程拿到的對象是同一個對象 這樣會有線程安全的問題。
-
LruCache
溢出淘汰機制元件,預設是最近最少使用的淘汰掉。
-
PerpetualCache
存儲政策元件,采用記憶體存儲。
3.2 二級緩存的使用
二級緩存預設是不開啟的,需要為其聲明緩存空間才可以使用,通過在mapper上設定注解
或者在mybatis的配置檔案中,設定打開二級緩存的支援
然後在mapper中設定
< cache/> 表示開啟這個mapper的二級緩存空間
值得注意的是,如果UserMapper中加了二級緩存的注解 并且UserMapper.xml中同時設定了< cache/> 将會報錯。需要将其中之一改成二級緩存的空間引用。例如在xml中将< cache/> 改成 指向注解聲明的緩存空間。
@CacheNamespace 注解詳細的配置資訊見下圖,說明可以對這個二級緩存做自定義。實作自定義的相關實作類。例如自定義的存儲實作類,自定義的緩存溢出淘汰機制。預設是最近最少使用。
3.3 二級緩存的命中場景
- 相同sql和參數
- 相同的statement的id
- RowBounds行範圍相同
- 會話送出之後
3.4 二級緩存源碼分析
3.4.1 query查詢操作。
- 首先執行的CachingExecutor的query方法,擷取動态綁定sql,建立緩存key然後執行另一個重載的query方法,緩存key和一級緩存中的一樣。
- 這裡做的事情就比較多了。從statement中得到二級緩存空間。判斷二級緩存空間是否存在,也就是有沒有開啟二級緩存的支援。如果有,則進入第一個if。沒有的話就交給裡面BaseExecutor執行它的query邏輯。
- flushCacheIfRequired(ms) 檢查是否設定了清空二級緩存的參數,如果是true,則執行清空二級緩存。預設的查詢相關的statement中的flushCacheRequired屬性是false,可以通過手動的将其設定為true,那麼在執行查詢的時候也會清空二級緩存。
- ensureNoOutParams 如果是callcabelStatement 不支援對出參的緩存。抛出異常
-
tcm.getObject 從二級緩存中找是否存在緩存key對應的結果。如果此時還沒有建立二級緩存空間對應的暫存區,則執行建立。事務緩存管理器裡面維護了一個暫存區Map。二級緩存空間作為key,暫存區作為value。一一對應。并且将這個key,記錄下來,表明它沒有命中。
TransactionalCache 中的 private final Set entriesMissedInCache;就是用來記錄沒有命中的緩存key
- 如果此時二級緩存中沒有,則執行查詢操作。
- 将結果集,放入到事務緩存管理器的暫存區中。 TransactionalCache 中的 private final Map<Object, Object> entriesToAddOnCommit //暫存區,key是之前建立出來的緩存key,value是對應的查詢結果。
3.4.2 commit送出操作。
- 首先來到CachingExecutor 中的comit方法 主要做兩件事情,一是将會話送出,清空一級緩存。二是送出事務緩存管理器中的所有暫存區,進行更新二級緩存的操作。
-
TransactionalCacheManager 中的commit
會話送出這裡不考慮,看看事務緩存管理器中的commit方法
周遊暫存區,對每一個暫存區執行送出操作。
-
TransactionalCache 中的commit
執行flushPendingEntries 和 reset
-
flushPendingEntries
周遊暫存區所有的鍵值對,全部放到二級緩存空間中。然後周遊之前存放的沒有命中二級緩存中的緩存key。如果這個鍵在暫存區沒有出現過。則将這個鍵和null值送出給二級緩存空間。這樣做的目的是為了避免無效的查詢,多次的去查詢資料庫,而造成資料庫很大的壓力負擔。也就是解決緩存穿透的問題。
3.4.3 update操作
- 首先來到的是CachingExecutor中的update方法,做兩件事情,清空暫存區并設定送出清空标記為true。交給baseExecutor執行它的update邏輯。
- 檢查清空緩存标記是否為true,對于update相關的statement中的flushCacheRequired屬性預設是true,是以對于update操作必然會清空二級緩存。而Select相關的statement中的flushCacheRequired屬性預設是false。如果将這個屬性手動的設定為true,則也會執行清空二級緩存的操作。讓我們看看緩存事務管理器中的clear是做了哪些事情
- 根據二級緩存空間拿到對應的暫存區,執行clear
- 設定送出時清空标記為true,然後清空暫存區。可以看到此時并沒有真正的對二級緩存空間進行清空,而僅僅是設定了送出時清空标記,等到執行commit的時候,才會清空二級緩存空間。
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。