天天看點

從坑中爬出,為大家分享Mybatis緩存機制

SpringBoot內建Mybatis幾乎已經成為大多數項目的标配了,但在使用的過程中Mybatis的緩存功能往往會被大家遺忘,甚至很多開發者都沒意識到在SpringBoot內建Mybatis還有一級緩存和二級緩存的事。

本來沒計劃寫本篇文章,但在實踐的過程掉坑裡了,當從坑中爬起來時,發現有必要給大家寫寫Mybatis的緩存。

事情是這樣的:項目中使用了樂觀鎖,并進行了失敗嘗試(3次)。但運作的時候發現嘗試也是失敗的。起初以為是并發問題,然後把嘗試次數無限放大,發現次次都是失敗的。

這其中一定有問題,經過研究發現是Mybatis的一級緩存導緻的,于是專門研究了Mybatis的一級和二級緩存分享給大家。

其實在日常的項目中,我們幾乎都會用到緩存,比如一些不怎麼改變的配置項,會采用緩存來減少資料庫的壓力。Mybatis的一級二級緩存所起到的作用也是相同的。都是為了減少資料庫壓力,提高系統性能。

Mybatis的一級緩存與二級緩存的主要差別是它們所緩存的範圍不同。一級緩存是單個session級别的,二級緩存是多個session級别的,隻不過多個session需要是同一個namespace下的。關于細節我們後面會逐一介紹。

這裡所說的session與我們在Http請求中所說的session可以類别,但并不是同一個session。Http中是session指定的是HttpSession,而這裡所說的session是指的查詢資料庫的SqlSession。

一次網頁請求,可以建立一個session(HttpSession),一次資料庫查詢操作同樣會建立一個session(SqlSession)。對照一下,就會很容易了解。

先通過通過下圖我們來看看一級緩存的整個流轉過程。

從坑中爬出,為大家分享Mybatis緩存機制

當使用者第一次查詢id為1的訂單時,緩存中沒有資料,是以從資料庫中進行加載,加載完成會進行緩存。這裡緩存的位置就是記憶體中的一塊空間,資料格式為HashMap。

當第二次讀取時,便會直接讀取緩存中的資料。當SqlSession執行commit操作(包括插入、更新、删除)時,會清空SqlSession的一級緩存,主要目的是確定緩存中的資料是最新的,避免髒讀。

一級緩存是本地(局部)緩存,不能被關閉,隻能配置緩存範圍:SESSION或STATEMENT。也就是說一級緩存不需要在配置檔案去配置,預設開啟。

看一下Mybatis源碼中的org.apache.ibatis.session.Configuration類的部分源碼:

我們可以看到緩存是預設開啟的,而localCacheScope預設為Session級别。LocalCacheScope中隻定義了SESSION和STATEMENT兩個枚舉項。

需要注意的是cacheEnabled配置的是二級緩存,而localCacheScope配置的是一級緩存。預設情況下SpringBoot內建Mybatis時一級緩存和二級緩存都是開啟狀态。

在Spring Boot內建Mybatis的項目中,執行如下單元測試:

列印結果

也證明了上面的說法。

關于Spring Boot內建Mybatis我們在之前文章中已經專門講過,這裡不再贅述,直奔重點。先看一個單元測試:

在該單元測試中,手動擷取SqlSession,并通過SqlSession獲得OrderMapper,然後進行資料的查詢。執行單元測試之前需在application.properties中配置列印SQL語句的日志:

注意:level後面的包名需要替換成mapper所在的package路徑。

此時執行單元測試,會發現隻有第一次查詢了資料庫,後面兩次都未查詢。

從坑中爬出,為大家分享Mybatis緩存機制

同時,在日志中隻列印了一次查詢資料庫的SQL語句。

此時我們執行如下單元測試:

會發現三次都查詢了資料庫,為什麼呢?這是因為每次Mapper調用findById方法都會建立一個session,并且在執行完畢後關閉session。是以三次調用并不在一個session中,一級緩存并沒有起作用。

而此時,如果将該方法放在一個事務當中,修改如下:

此時,我們發現一級緩存又生效了。而前文提到的樂觀鎖重試的Bug就是由于在此場景下使用了一級緩存,查詢不到最新的資料庫資料導緻的。此處也是大家在使用的過程中需要留意的。

實踐中,将Mybatis和Spring進行整合開發,事務控制在service中。如果是執行兩次service調用查詢相同的使用者資訊,不走一級緩存,因為Service方法結束,SqlSession就關閉,一級緩存就清空。

二級緩存是針對不同SqlSession直接的緩存,可以了解為mapper級别。這些SqlSession需要是同一個namespace。那namespace在哪裡展現呢?

就是我們在xxMapper.xml檔案中配置的namespace:

下面看一下二級緩存的示意圖。

從坑中爬出,為大家分享Mybatis緩存機制

sqlSession1去查詢使用者id為1的訂單資訊,查詢到使用者資訊會将查詢資料存儲到二級緩存中。sqlSession2去查詢時便會直接通過二級緩存進行查詢。

二級緩存與一級緩存差別,二級緩存的範圍更大,多個sqlSession可以共享一個OrderMapper的二級緩存區域。資料類型仍然為HashMap。每一個namespace的mapper都有一個二緩存區域,兩個mapper的namespace如果相同,這兩個mapper執行sql查詢到資料将存在相同的二級緩存區域中。

在上面的Configuration類中我們已經看到預設開啟了二級緩存,此開啟操作可以通過在application中進行開啟或關閉(false):

當然,也可以在SqlMapConfig.xml中加入:

來開啟。

此時隻是完成了二級緩存的全局開關,但并沒有針對具體的Mapper生效。如果需要對指定的Mapper使用二級緩存,還需要在對應的xml檔案中配置如下内容:

此時,該namespace下的Mapper便開啟了二級緩存。

二級緩存需要查詢結果映射的pojo對象實作java.io.Serializable接口。如果存在父類、成員pojo都需要實作序列化接口。否則,執行的過程中會直接報錯。

此時,Order類實作如下:

由于二級緩存資料存儲媒體多種多樣,不一定在記憶體有可能是硬碟或者遠端伺服器。是以,pojo類實作序列化接口是為了将緩存資料取出執行反序列化操作。

下面看一下具體的單元測試:

由于開啟了二級緩存,我們直接使用service進行查詢,就可以發現緩存已經生效了。

從坑中爬出,為大家分享Mybatis緩存機制

在圖中我們可以看到,還列印出了命中緩存的機率為:0.5。

由于cache是針對整個Mapper中的查詢方法的,是以當某個方法不需要緩存時,可在對應的select标簽中添加useCache值為false來禁用二級緩存。

查詢結果實時性要求不高的情況下可采用mybatis二級緩存降低資料庫通路量,提高通路速度,同時配合設定緩存重新整理間隔flushInterval來根據需要改變重新整理緩存的頻次。

通常情況下,如果同時設定了一級緩存和二級緩存,會先使用二級緩存的資料,然後再使用一級緩存的資料,最後才會通路資料庫。

關于Mybatis緩存本篇文章就講這麼多,當大家心中對Mybatis的緩存有一個基礎的印象之後,後面遇到類似的問題或bug時便有了思考的方向。