Mybatis緩存機制

MyBatis的緩存機制

在日常工作中,開發人員多數情況下是使用MyBatis的默認緩存配置,但是MyBatis緩存機制有一些不足之處,在使用中容易引起髒數據,形成一些潛在的隱患。

 

Mybatis的一級緩存

Mybatis的一級緩存是SqlSession級別的。一級緩存的作用域是一個SqlSession。Mybatis默認開啓一級緩存。

在同一個SqlSession中,執行相同的查詢SQL,第一次會去查詢數據庫,並寫到緩存中;第二次直接從緩存中取。當執行SQL時兩次查詢中間發生了增刪改操作,則SqlSession的緩存清空。

一級緩存示意圖

一級緩存區域是根據SqlSession爲單位劃分的。

每次查詢會先去緩存中找,如果找不到,再去數據庫查詢,然後把結果寫到緩存中。Mybatis的內部緩存使用一個HashMap,key爲hashcode+statementId+sql語句。Value爲查詢出來的結果集映射成的java對象。

SqlSession執行insert、update、delete等操作commit後會清空該SQLSession緩存。

 

Mybatis的一級緩存帶來的髒數據問題

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

@Test
public void testLocalCacheScope() throws Exception {
        SqlSession sqlSession1 = factory.openSession(true); 
        SqlSession sqlSession2 = factory.openSession(true); 

        StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

        System.out.println("studentMapper讀取數據: " + studentMapper.getStudentById(1));
        System.out.println("studentMapper讀取數據: " + studentMapper.getStudentById(1));
        System.out.println("studentMapper2更新了" + studentMapper2.updateStudentName("小岑",1) + "個學生的數據");
        System.out.println("studentMapper讀取數據: " + studentMapper.getStudentById(1));
        System.out.println("studentMapper2讀取數據: " + studentMapper2.getStudentById(1));
}

 

執行結果

 

sqlSession2更新了id爲1的學生的姓名,從凱倫改爲了小岑,但session1之後的查詢中,id爲1的學生的名字還是凱倫,出現了髒數據,也證明了之前的設想,一級緩存只在數據庫會話內部共享。

 

Mybatis的二級緩存

Mybatis的二級緩存是指mapper映射文件。二級緩存的作用域是同一個namespace下的mapper映射文件內容,多個SqlSession共享。Mybatis需要手動設置啓動二級緩存。

在同一個namespace下的mapper文件中,執行相同的查詢SQL,第一次會去查詢數據庫,並寫到緩存中;第二次直接從緩存中取。當執行SQL時兩次查詢中間發生了增刪改操作,則二級緩存清空。

使用二級緩存時,由於二級緩存的數據不一定都是存儲到內存中,它的存儲介質多種多樣,所以需要給緩存的對象執行序列化。

 

二級緩存示意圖

 

二級緩存作用域是同一個namespace下的mapper映射文件內容。

第一次調用mapper下的SQL去查詢用戶信息。查詢到的信息會存到該mapper對應的二級緩存區域內。

第二次調用相同namespace下的mapper映射文件中相同的SQL去查詢用戶信息。會去對應的二級緩存內取結果。

如果調用相同namespace下的mapper映射文件中的增刪改SQL,並執行了commit操作。此時會清空該namespace下的二級緩存。

 

如何開啓二級緩存?

在覈心配置文件SqlMapConfig.xml中加入以下內容(開啓二級緩存總開關):

<settings>
    <!-- 打開延遲加載的開關 -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- 將積極加載改爲消極加載,即延遲加載 -->
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

 

在映射文件中,加入<cache/>節點,開啓二級緩存:

在<cache/>中可以自定義以下配置。

type:cache使用的類型,默認是PerpetualCache,這在一級緩存中提到過。

eviction: 定義回收的策略,常見的有FIFO,LRU。

flushInterval: 配置一定時間自動刷新緩存,單位是毫秒。

size: 最多緩存對象的個數。

readOnly: 是否只讀,若配置可讀寫,則需要對應的實體類能夠序列化。

blocking: 若緩存中找不到對應的key,是否會一直blocking,直到有對應的數據進入緩存。

 

實驗1

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

@Test
public void testCacheWithoutCommitOrClose() throws Exception {
        SqlSession sqlSession1 = factory.openSession(true); 
        SqlSession sqlSession2 = factory.openSession(true); 

        StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

        System.out.println("studentMapper讀取數據: " + studentMapper.getStudentById(1));
        System.out.println("studentMapper2讀取數據: " + studentMapper2.getStudentById(1));
}

 

執行結果

我們可以看到,當sqlsession1沒有調用commit()方法時,二級緩存並沒有起到作用。如果sqlsession1調用了commit()方法,那麼二級緩存將起到作用。

 

實驗2

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

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

@Test
public void testCacheWithDiffererntNamespace() throws Exception {
        SqlSession sqlSession1 = factory.openSession(true); 
        SqlSession sqlSession2 = factory.openSession(true); 
        SqlSession sqlSession3 = factory.openSession(true); 

        StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
        ClassMapper classMapper = sqlSession3.getMapper(ClassMapper.class);

        System.out.println("studentMapper讀取數據: " + studentMapper.getStudentByIdWithClassInfo(1));
        sqlSession1.close();
        System.out.println("studentMapper2讀取數據: " + studentMapper2.getStudentByIdWithClassInfo(1));

        classMapper.updateClassName("特色一班",1);
        sqlSession3.commit();
        System.out.println("studentMapper2讀取數據: " + studentMapper2.getStudentByIdWithClassInfo(1));
}

 

執行結果

 

在這個實驗中,我們引入了兩張新的表,一張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中同樣的查詢再次發起時,從緩存中讀取了髒數據。

 

上面的解決方法就是,使用Cache ref,讓ClassMapper引用StudenMapper命名空間,這樣兩個映射文件對應的Sql操作都使用的是同一塊緩存了。

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

 

 

總結

MyBatis的二級緩存相對於一級緩存來說,實現了SqlSession之間緩存數據的共享,同時粒度更加的細,能夠到namespace級別,通過Cache接口實現類不同的組合,對Cache的可控性也更強。

MyBatis在多表查詢時,極大可能會出現髒數據,有設計上的缺陷,安全使用二級緩存的條件比較苛刻。

在分佈式環境下,由於默認的MyBatis Cache實現都是基於本地的,分佈式環境下必然會出現讀取到髒數據,需要使用集中式緩存將MyBatis的Cache接口實現,有一定的開發成本,直接使用Redis,Memcached等分佈式緩存可能成本更低,安全性也更高。

 

參考:

https://tech.meituan.com/mybatis_cache.html

https://github.com/kailuncen/mybatis-cache-demo

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章