關(guān)于 Mybatis 緩存的那點(diǎn)事兒,你知道嗎?
緩存實(shí)現(xiàn)的方式
一級(jí)緩存
二級(jí)緩存
案例實(shí)操
1. 一級(jí)緩存
基于 PerpetualCache 的 HashMap 本地緩存(mybatis 內(nèi)部實(shí)現(xiàn) cache 接口),其存儲(chǔ)作用域?yàn)?Session,當(dāng) Session flush 或 close 之后,該 Session 中的所有 Cache 就將清空;
2. 二級(jí)緩存
一級(jí)緩存其機(jī)制相同,默認(rèn)也是采用 PerpetualCache 的 HashMap 存儲(chǔ),不同在于其存儲(chǔ)作用域?yàn)?Mapper(Namespace),并且可自定義存儲(chǔ)源,如 Ehcache;
對(duì)于緩存數(shù)據(jù)更新機(jī)制,當(dāng)某一個(gè)作用域(一級(jí)緩存 Session/二級(jí)緩存 Namespaces)的進(jìn)行了 C/R/U/D 操作后,默認(rèn)該作用域下所有 select 中的緩存將被 clear。
如果二緩存開(kāi)啟,首先從二級(jí)緩存查詢數(shù)據(jù),如果二級(jí)緩存有則從二級(jí)緩存中獲取數(shù)據(jù),如果二級(jí)緩存沒(méi)有,從一級(jí)緩存找是否有緩存數(shù)據(jù),如果一級(jí)緩存沒(méi)有,查詢數(shù)據(jù)庫(kù)。
3. ?二級(jí)緩存局限性
mybatis 二級(jí)緩存對(duì)細(xì)粒度的數(shù)據(jù)級(jí)別的緩存實(shí)現(xiàn)不好,對(duì)同時(shí)緩存較多條數(shù)據(jù)的緩存,比如如下需求:對(duì)商品信息進(jìn)行緩存,由于商品信息查詢?cè)L問(wèn)量大,但是要求用戶每次都能查詢最新的商品信息,此時(shí)如果使用 mybatis 的二級(jí)緩存就無(wú)法實(shí)現(xiàn)當(dāng)一個(gè)商品變化時(shí)只刷新該商品的緩存信息而不刷新其它商品的信息,因?yàn)?mybaits 的二級(jí)緩存區(qū)域以 mapper 為單位劃分,當(dāng)一個(gè)商品信息變化會(huì)將所有商品信息的緩存數(shù)據(jù)全部清空
4. 一級(jí)緩存(默認(rèn)開(kāi)啟)
Mybatis 默認(rèn)提供一級(jí)緩存,緩存范圍是一個(gè) sqlSession。在同一個(gè) SqlSession 中,兩次執(zhí)行相同的 sql 查詢,第二次不再?gòu)臄?shù)據(jù)庫(kù)查詢。
原理:一級(jí)緩存采用 Hashmap 存儲(chǔ),mybatis 執(zhí)行查詢時(shí),從緩存中查詢,如果緩存中沒(méi)有從數(shù)據(jù)庫(kù)查詢。如果該?SqlSession?執(zhí)行?clearCache()?提交或者增加刪除修改操作,清除緩存。
默認(rèn)就存在,了解觀察結(jié)果即可
a.緩存存在情況(session 未提交)
@Test?
public void test01() {?
SqlSession sqlSession=sqlSessionFactory.openSession();
AccountDao accountDao=sqlSession.getMapper(AccountDao.class);
Account account=accountDao.queryAccountById(1);?
System.out.println(account);?
accountDao.queryAccountById(1);
}
日志僅打印一條 sql
b.刷新緩存
Session 提交此時(shí)緩存數(shù)據(jù)被刷新
@Test?
public void test02() {?
SqlSession sqlSession=sqlSessionFactory.openSession();
AccountDao accountDao=sqlSession.getMapper(AccountDao.class);
Account account=accountDao.queryAccountById(1);?
System.out.println(account);?
sqlSession.clearCache();?
accountDao.queryAccountById(1);
}
效果:

