MyBatis緩存 mybatis – MyBatis 3 | cache MyBatis 一級緩存全詳解(一) MyBatis 內置了一個強大的事務性查詢緩存機制,它可以非常方便地配置和定製。 為了使它更加強大而且易於配置,我們對 MyBatis 3 中的緩存實現進行了許多改進。 1.一級緩存 1 ...
MyBatis緩存
MyBatis 內置了一個強大的事務性查詢緩存機制,它可以非常方便地配置和定製。 為了使它更加強大而且易於配置,我們對 MyBatis 3 中的緩存實現進行了許多改進。
1.一級緩存
1.1基本說明
Mybatis的一級緩存(也叫本地緩存/Local Cache)是指SqlSession級別的,作用域是SqlSession。
Mybatis預設開啟一級緩存,在同一個SqlSession中,相同的Sql查詢的時候,第一次查詢的時候,就會從緩存中取,如果發現沒有數據,那麼就從資料庫查詢出來,並且緩存到HashMap中,如果下次還是相同的查詢,就直接從緩存中查詢,就不在去查詢資料庫,對應的就不在去執行SQL語句。
當查詢到的數據,進行增刪改的操作的時候,緩存將會失效。在spring容器管理中每次查詢都是創建一個新的sqlSession,所以在分散式環境中不會出現數據不一致的問題。
一級緩存原理圖:

在參數和SQL完全一樣的情況下,我們使用同一個SqlSession對象調用一個Mapper方法,往往只執行一次SQL,因為使用SelSession第一次查詢後,MyBatis會將其放在緩存中,以後再查詢的時候,如果沒有聲明需要刷新,並且緩存沒有超時的情況下,SqlSession都會取出當前緩存的數據,而不會再次發送SQL到資料庫。
每一次會話都對應自己的一級緩存,作用範圍比較小,一旦會話關閉就查詢不到了。
1.2快速入門
需求:當第一次查詢id=1的Monster後,再次查詢id=1的monster對象,就會直接從一級緩存獲取,不會再次發出sql
(1)Monster實體類
package com.li.entity;
import java.util.Date;
/**
* @author 李
* @version 1.0
*/
public class Monster {
//屬性和表的欄位對應
private Integer id;
private Integer age;
private String name;
private String email;
private Date birthday;
private double salary;
private Integer gender;
//省略全參、無參構造器、setter、getter、toString方法
}
(2)MonsterMapper介面方法
//查詢-根據id
public Monster getMonsterById(Integer id);
//查詢所有的Monster
public List<Monster> findAllMonster();
(3)映射文件(部分)
<mapper namespace="com.li.mapper.MonsterMapper">
<!--配置getMonsterById方法-->
<select id="getMonsterById" resultType="Monster">
SELECT * FROM `monster` WHERE id=#{id};
</select>
<!--實現findAllMonster方法-->
<select id="findAllMonster" resultType="Monster">
SELECT * FROM `monster`;
</select>
</mapper>
(4)測試(部分代碼)
//測試一級緩存
@Test
public void level1CacheTest() {
System.out.println("==========第一次查詢=========");
Monster monster = monsterMapper.getMonsterById(10);
System.out.println("monster=" + monster);
System.out.println("==========第二次查詢=========");
Monster monster2 = monsterMapper.getMonsterById(10);
System.out.println("monster=" + monster2);
//關閉sqlSession會話
if (sqlSession != null) {
sqlSession.close();
}
}
一級緩存預設打開,在同一個會話中,當重覆查詢時,不會再發出sql語句,而是從一級緩存中直接獲取數據:

註意是重覆查詢,如果是不同的查詢操作還是會向資料庫發出sql
1.3一級緩存是什麼?
一級緩存到底是什麼?
我們通過查看SqlSession的結構可以看出,一級緩存就是一個HashMap,緩存其實就是一個本地存放的map對象,每一個SqlSession都會存放一個map對應的引用。

