為什么?Mybatis的一級和二級緩存都不建議使用?

緩存是在哪起作用的?

個人認為mybatis一級緩存和二級緩存并不是一個很好的設計,工作中我基本上也不會使用一級緩存和二級緩存,因為一旦使用不當會造成很多問題,所以我們今天就來看看到底會有什么問題?

Executor會調用StatementHandler執(zhí)行sql,起一個承上啟下的作用。

Executor的設計是一個典型的裝飾者模式,SimpleExecutor,ReuseExecutor是具體實現類,而CachingExecutor是裝飾器類。

可以看到具體組件實現類有一個父類BaseExecutor,而這個父類是一個模板模式的典型應用,操作一級緩存的操作都在這個類中,而具體的操作數據庫的功能則讓子類去實現。

「二級緩存則是一個裝飾器類,當開啟二級緩存的時候,會使用CachingExecutor對具體實現類進行裝飾,所以查詢的時候一定是先查詢二級緩存再查詢一級緩存」

「那么一級緩存和二級緩存有什么區(qū)別呢?」

一級緩存

// BaseExecutorprotected PerpetualCache localCache;

一級緩存是BaseExecutor中的一個成員變量localCache(對HashMap的一個簡單封裝),因此一級緩存的生命周期與SqlSession相同,如果你對SqlSession不熟悉,你可以把它類比為JDBC編程中的Connection,即數據庫的一次會話。

「一級緩存和二級緩存key的構建規(guī)則是一致的,都是一個CacheKey對象,因為Mybatis中涉及動態(tài)SQL等多方面的因素,緩存的key不能僅僅通過String來表示」

當執(zhí)行sql的如下4個條件都相等時,CacheKey才會相等

1 mappedStatment的id

2 指定查詢結構集的范圍

3 查詢所使用SQL語句

4 用戶傳遞給SQL語句的實際參數值

「當查詢的時候先從緩存中查詢,如果查詢不到的話再從數據庫中查詢」

org.apache.ibatis.executor.BaseExecutor#query

當使用同一個SqlSession執(zhí)行更新操作時,會先清空一級緩存。因此一級緩存中內容被使用的概率也很低

一級緩存測試

「看到美團技術團隊上關于一級緩存和二級緩存的一些測試寫的挺不錯的,就直接引用過來了」

原文地址:https://tech.meituan.com/2018/01/19/mybatis-cache.html 測試代碼github地址:https://github.com/kailuncen/mybatis-cache-demo

接下來通過實驗,了解MyBatis一級緩存的效果,每個單元測試后都請恢復被修改的數據。

首先是創(chuàng)建示例表student,創(chuàng)建對應的POJO類和增改的方法,具體可以在entity包和mapper包中查看。

