一.概述
MyBatis包含一個非常強大的查詢緩存特性,它可以非常方便地配置和定製。緩存可以極大的提升查詢效率。
MyBatis系統中默認定義了兩級緩存:一級緩存和二級緩存
- 默認情況下,只有一級緩存(SqlSession級別的緩存,也稱爲本地緩存)開啓。
- 二級緩存需要手動開啓和配置,他是基於namespace級別的緩存。
- 爲了提高擴展性。MyBatis定義了緩存接口Cache。我們可以通過實現Cache接口來自定義二級緩存
MyBatis 跟緩存相關的類都在cache 包裏面,其中有一個Cache 接口,只有一個默認的實現類 PerpetualCache,它是用HashMap 實現的。
二.一級緩存
2.1 概述
MyBatis的一級查詢緩存(也叫作本地緩存)是基於org.apache.ibatis.cache.impl.PerpetualCache 類的 HashMap本地緩存,其作用域是SqlSession。當Session flush或close後,該Session中的所有Cache都將被清空。
本地緩存不能被關閉,但是可以調用clearCache()來清空本地緩存,或者改變緩存的作用域。
在mybatis3.1之後, 可以配置本地緩存的作用域. 在 mybatis.xml 中配置
2.2 一級緩存生效情況
同一次會話期間只要查詢過的數據都會保存在當前SqlSesison的一個Map中。
MyBatis會在一次會話的表示----一個SqlSession對象中創建一個本地緩存(local cache),對於每一次查詢,都會嘗試根據查詢的條件去本地緩存中查找是否在緩存中,如果在緩存中,就直接從緩存中取出,然後返回給用戶;否則,從數據庫讀取數據,將查詢結果存入緩存並返回給用戶。
一級緩存的生命週期:
- MyBatis在開啓一個數據庫會話時,會 創建一個新的SqlSession對象,SqlSession對象中會有一個新的Executor對象,Executor對象中持有一個新的PerpetualCache對象;當會話結束時,SqlSession對象及其內部的Executor對象還有PerpetualCache對象也一併釋放掉。
- 如果SqlSession調用了close()方法,會釋放掉一級緩存PerpetualCache對象,一級緩存將不可用;
- 如果SqlSession調用了clearCache(),會清空PerpetualCache對象中的數據,但是該對象仍可使用;
- SqlSession中執行了任何一個update操作(update()、delete()、insert()) ,都會清空PerpetualCache對象的數據,但是該對象可以繼續使用;
一級緩存的工作流程:
- 對於某個查詢,根據statementId,params,rowBounds來構建一個key值,根據這個key值去緩存Cache中取出對應的key值存儲的緩存結果
- 判斷從Cache中根據特定的key值取的數據數據是否爲空,即是否命中;
- 如果命中,則直接將緩存結果返回;
- 如果沒命中: 1)去數據庫中查詢數據,得到查詢結果;2)將key和查詢到的結果分別作爲key,value對存儲到Cache中;3)將查詢結果返回;
2.3 一級緩存失效情況
接下來來驗證一下,MyBatis 的一級緩存到底是不是只能在一個會話裏面共享,以及跨會話(不同session)操作相同的數據會產生什麼問題。判斷是否命中緩存:如果再次發送SQL 到數據庫執行,說明沒有命中緩存;如果直接打印對象,說明是從內存緩存中取到了結果。
一級緩存失效的四種情況:
1.不同的SqlSession對應不同的一級緩存
/* 1.不同的SqlSession:使用不同的一級緩存
* 只有在當前的同一個SqlSession期間查到的數據就會保存在這個SqlSession中,下次使用這個SqlSession查詢會從緩衝中拿
*/
@Test
public void test01() throws IOException {
//第一次會話
SqlSession session1=sqlSessionFcatory.openSession();
TeacherDao teacherDao1=session1.getMapper(TeacherDao.class);
Teacher teacher1=teacherDao1.getTeacherById(1);
System.out.println(teacher1);
//第二個會話
SqlSession session2=sqlSessionFcatory.openSession();
TeacherDao teacherDao2=session2.getMapper(TeacherDao.class);
Teacher teacher2=teacherDao2.getTeacherById(1);
System.out.println(teacher2);
session1.close();
session2.close();
}
執行以上的SQL,我們可以看到如下的控制檯打印的信息,發現兩次查詢發送了兩次數據庫的操作,說明緩存沒有起作用,驗證了不同的SqlSession對應不同的一級緩存。
2.同一個SqlSession但是查詢條件不同
@Test
public void test01() throws IOException {
//第一次會話
SqlSession session1=sqlSessionFcatory.openSession();
TeacherDao teacherDao1=session1.getMapper(TeacherDao.class);
Teacher teacher1=teacherDao1.getTeacherById(1);
System.out.println(teacher1);
Teacher teacher2=teacherDao1.getTeacherById(2);
System.out.println(teacher2);
session1.close();
}
3.同一個SqlSession兩次查詢期間執行了任何一次增刪改操作,因爲增刪改操作會把緩存清空
@Test
public void test01() throws IOException {
//第一次會話
SqlSession session1=sqlSessionFcatory.openSession();
TeacherDao teacherDao1=session1.getMapper(TeacherDao.class);
Teacher teacher1=teacherDao1.getTeacherById(1);
System.out.println(teacher1);
System.out.println("===================");
//執行任何一個增刪改操作
Teacher teacher=new Teacher();
teacher.setId(2);
teacher.setName("你好");
teacherDao1.updateTeacher(teacher);
System.out.println("===================");
Teacher teacher2=teacherDao1.getTeacherById(1);
System.out.println(teacher2);
session1.commit();
session1.close();
}
執行以上的SQL,我們可以看到如下的控制檯打印的信息,發現即使使用同一個SqlSession進行兩次相同的查詢,還是發送了兩次數據庫的操作,說明在同一個會話中,增刪改操作會清空Cache。
4.同一個SqlSession兩次查詢期間手動清空了緩存
@Test
public void test01() throws IOException {
//第一次會話
SqlSession session1=sqlSessionFcatory.openSession();
TeacherDao teacherDao1=session1.getMapper(TeacherDao.class);
Teacher teacher1=teacherDao1.getTeacherById(1);
System.out.println(teacher1);
System.out.println("===================");
System.out.println("手動清空緩存");
//清空當前SqlSession的一級緩存
session1.clearCache();
System.out.println("===================");
Teacher teacher2=teacherDao1.getTeacherById(1);
System.out.println(teacher2);
session1.commit();
session1.close();
}
執行以上的SQL,我們可以看到如下的控制檯打印的信息,發現兩次查詢發送了兩次數據庫的操作,說明手動清空緩存起了作用。
三.二級緩存
3.1 概述
二級緩存是用來解決一級緩存不能跨會話共享的問題的,範圍是namespace 級別的,可以被多個SqlSession 共享(只要是同一個接口裏面的相同方法,都可以共享),生命週期和應用同步。二級緩存,全局作用域緩存。二級緩存默認不開啓,需要手動配置。
MyBatis提供二級緩存的接口以及實現,緩存實現需要POJO實現Serializable接口。
二級緩存在SqlSession關閉或提交之後纔會生效。
3.2 步驟
1.全局配置文件中開啓二級緩存
<setting name="cacheEnabled" value="true"/>
2.需要使用二級緩存的映射文件中使用Cache配置緩存
<cache />
3.注意,POJO需要實現Serializable接口
3.3 緩存的相關屬性(<cache/>)
3.4 緩存有關設置
1.全局setting的cacheEnable:配置二級緩存的開關。一級緩存一直是打開的。
2.select標籤的useCache屬性:配置這個select是否使用二級緩存。一級緩存一直是使用的
3.sql標籤的flushCache屬性:增刪改默認flushCache=true。sql執行以後,會同時清空一級和二級緩存。查詢默認flushCache=false。
4.sqlSession.clearCache():只是用來清除一級緩存。
5、當在某一個作用域 (一級緩存Session/二級緩存Namespaces) 進行了 C/U/D 操作後,默認該作用域下所有 select 中的緩存將被clear。
3.5 二級緩存生效測試
@Test
public void test02(){
SqlSession session1=sqlSessionFcatory.openSession();
SqlSession session2=sqlSessionFcatory.openSession();
TeacherDao teacherDao1=session1.getMapper(TeacherDao.class);
TeacherDao teacherDao2=session2.getMapper(TeacherDao.class);
//1.第一個Dao查詢1號teacher
Teacher teacher1=teacherDao1.getTeacherById(1);
System.out.println(teacher1);
session1.close();
//2.第二個Dao查詢2號teacher
Teacher teacher2=teacherDao2.getTeacherById(1);
System.out.println(teacher2);
session2.close();
}
執行以上的SQL,我們可以看到如下的控制檯打印的信息,兩次查詢只發送了一次數據庫操作,且命中率增加了,說明二級緩存起了作用。
四.緩存的細節
4.1 什麼時候使用二級緩存
一級緩存默認是打開的,二級緩存需要配置纔可以開啓。那麼我們必須思考一個問題,在什麼情況下才有必要去開啓二級緩存?
- 因爲所有的增刪改都會刷新二級緩存,導致二級緩存失效,所以適合在查詢爲主的應用中使用,比如歷史交易、歷史訂單的查詢。否則緩存就失去了意義。
- 如果多個namespace 中有針對於同一個表的操作,比如Blog 表,如果在一個namespace 中刷新了緩存,另一個namespace 中沒有刷新,就會出現讀到髒數據的情況。所以,推薦在一個Mapper 裏面只操作單表的情況使用。
如果要讓多個namespace 共享一個二級緩存,應該怎麼做?跨namespace 的緩存共享的問題,可以使用<cache-ref>來解決:
<cache-ref namespace="com.test.Dao.TeacherDao" />
cache-ref 代表引用別的命名空間的Cache 配置,兩個命名空間的操作使用的是同一個Cache。在關聯的表比較少,或者按照業務可以對錶進行分組的時候可以使用。
4.2 緩存的執行順序
1.不會出現一級緩存和二級緩存中有同一個數據。原因是:
- 二級緩存中有數據:一級緩存關閉了就有
- 一級緩存中有數據:二級緩存中沒有此數據,就會看一級緩存,一級緩存沒有就去查數據庫,數據庫查詢的結果就會放在一級緩存中。
2.如果你的MyBatis使用了二級緩存,並且你的Mapper和select語句也配置使用了二級緩存,那麼在執行select查詢的時候,MyBatis會先從二級緩存中取輸入,其次纔是一級緩存,即MyBatis查詢數據的順序是:二級緩存 —> 一級緩存 —> 數據庫。
4.3 緩存流程圖
4.4 緩存執行順序的測試
@Test
public void test02(){
SqlSession session1=sqlSessionFcatory.openSession();
SqlSession session2=sqlSessionFcatory.openSession();
TeacherDao teacherDao1=session1.getMapper(TeacherDao.class);
//1.第一個Dao查詢1號teacher
Teacher teacher1=teacherDao1.getTeacherById(1);
System.out.println(teacher1);
session1.close();
System.out.println("=============");
TeacherDao teacherDao2=session2.getMapper(TeacherDao.class);
Teacher teacher2=teacherDao2.getTeacherById(1);
System.out.println(teacher2);
System.out.println("=============");
Teacher teacher3=teacherDao2.getTeacherById(1);
System.out.println(teacher2);
System.out.println("=============");
//查詢第二個老師
Teacher teacher4=teacherDao2.getTeacherById(2);
System.out.println(teacher4);
Teacher teacher5=teacherDao2.getTeacherById(2);
System.out.println(teacher5);
session2.close();
}
執行以上的SQL,我們可以看到如下的控制檯打印的信息,第一次查詢發送了數據庫的操作,然後關閉了SqlSession,將查詢的數據放在了二級緩存中。第二次查詢雖然是一個新的會話,但是沒有進行數據庫的操作,而是從二級緩存中拿出了數據。第三次查詢還是從二級緩存中拿到了數據。第四次查詢用的同一個會話,但是查詢的數據不同,還是進行了數據庫的操作。第五次查詢和第四次查詢相同的數據,但是第四次查詢的時候沒有關閉SqlSession,所以第五次查詢是從一級緩存中拿出數據,明顯可見命中率減少了。
五.整合第三方緩存
5.1 概述
除了MyBatis 自帶的二級緩存之外,我們也可以通過實現Cache 接口來自定義二級緩存。MyBatis 官方提供了一些第三方緩存集成方式,比如ehcache 和redis。
EhCache 是一個純Java的進程內緩存框架,具有快速、精幹等特點,是Hibernate中默認的CacheProvider。MyBatis定義了Cache接口方便我們進行自定義擴展。
5.2 步驟
1.導入ehcache包,以及整合包,日誌包
ehcache-core-2.6.8.jar
mybatis-ehcache-1.0.3.jar
slf4j-api-1.6.1.jar
slf4j-log4j12-1.6.2.jar
2.編寫ehcache.xml配置文件,放在類路徑的根目錄下
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<!-- 磁盤保存路徑 -->
<diskStore path="D:\44\ehcache" />
<defaultCache
maxElementsInMemory="1"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
<!--
屬性說明:
l diskStore:指定數據在磁盤中的存儲位置。
l defaultCache:當藉助CacheManager.add("demoCache")創建Cache時,EhCache便會採用<defalutCache/>指定的的管理策略
以下屬性是必須的:
l maxElementsInMemory - 在內存中緩存的element的最大數目
l maxElementsOnDisk - 在磁盤上緩存的element的最大數目,若是0表示無窮大
l eternal - 設定緩存的elements是否永遠不過期。如果爲true,則緩存的數據始終有效,如果爲false那麼還要根據timeToIdleSeconds,timeToLiveSeconds判斷
l overflowToDisk - 設定當內存緩存溢出的時候是否將過期的element緩存到磁盤上
以下屬性是可選的:
l timeToIdleSeconds - 當緩存在EhCache中的數據前後兩次訪問的時間超過timeToIdleSeconds的屬性取值時,這些數據便會刪除,默認值是0,也就是可閒置時間無窮大
l timeToLiveSeconds - 緩存element的有效生命期,默認是0.,也就是element存活時間無窮大
diskSpoolBufferSizeMB 這個參數設置DiskStore(磁盤緩存)的緩存區大小.默認是30MB.每個Cache都應該有自己的一個緩衝區.
l diskPersistent - 在VM重啓的時候是否啓用磁盤保存EhCache中的數據,默認是false。
l diskExpiryThreadIntervalSeconds - 磁盤緩存的清理線程運行間隔,默認是120秒。每個120s,相應的線程會進行一次EhCache中數據的清理工作
l memoryStoreEvictionPolicy - 當內存緩存達到最大,有新的element加入的時候, 移除緩存中element的策略。默認是LRU(最近最少使用),可選的有LFU(最不常使用)和FIFO(先進先出)
-->
3.配置cache標籤
<cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache>
5.3 流程圖