1.4一級緩存的執行流程
1.5一級緩存失效分析
1.5.1關閉SqlSession會話後,一級緩存失效
我們知道,一級緩存是和SqlSession會話關聯的,一旦SqlSession關閉了,一級緩存就會失效。測試如下:
//測試一級緩存失效
@Test
public void level1CacheTest2() {
System.out.println("==========第1次查詢=========");
Monster monster = monsterMapper.getMonsterById(10);
System.out.println("monster=" + monster);
//關閉sqlSession,一級緩存失效
if (sqlSession != null) {
sqlSession.close();
}
System.out.println("==========第2次查詢=========");
sqlSession = MybatisUtils.getSqlSession();//重新獲取SqlSession對象
monsterMapper = sqlSession.getMapper(MonsterMapper.class);//重新初始化
Monster monster2 = monsterMapper.getMonsterById(10);
System.out.println("monster2=" + monster2);
if (this.sqlSession != null) {
this.sqlSession.close();
}
}
結果:可以看到兩次查詢都發出了sql操作,說明如果SqlSession會話關閉了,第二次查詢依然回到資料庫查詢,一級緩存失效。
1.5.2手動清理緩存,一級緩存失效
當執行sqlSession.clearCache()時(手動清理緩存),一級緩存失效。
clearCache()方法底層執行如下:

測試方法如下:
//測試一級緩存失效
@Test
public void level1CacheTest3() {
System.out.println("==========第1次查詢=========");
Monster monster = monsterMapper.getMonsterById(10);
System.out.println("monster=" + monster);
//手動清理緩存,也會導致一級緩存失效
sqlSession.clearCache();
System.out.println("==========第2次查詢=========");
Monster monster2 = monsterMapper.getMonsterById(10);
System.out.println("monster2=" + monster2);
if (this.sqlSession != null) {
this.sqlSession.close();
}
}
測試結果如下,查詢操作相同,且在同一個SqlSession會話內,但底層仍然到資料庫執行了兩次相同操作,這說明當手動清理緩存後,一級緩存也會失效。

1.5.3對查詢的數據,進行增刪改操作時,一級緩存失效
在兩次相同的查詢中間進行update操作,是否會對一級緩存產生影響?
//如果被查詢的數據進行了增刪改操作,會導致一級緩存數據失效
@Test
public void level1CacheTest4() {
System.out.println("==========第1次查詢=========");
Monster monster = monsterMapper.getMonsterById(10);
System.out.println("monster=" + monster);
//對要查詢的數據id=10進行update操作
monster.setName("金蟬子");
monsterMapper.updateMonster(monster);
System.out.println("==========第2次查詢=========");
Monster monster2 = monsterMapper.getMonsterById(10);
System.out.println("monster2=" + monster2);
if (sqlSession != null) {
sqlSession.commit();//註意提交事務
sqlSession.close();
}
}
如下,在兩次相同查詢操作之間進行update操作,一級緩存同樣失效了,因為第二次查詢操作仍然向資料庫發出sql語句。

2.二級緩存
2.1基本介紹
- 二級緩存和一級緩存都是為了提高檢索效率而創建的技術
- 兩者最大的區別就是作用域範圍不一樣
- 一級緩存的作用域是sqlSession會話級別,在一次會話中有效
- 二級緩存的作用域是全局範圍,針對不同的SqlSession會話都有效。二級緩存又稱"全局緩存",是基於namespace級別的緩存,一個namespace對應一個二級緩存
- 當一級緩存的會話被關閉時,一級緩存的數據就會被放入二級緩存,前提是二級緩存是開啟的。
二級緩存原理圖:
開啟二級緩存後,會使用CachingExecutor裝飾Executor,進入一級緩存的查詢流程前,先在CachingExecutor進行二級緩存的查詢,具體的工作流程如下所示。

二級緩存開啟後,同一個namespace下的所有操作語句,都影響著同一個Cache,即二級緩存被多個SqlSession共用,是一個全局的變數。當開啟緩存後,數據的查詢執行的流程就是 二級緩存 -> 一級緩存 -> 資料庫。
2.2快速入門
(1)mybatis-config.xml配置中開啟二級緩存
設置名 | 描述 | 有效值 | 預設值 |
---|---|---|---|
cacheEnabled | 全局性開啟或關閉所有映射器配置文件中已配置的任何緩存 | true、false | true |
<settings>
<!--開啟二級緩存,預設下值為true-->
<setting name="cacheEnabled" value="true"/>
</settings>
(2)使用二級緩存時entity類實現序列化介面(serializable),因為二級緩存可能使用到序列化技術
大部分情況下,二級緩存不去置序列化也可以使用,只是有些二級緩存產品可能用到序列化