5. 二級(jí)緩存
一級(jí)緩存是在同一個(gè) sqlSession 中,二級(jí)緩存是在同一個(gè) namespace 中,因此相同的 namespace 不同的 sqlsession 可以使用二級(jí)緩存。
使用場(chǎng)景
對(duì)查詢頻率高,變化頻率低的數(shù)據(jù)建議使用二級(jí)緩存。
對(duì)于訪問(wèn)多的查詢請(qǐng)求且用戶對(duì)查詢結(jié)果實(shí)時(shí)性要求不高,此時(shí)可采用 mybatis 二級(jí)緩存技術(shù)降低數(shù)據(jù)庫(kù)訪問(wèn)量,提高訪問(wèn)速度,業(yè)務(wù)場(chǎng)景比如:耗時(shí)較高的統(tǒng)計(jì)分析 sql、電話賬單查詢 sql 等。
全局文件配置(mybatis.xml)
<setting name="cacheEnabled" value="true"/>?Mapper.xml 中加入 :打開(kāi)該 mapper 的二級(jí)緩存?
<cache/>
cache 標(biāo)簽常用屬性
<cache
eviction="FIFO"??
flushInterval="60000" ?
size="512" ?
readOnly="true"/>
說(shuō)明:
映射語(yǔ)句文件中的所有 select 語(yǔ)句將會(huì)被緩存。
映射語(yǔ)句文件中的所有 insert,update 和 delete 語(yǔ)句會(huì)刷新緩存。
緩存會(huì)使用 Least Recently Used(LRU,最近最少使用的)算法來(lái)收回。
緩存會(huì)根據(jù)指定的時(shí)間間隔來(lái)刷新.
緩存會(huì)存儲(chǔ) 1024 個(gè)對(duì)象
PO 對(duì)象必須支持序列化
public class User implements Serializable {?
}
關(guān)閉 Mapper 下的具體的 statement 的緩存
使用 useCache:默認(rèn)為 true
<select id="findUserByid" parameterType="int" resultType="User"
useCache="false">?
SELECT * FROM user WHERE id=#{id}?
</select>
刷新二級(jí)緩存
操作 CUD 的 statement 時(shí)候,會(huì)強(qiáng)制刷新二級(jí)緩存 即默認(rèn) flushCache="true" ,如果想關(guān)閉設(shè)定為 flushCache="false"即可 ,不建議關(guān)閉刷新,因?yàn)椴僮鞲聞h除修改,關(guān)閉后容易獲取臟數(shù)據(jù)。
二級(jí)緩存測(cè)試:
@Test?
public void test03() {?
SqlSession sqlSession=sqlSessionFactory.openSession();
AccountDao accountDao=sqlSession.getMapper(AccountDao.class);
Account account=accountDao.queryAccountById(1);?
System.out.println(account);?
sqlSession.close();?
SqlSession sqlSession2=sqlSessionFactory.openSession();?
AccountDao accountDao2=sqlSession2.getMapper(AccountDao.class);
accountDao2.queryAccountById(1);?
sqlSession.close();?
}
效果:
擴(kuò)展
分布式緩存 ehcache
如果有多條服務(wù)器 ,不使用分布緩存,緩存的數(shù)據(jù)在各個(gè)服務(wù)器單獨(dú)存儲(chǔ),不方便系統(tǒng)開(kāi)發(fā)。所以要使用分布式緩存對(duì)緩存數(shù)據(jù)進(jìn)行集中管理。因此可是使用 ehcache ?memcached redis
mybatis 本身來(lái)說(shuō)是無(wú)法實(shí)現(xiàn)分布式緩存的,所以要與分布式緩存框架進(jìn)行整合。 EhCache 是一個(gè)純 Java 的進(jìn)程內(nèi)緩存框架,具有快速、精干等特點(diǎn);Ehcache 是一種廣泛 使用的開(kāi)源 Java 分布式緩存。主要面向通用緩存,Java EE 和輕量級(jí)容器。它具有內(nèi)存和磁盤(pán)存儲(chǔ),緩存加載器,緩存擴(kuò)展,緩存異常處理程序,一個(gè) gzip 緩存 servlet 過(guò)濾器,支持 REST 和 SOAP api 等特點(diǎn)。
Jar 依賴
<dependency>?
<groupId>net.sf.ehcache</groupId>?
<artifactId>ehcache-core</artifactId>?
<version>2.4.4</version>?
</dependency>?
<dependency>?
<groupId>org.mybatis.caches</groupId>?
<artifactId>mybatis-ehcache</artifactId>?
<version>1.0.3</version>?
</dependency>
緩存接口配置
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
在 src 下 加入 ehcache.xml(不是必須的沒(méi)有使用默認(rèn)配置)
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"?
xsi:noNamespaceSchemaLocation="../bin/ehcache.xsd">?
name:Cache 的唯一標(biāo)識(shí)?
maxElementsInMemory:內(nèi)存中最大緩存對(duì)象數(shù)?
maxElementsOnDisk:磁盤(pán)中最大緩存對(duì)象數(shù),若是 0 表示無(wú)窮大?
eternal:Element 是否永遠(yuǎn)不過(guò)期,如果為 true,則緩存的數(shù)據(jù)始終有效,如果為 false?
那么還要根據(jù) timeToIdleSeconds,timeToLiveSeconds 判斷?
overflowToDisk:配置此屬性,當(dāng)內(nèi)存中 Element 數(shù)量達(dá)到 maxElementsInMemory 時(shí),?
Ehcache 將會(huì) Element 寫(xiě)到磁盤(pán)中?
timeToIdleSeconds:設(shè)置 Element 在失效前的允許閑置時(shí)間。僅當(dāng) element 不是永久有效?
時(shí)使用,可選屬性,默認(rèn)值是 0,也就是可閑置時(shí)間無(wú)窮大?
timeToLiveSeconds:設(shè)置 Element 在失效前允許存活時(shí)間。最大時(shí)間介于創(chuàng)建時(shí)間和失效?
時(shí)間之間。僅當(dāng) element 不是永久有效時(shí)使用,默認(rèn)是 0.,也就是 element 存活時(shí)間無(wú)窮?
大?
diskPersistent:是否緩存虛擬機(jī)重啟期數(shù)據(jù)?
diskExpiryThreadIntervalSeconds:磁盤(pán)失效線程運(yùn)行時(shí)間間隔,默認(rèn)是 120 秒?
diskSpoolBufferSizeMB:這個(gè)參數(shù)設(shè)置 DiskStore(磁盤(pán)緩存)的緩存區(qū)大小。默認(rèn)是?
30MB。每個(gè) Cache 都應(yīng)該有自己的一個(gè)緩沖區(qū)?
memoryStoreEvictionPolicy:當(dāng)達(dá)到 maxElementsInMemory 限制時(shí),Ehcache 將會(huì)根據(jù)?
指定的策略去清理內(nèi)存。默認(rèn)策略是 LRU(最近最少使用)。你可以設(shè)置為 FIFO(先進(jìn)先?
出)或是 LFU(較少使用)?
-->?
<defaultCache overflowToDisk="true" eternal="false"/>?
<diskStore path="D:/cache"?/>?
timeToIdleSeconds="300" timeToLiveSeconds="600" maxElementsInMemory="1000"?
maxElementsOnDisk="10" diskPersistent="true"
diskExpiryThreadIntervalSeconds="300"?
diskSpoolBufferSizeMB="100" memoryStoreEvictionPolicy="LRU" />?
-->
測(cè)試:
@Test?
public void test04() {?
SqlSession sqlSession=sqlSessionFactory.openSession();
AccountDao accountDao=sqlSession.getMapper(AccountDao.class);
Account account=accountDao.queryAccountById(1);?
System.out.println(account);?
sqlSession.close();?
SqlSession sqlSession2=sqlSessionFactory.openSession();?
AccountDao accountDao2=sqlSession2.getMapper(AccountDao.class);
accountDao2.queryAccountById(1);?
sqlSession.close();?
}
效果:
Cache Hit Ratio [com.xxx.dao.AccountDao]:0.5