CREATETABLE`student`(`id`int(11)unsignedNOTNULLAUTO_INCREMENT,`name`varchar(200)COLLATEutf8_binDEFAULTNULL,`age`tinyint(3)unsignedDEFAULTNULL,? PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=4DEFAULTCHARSET=utf8COLLATE=utf8_bin;

在以下實驗中,id為1的學生名稱是凱倫

「實驗1」

開啟一級緩存,范圍為會話級別,調用三次getStudentById,代碼如下所示:一級緩存測試

「看到美團技術團隊上關于一級緩存和二級緩存的一些測試寫的挺不錯的,就直接引用過來了」

原文地址:https://tech.meituan.com/2018/01/19/mybatis-cache.html 測試代碼github地址:https://github.com/kailuncen/mybatis-cache-demo

接下來通過實驗,了解MyBatis一級緩存的效果,每個單元測試后都請恢復被修改的數據。

首先是創(chuàng)建示例表student,創(chuàng)建對應的POJO類和增改的方法,具體可以在entity包和mapper包中查看。

CREATETABLE`student`(`id`int(11)unsignedNOTNULLAUTO_INCREMENT,`name`varchar(200)COLLATEutf8_binDEFAULTNULL,`age`tinyint(3)unsignedDEFAULTNULL,? PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=4DEFAULTCHARSET=utf8COLLATE=utf8_bin;

在以下實驗中,id為1的學生名稱是凱倫

「實驗1」

開啟一級緩存,范圍為會話級別,調用三次getStudentById,代碼如下所示:

執(zhí)行結果:? ?

我們可以看到,只有第一次真正查詢了數據庫,后續(xù)的查詢使用了一級緩存。

「實驗2」

增加了對數據庫的修改操作,驗證在一次數據庫會話中,如果對數據庫發(fā)生了修改操作,一級緩存是否會失效。

執(zhí)行結果?

我們可以看到,在修改操作后執(zhí)行的相同查詢,查詢了數據庫,一級緩存失效。

「實驗3」

開啟兩個SqlSession,在sqlSession1中查詢數據,使一級緩存生效,在sqlSession2中更新數據庫,驗證一級緩存只在數據庫會話內部共享。

? ? 輸出如下

sqlSession1和sqlSession2讀的時相同的數據,但是都查詢了數據庫,說明了「一級緩存只在數據庫會話層面共享」

sqlSession2更新了id為1的學生的姓名,從凱倫改為了小岑,但sqlSession1之后的查詢中,id為1的學生的名字還是凱倫,出現了臟數據,也證明了之前的設想,一級緩存只在數據庫會話層面共享

「MyBatis的一級緩存最大范圍是SqlSession內部,有多個SqlSession或者分布式的環(huán)境下,數據庫寫操作會引起臟數據,建議設定緩存級別為Statement,即進行如下配置」

原因也很簡單,看BaseExecutor的query()方法,當配置成STATEMENT時,每次查詢完都會清空緩存

「看到這你可能會想,我用mybatis后沒設置這個參數啊,好像也沒發(fā)生臟讀的問題啊,其實是因為你和spring整合了」

當mybatis和spring整合后(整合的相關知識后面還有一節(jié))

在未開啟事務的情況之下,每次查詢,spring都會關閉舊的sqlSession而創(chuàng)建新的sqlSession,因此此時的一級緩存是沒有起作用的

在開啟事務的情況之下,spring使用threadLocal獲取當前線程綁定的同一個sqlSession,因此此時一級緩存是有效的,當事務執(zhí)行完畢,會關閉sqlSession

「當mybatis和spring整合后,未開啟事務的情況下,不會有任何問題,因為一級緩存沒有生效。當開啟事務的情況下,可能會有問題,由于一級緩存的存在,在事務內的查詢隔離級別是可重復讀,即使你數據庫的隔離級別設置的是提交讀」

二級緩存

//Configurationprotected final Map caches =newStrictMap<>("Caches collection");

「而二級緩存是Configuration對象的成員變量,因此二級緩存的生命周期是整個應用級別的。并且是基于namespace構建的,一個namesapce構建一個緩存」

「二級緩存不像一級緩存那樣查詢完直接放入一級緩存,而是要等事務提交時才會將查詢出來的數據放到二級緩存中。」

因為如果事務1查出來直接放到二級緩存,此時事務2從二級緩存中拿到了事務1緩存的數據,但是事務1回滾了,此時事務2不就發(fā)生了臟讀了嗎?

「二級緩存的相關配置有如下3個」

「1.mybatis-config.xml」

這個是二級緩存的總開關,只有當該配置項設置為true時,后面兩項的配置才會有效果

從Configuration類的newExecutor方法可以看到,當cacheEnabled為true,就用緩存裝飾器裝飾一下具體組件實現類,從而讓二級緩存生效

「2.mapper映射文件中」mapper映射文件中如果配置了和中的任意一個標簽,則表示開啟了二級緩存功能,沒有的話表示不開啟

二級緩存的部分配置如上,type就是填寫一個全類名,用來指定二級緩存的實現類,這個實現類需要實現Cache接口,默認是PerpetualCache(你可以利用這個屬性將mybatis二級緩存和Redis,Memcached等緩存組件整合在一起)

org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache

這個eviction表示緩存清空策略,可填選項如下

選項解釋裝飾器類LRU最近最少使用的:移除最長時間不被使用的對象LruCacheFIFO先進先出:按對象進入緩存的順序來移除它們FifoCacheSOFT軟引用:移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對象SoftCacheWEAK弱引用:更積極地移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對象WeakCache

典型的裝飾者模式的實現,換緩存清空策略就是換裝飾器。

「3.<select>節(jié)點中的useCache屬性」

該屬性表示查詢產生的結果是否要保存的二級緩存中,useCache屬性的默認值為true,這個配置可以將二級緩存細分到語句級別

測試二級緩存

二級緩存是基于namespace實現的,即一個mapper映射文件用一個緩存

在本實驗中,id為1的學生名稱初始化為點點。

「實驗1」

測試二級緩存效果,不提交事務,sqlSession1查詢完數據后,sqlSession2相同的查詢是否會從緩存中獲取數據。

執(zhí)行結果:

我們可以看到,當sqlsession沒有調用commit()方法時,二級緩存并沒有起到作用。

「實驗2」

測試二級緩存效果,當提交事務時,sqlSession1查詢完數據后,sqlSession2相同的查詢是否會從緩存中獲取數據。

從圖上可知,sqlsession2的查詢,使用了緩存,緩存的命中率是0.5。

「實驗3」

測試update操作是否會刷新該namespace下的二級緩存。

我們可以看到,在sqlSession3更新數據庫,并提交事務后,sqlsession2的StudentMapper namespace下的查詢走了數據庫,沒有走Cache。

「實驗4」

驗證MyBatis的二級緩存不適應用于映射文件中存在多表查詢的情況。

getStudentByIdWithClassInfo的定義如下

通常我們會為每個單表創(chuàng)建單獨的映射文件,由于MyBatis的二級緩存是基于namespace的,多表查詢語句所在的namspace無法感應到其他namespace中的語句對多表查詢中涉及的表進行的修改,引發(fā)臟數據問題。

執(zhí)行結果

在這個實驗中,我們引入了兩張新的表,一張class,一張classroom。class中保存了班級的id和班級名,classroom中保存了班級id和學生id。我們在StudentMapper中增加了一個查詢方法getStudentByIdWithClassInfo,用于查詢學生所在的班級,涉及到多表查詢。在ClassMapper中添加了updateClassName,根據班級id更新班級名的操作。

當sqlsession1的studentmapper查詢數據后,二級緩存生效。保存在StudentMapper的namespace下的cache中。當sqlSession3的classMapper的updateClassName方法對class表進行更新時,updateClassName不屬于StudentMapper的namespace,所以StudentMapper下的cache沒有感應到變化,沒有刷新緩存。當StudentMapper中同樣的查詢再次發(fā)起時,從緩存中讀取了臟數據。

「實驗5」

為了解決實驗4的問題呢,可以使用Cache ref,讓ClassMapper引用StudenMapper命名空間,這樣兩個映射文件對應的SQL操作都使用的是同一塊緩存了。

mapper文件中的配置如下

執(zhí)行結果:

不過這樣做的后果是,緩存的粒度變粗了,多個Mapper namespace下的所有操作都會對緩存使用造成影響。

總結

mybatis的一級緩存和二級緩存都是基于本地的,分布式環(huán)境下必然會出現臟讀。

二級緩存可以通過實現Cache接口,來集中管理緩存,避免臟讀,但是有一定的開發(fā)成本,并且在多表查詢時,使用不當極有可能會出現臟數據

「除非對性能要求特別高,否則一級緩存和二級緩存都不建議使用」

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容