(3)在對應的xxMapper.xml中設置二級緩存的策略
<!--配置二級緩存
FIFO:先進先出,按對象進入緩存的順序來移除它們
flushInterval:刷新間隔為60000ms,即60s
size:存儲結果對象或列表的 512 個引用,預設為1024
readOnly:只讀屬性,如果只用於讀操作,建議設置成true,如果有修改操作,則設置為false(預設)
-->
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
(4)測試
//測試二級緩存
@Test
public void level2CacheTest() {
System.out.println("==========第一次查詢=========");
Monster monster = monsterMapper.getMonsterById(5);
System.out.println("monster=" + monster);
//關閉這個會話
if (sqlSession != null) {
sqlSession.close();
}
System.out.println("==========第二次查詢=========");
//獲取新的sqlSession會話
sqlSession = MybatisUtils.getSqlSession();
monsterMapper = sqlSession.getMapper(MonsterMapper.class);
Monster monster2 = monsterMapper.getMonsterById(5);
System.out.println("monster=" + monster2);
if (this.sqlSession != null) {
this.sqlSession.close();
}
}
測試結果:二級緩存的作用域是全局範圍,因此不同的sqlSession會話都有效
二級緩存命中率 = 緩存生效的次數 / 總查詢的次數
2.3註意事項和使用細節
2.3.1二級緩存的策略的參數
要啟用全局的二級緩存,只需要在你的 SQL 映射文件中添加一行:
<cache/>
可以通過 cache 元素的屬性來修改你的策略。比如:
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
配置創建了一個 FIFO 緩存,每隔 60 秒刷新,最多可以存儲結果對象或列表的 512 個引用,而且返回的對象被認為是只讀的,因此對它們進行修改可能會在不同線程中的調用者產生衝突。
eviction:緩存的回收策略。
flushInterval(刷新間隔)屬性為任意的正整數,設置的值應該是一個以毫秒為單位時間。 預設情況是不設置,也就是沒有刷新間隔,緩存僅僅會在調用語句時刷新。
size(引用數目)屬性為任意正整數,欲緩存對象的大小和運行環境中可用的記憶體資源有關。預設值為 1024。
readOnly(只讀)屬性可以為 true 或 false。只讀緩存會給所有調用者返回緩存對象的相同實例。 因此這些對象不能被修改,從而使性能提升。而可讀寫的緩存會(通過序列化)返回緩存對象的拷貝。 速度上會慢一些,但是更安全,因此預設值是 false。
2.3.2四大策略
LRU
– 最近最少使用:移除最長時間不被使用的對象。(預設策略)FIFO
– 先進先出:按對象進入緩存的順序來移除它們。SOFT
– 軟引用:基於垃圾回收器狀態和軟引用規則移除對象。WEAK
– 弱引用:更積極地基於垃圾收集器狀態和弱引用規則移除對象。
2.3.3如何禁用二級緩存

(1)在mybatis-config.xml文件的settings標簽中設置二級緩存的開關。
註意這裡的配置只是和二級緩存有關,和一級緩存無關
<settings>
<!--全局性開啟或關閉所有映射器配置文件中已經配置的任何緩存,可以理解為二級緩存的總開關,預設為true-->
<setting name="cacheEnabled" value="false"/>
</settings>
(2)二級緩存的設置不僅要在配置文件中設置,還要在對應的映射文件中配置才有效。因此如果要禁用二級緩存,也可以在對應的映射文件中註銷cache元素,這時候二級緩存對該映射文件無效。

(3)或者使用控制力度更加精確的方法,直接在配置方法上指定
設置useCache="false"可以禁用當前select語句的二級緩存,即每次查詢都會發出sql去查詢,預設情況為true

