MyBati-緩存機制

一.概述

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),對於每一次查詢,都會嘗試根據查詢的條件去本地緩存中查找是否在緩存中,如果在緩存中,就直接從緩存中取出,然後返回給用戶;否則,從數據庫讀取數據,將查詢結果存入緩存並返回給用戶。

一級緩存的生命週期:

  1. MyBatis在開啓一個數據庫會話時,會 創建一個新的SqlSession對象,SqlSession對象中會有一個新的Executor對象,Executor對象中持有一個新的PerpetualCache對象;當會話結束時,SqlSession對象及其內部的Executor對象還有PerpetualCache對象也一併釋放掉。
  2. 如果SqlSession調用了close()方法,會釋放掉一級緩存PerpetualCache對象,一級緩存將不可用;
  3. 如果SqlSession調用了clearCache(),會清空PerpetualCache對象中的數據,但是該對象仍可使用;
  4. SqlSession中執行了任何一個update操作(update()、delete()、insert()) ,都會清空PerpetualCache對象的數據,但是該對象可以繼續使用;

一級緩存的工作流程:

  1. 對於某個查詢,根據statementId,params,rowBounds來構建一個key值,根據這個key值去緩存Cache中取出對應的key值存儲的緩存結果​
  2. 判斷從Cache中根據特定的key值取的數據數據是否爲空,即是否命中;​
  3. 如果命中,則直接將緩存結果返回;​
  4. 如果沒命中:        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/>)

eviction=“FIFO”:緩存回收策略:
LRU – 最近最少使用的:移除最長時間不被使用的對象。
FIFO – 先進先出:按對象進入緩存的順序來移除它們。
SOFT – 軟引用:移除基於垃圾回收器狀態和軟引用規則的對象。
WEAK – 弱引用:更積極地移除基於垃圾收集器狀態和弱引用規則的對象。
默認的是 LRU
flushInterval:刷新間隔,單位毫秒
默認情況是不設置,也就是沒有刷新間隔,緩存僅僅調用語句時刷新
size:引用數目,正整數
代表緩存最多可以存儲多少個對象,太大容易導致內存溢出
readOnly:只讀,true/false
•true:只讀緩存;會給所有調用者返回緩存對象的相同實例因此這些對象不能被修改。這提供了很重要的性能優勢。
•false:讀寫緩存;會返回緩存對象的拷貝(通過序列化)。這會慢一些,但是安全,因此默認是 false

3.4 緩存有關設置

1.全局settingcacheEnable:配置二級緩存的開關。一級緩存一直是打開的

2.select標籤的useCache屬性:配置這個select是否使用二級緩存。一級緩存一直是使用

3.sql標籤的flushCache屬性:增刪改默認flushCache=truesql執行以後,會同時清空一級和二級緩存。查詢默認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 流程圖

 

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