註意:一般不用去修改,使用預設的即可
2.3.4刷新二級緩存的設置
insert,update,delete 操作數據後需要刷新緩存,如果不執行刷新緩存會出現臟讀:

預設情況下 flushCache 的值為true,一般不用修改。
3.一級緩存和二級緩存的執行順序
緩存的執行順序為:二級緩存-->一級緩存-->資料庫
測試:驗證緩存的執行順序,我們事先打開二級緩存和一級緩存。
//二級緩存->一級緩存->資料庫
@Test
public void cacheSeqTest() {
System.out.println("==========第1次查詢=========");
//Cache Hit Ratio: 0.0
Monster monster = monsterMapper.getMonsterById(8);
System.out.println(monster);
//當一級緩存的會話被關閉時,一級緩存的數據就會被放入二級緩存,前提是二級緩存是開啟的
sqlSession.close();
sqlSession = MybatisUtils.getSqlSession();
monsterMapper = sqlSession.getMapper(MonsterMapper.class);
System.out.println("==========第2次查詢=========");
//從二級緩存獲取 id=8 的 monster信息
//Cache Hit Ratio: 0.5
Monster monster2 = monsterMapper.getMonsterById(8);
System.out.println(monster2);
System.out.println("==========第3次查詢=========");
//這時一二級緩存都有數據,但是由於先查詢二級緩存,因此數據依然是從二級緩存中獲取的
//Cache Hit Ratio: 0.6666666666666666
Monster monster3 = monsterMapper.getMonsterById(8);
System.out.println(monster3);
sqlSession.close();
}

註意事項:
不會出現一級緩存和二級緩存中有同一個數據,因為二級緩存的數據是在一級緩存關閉之後才有的。(當一級緩存的會話被關閉時,如果二級緩存開啟了,一級緩存的數據就會被放入二級緩存)
//分析執行順序,二級緩存的數據是在一級緩存被關閉之後才有的,不會出現一二級緩存同時擁有相同數據的情況
@Test
public void cacheSeqTest2() {
System.out.println("==========第1次查詢=========");
//二級緩存命中率 Cache Hit Ratio: 0.0,走資料庫
Monster monster = monsterMapper.getMonsterById(8);
System.out.println(monster);
System.out.println("==========第2次查詢=========");
//Cache Hit Ratio: 0.0
//拿的是一級緩存的數據,不會發出sql
Monster monster2 = monsterMapper.getMonsterById(8);
System.out.println(monster2);
System.out.println("==========第3次查詢=========");
//Cache Hit Ratio: 0.0
//拿的是一級緩存的數據,不會發出sql
Monster monster3 = monsterMapper.getMonsterById(8);
System.out.println(monster3);
if (sqlSession != null) {
sqlSession.close();
}
}
4.EhCache緩存
4.1基本介紹
- EhCache是一個純Java的緩存框架,具有快速、精幹等特點
- Mybatis有自己預設的二級緩存(前面我們已經講過了),但是在實際項目中,往往使用的是更加專業的第三方緩存產品,作為MyBatis的二級緩存,EhCache就是非常優秀的緩存產品
4.2配置和使用EhCache
(1)加入相關依賴,修改pom.xml文件

<dependencies>
<!--引入EhCache核心庫-->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.11</version>
</dependency>
<!--引入需要使用的slf4j-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<!--引入mybatis整合ehcache庫-->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.2.1</version>
</dependency>
</dependencies>
(2)確保mybatis-config.xml文件打開了二級緩存
<settings>
<!--不配置也可以,因為二級緩存預設是打開的-->
<setting name="cacheEnabled" value="true"/>
</settings>
(3)在resource目錄下加入ehcache.xml 配置文件
Java Ehcache緩存的timeToIdleSeconds和timeToLiveSeconds區別 - TaoBye
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
<!--diskStore:為緩存路徑,ehcache分為記憶體和磁碟兩級,此屬性定義磁碟的緩存位置。參數解釋如下:
user.home – 用戶主目錄
user.dir – 用戶當前工作目錄
java.io.tmpdir – 預設臨時文件路徑 -->
<diskStore path="java.io.tmpdir/Tmp_EhCache"/>
<!--defaultCache:預設緩存策略,當ehcache找不到定義的緩存時,使用這個緩存策略。只能定義一個-->
<!--name:緩存名稱。
maxElementsInMemory:緩存最大數目
maxElementsOnDisk:硬碟最大緩存個數。
eternal:對象是否永久有效,一但設置了,timeout將不起作用。
overflowToDisk:是否保存到磁碟,當系統宕機時
timeToIdleSeconds:設置對象在失效前的允許閑置時間(單位:秒)。僅當eternal=false對象不是永久有效時使用,可選屬性,預設值是0,也就是可閑置時間無窮大。
timeToLiveSeconds:設置對象在失效前允許存活時間(單位:秒)。最大時間介於創建時間和失效時間之間。僅當eternal=false對象不是永久有效時使用,預設是0,也就是對象存活時間無窮大。
diskPersistent:是否緩存虛擬機重啟期數據 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
diskSpoolBufferSizeMB:這個參數設置DiskStore(磁碟緩存)的緩存區大小。預設是30MB。每個Cache都應該有自己的一個緩衝區。
diskExpiryThreadIntervalSeconds:磁碟失效線程運行時間間隔,預設是120秒。
memoryStoreEvictionPolicy:當達到maxElementsInMemory限制時,Ehcache將會根據指定的策略去清理記憶體。預設策略是LRU(最近最少使用)。你可以設置為FIFO(先進先出)或是LFU(較少使用)。
clearOnFlush:記憶體數量最大時是否清除。
memoryStoreEvictionPolicy:可選策略有:LRU(最近最少使用,預設策略)、FIFO(先進先出)、LFU(最少訪問次數)。
FIFO,first in first out,這個是大家最熟的,先進先出。
LFU, Less Frequently Used,就是上面例子中使用的策略,直白一點就是講一直以來最少被使用的。如上面所講,緩存的元素有一個hit屬性,hit值最小的將會被清出緩存。
LRU,Least Recently Used,最近最少使用的,緩存的元素有一個時間戳,當緩存容量滿了,而又需要騰出地方來緩存新的元素的時候,那麼現有緩存元素中時間戳離當前時間最遠的元素將被清出緩存。-->
<defaultCache
eternal="false"
maxElementsInMemory="10000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="259200"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
(4)在XxxMapper.xml中啟用了EhCace,當然原來Mybatis自帶的緩存配置需要註銷
<!--啟用ehcache-->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
(5)測試
//測試二級緩存
@Test
public void ehCacheTest() {
System.out.println("==========第1次查詢=========");
Monster monster = monsterMapper.getMonsterById(5);
System.out.println("monster=" + monster);
//關閉當前會話,一級緩存數據失效,將數據放入二級緩存(此時為 ehcache)
if (sqlSession != null) {
sqlSession.close();
}
//獲取新的sqlSession會話
sqlSession = MybatisUtils.getSqlSession();
monsterMapper = sqlSession.getMapper(MonsterMapper.class);
System.out.println("==========第2次查詢=========");
//從二級緩存ehcache中獲取數據,不會發出sql
Monster monster2 = monsterMapper.getMonsterById(5);
System.out.println("monster=" + monster2);
System.out.println("==========第3次查詢=========");
//還是從二級緩存獲取數據,不會發出sql
Monster monster3 = monsterMapper.getMonsterById(5);
System.out.println("monster=" + monster3);
if (this.sqlSession != null) {
this.sqlSession.close();
}
}


4.3EhCache緩存細節說明
如何理解EhCache和Mybatis緩存的關係?
-
MyBatis提供了一個Cache介面,只要實現了該Cache介面,就可以作為二級緩存產品和MyBatis整合使用,EhCache就是實現了該介面。
-
MyBatis預設情況(即一級緩存)是使用的PerpetualCache類實現Cache介面的,是核心類
-
當我們使用了EhCache後,就是EhcacheCache類實現Cache介面,它是核心類
-
緩存的本質就是
Map<Object,Object>