MyBatis緩存詳解


緩存體系結構

緩存一般是ORM框架都會提供的功能,目的就是提升查詢效率和減少數據庫的壓力。跟Hibernate一樣,MyBatis也有一級緩存和二級緩存,並且預留了集成第三方緩存的接口。

MyBatis跟緩存相關的類都在cache包里面,其中有一個Cache接口,只有一個默認的實現類PerpetualCache,它使用HashMap實現的。

除此之外,還有很多的裝飾器,通過這些裝飾器可以額外實現很多的功能:回收策略、日志記錄、定時刷新等。

如果對裝飾器不懂的,可以去看一下設計模式中的裝飾者模式的資料。

但無論怎么裝飾,最后使用的還是最基本的實現類PerpetualCache。

所有的緩存實現類總體上可以分為三類:基本緩存、淘汰算法緩存、裝飾器緩存。

緩存實現類 描述 作用 裝飾條件
基本緩存 緩存基本實現類 默認是PerpetualCache,也可以自定義比如RedisCache、EhCache等,具備基本功能的緩存類
LruCache LRU策略的緩存 當緩存達到上限時,刪除最近最少使用的緩存(Least Recently Use) eviction="LRU"(默認)
FifoCache FIFO策略的緩存 當緩存到達上限時,刪除最先入隊的緩存 eviction="FIFO"

SoftCache

WeakCache

帶清理策略的緩存 通過JVM的軟引用和弱引用來實現緩存,當JVM內存不足時,會自動清理掉這些緩存,基於SoftReference和WeakReference

eviction="SOFT"

eviction="WEAK"

LoggingCache 帶日志功能的緩存 比如:輸出緩存命中率 基本
SynchronizedCache 同步緩存 基於synchronized關鍵字實現,解決並發問題 基本
BlockingCache 阻塞緩存 通過在get/put方式中加鎖,保證只有一個縣城操作緩存,基於Java重入鎖實現 blocking=true
SerializedCache 支持序列化的緩存 將對象序列化后放到緩存中,取出時反序列化 readOnly=false(默認)
ScheduledCache 定時調度的緩存 在進行get/put/remove/getSize等操作前,判斷緩存時間是否超過了設置的最長緩存時間(默認是一小時),如果是則清空緩存。即每隔一段時間清空一次緩存 flushInterval不為空
TransactionCache 事物緩存 在二級緩存中使用,可一次存入多個緩存,移除多個緩存 在TransactionCacheManager中用Map維護對應關系

 

 

 

 

 

 

 

 

 

 

 

一級緩存

一級緩存也叫本地緩存,MyBatis的一級緩存是在會話(SqlSession)層面進行緩存的。MyBatis的一級緩存是默認開啟的,不需要任何配置。

在MyBatis執行的流程里面,涉及到那么多的對象,那么緩存PerpetualCache應該放到哪個對象里面去維護?如果要在同一個會話里面共享一級緩存,這個對象肯定是在SqlSession里面創建的,作為SqlSession的一個屬性。

SqlSession只有一個默認實現類DefaultSqlSession,DefaultSqlSession里面只有兩個屬性,Configuration是全局的,所以緩存只可能放在Executor里面維護--SimpleExecutor/ReUseExecutor/BatchExecutor的父類BaseExecutor的構造函數中持有了PerpetualCache。

在同一個會話里面,多次執行相同的SQL語句,會直接從內存取到緩存的結果,不會再發送SQL到數據庫,但是不同的會話之間,即使執行相同的SQL語句(同一個Mapper的同一個方法的相同參數),也不能使用到一級緩存。

MyBatis一級緩存

查看BaseExecutor的query源代碼:

 1 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
 2     ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
 3     if (closed) {
 4       throw new ExecutorException("Executor was closed.");
 5     }
 6     if (queryStack == 0 && ms.isFlushCacheRequired()) {
 7       clearLocalCache();
 8     }
 9     List<E> list;
10     try {
11       queryStack++;
12       list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
13       if (list != null) {
14         handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
15       } else {
16         list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
17       }
18     } finally {
19       queryStack--;
20     }
21     if (queryStack == 0) {
22       for (DeferredLoad deferredLoad : deferredLoads) {
23         deferredLoad.load();
24       }
25       // issue #601
26       deferredLoads.clear();
27       if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
28         // issue #482
29         clearLocalCache();
30       }
31     }
32     return list;
33 }
34 
35 private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
36     List<E> list;
37     localCache.putObject(key, EXECUTION_PLACEHOLDER);
38     try {
39       list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
40     } finally {
41       localCache.removeObject(key);
42     }
43     localCache.putObject(key, list);
44     if (ms.getStatementType() == StatementType.CALLABLE) {
45       localOutputParameterCache.putObject(key, parameter);
46     }
47     return list;
48 }

看到,一級緩存是在BaseExecutor的query()--queryFromDatabase()中存入,在getFromDatabase()之前會get()。

而在同一個會話中,update(包括delete)會導致一級緩存被清空:

1 public int update(MappedStatement ms, Object parameter) throws SQLException {
2     ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
3     if (closed) {
4       throw new ExecutorException("Executor was closed.");
5     }
6     // 清空一級緩存
7     clearLocalCache();
8     return doUpdate(ms, parameter);
9 }

一級緩存的不足

當有兩個會話時,第一個會話先從數據庫中查出一條記錄放到緩存中了,這時第二個會話對這條記錄進行了update,由於一級緩存只能在同一個會話中共享,所以此時第一個會話中的緩存中的數據不會變,導致會話一第二次查詢時直接從緩存拿到了臟數據。如果要解決這個問題,就要用到二級緩存。

二級緩存

 二級緩存是用來解決一級緩存不能跨會話共享的問題的,范圍是namespace級別的,可以被多個SqlSession共享(只要是同一個Mapper中的同一個方法,都可以共享),生命周期和應用同步。

作為一個作用范圍更廣的緩存,他肯定是在SqlSession的外層,否則不可能被多個SqlSession共享。而一級緩存是在SqlSession內部的,所以,二級緩存是工作在一級緩存之前的,也就是只有取不到二級緩存的情況下才到一個會話中去取一級緩存。

二級緩存放在哪個對象中維護呢?要跨會話共享的話,SqlSession本身和它里面的Executor已經滿足不了需求了,那我們應該在BaseExecutor之外創建一個對象。實際上MyBatis用了一個裝飾器的類來維護,就是CachingExecutor。如果啟用了二級緩存,MyBatis在創建Executor對象的時候會對Executor進行裝飾。

CachingExecutor對於查詢請求,會判斷二級緩存是否有緩存結果,如果有就直接返回,如果沒有就委派交給真正的查詢器Executor實現類,比如SimpleExecutor來執行查詢,再走到一級緩存的流程,最后會把結果緩存起來,並且返回給用戶。

MyBatis二級緩存流程

 開啟二級緩存的方法

第一步:在MyBatis的全局配置文件mybatis-config.xml中配置:

1 <settings>
2     <setting name="cacheEnabled" value="true"/>
3 </settings>

 

只要沒有顯示的設置cacheEnabled=false,都會用CachingExecutor裝飾基本的執行器。

第二步:在Mapper.xml中配置<cache/>標簽:

 1 <!-- 聲明這個namespace使用二級緩存 
 2     size: 最多緩存對象個數,默認1024
 3     eviction: 回收策略,默認LRU
 4     flushInterval: 自動刷新時間 ms,未配置時只有調用時刷新
 5     readOnly: 默認是false(安全),改為true時可讀寫,對象必須支持序列化
 6 -->
 7 <cache type="org.apache.ibatis.cache.impl.PerpetualCache"
 8        size="1024"
 9        eviction="LRU"
10        flushInterval="120000"
11        readOnly="false"/>

 

 cache屬性詳解:

屬性 含義 取值
type 緩存實現類 需要實現Cache接口,默認是PerpetualCache
size 最多緩存對象個數 默認1024
eviction 回收策略(緩存淘汰算法)

LRU - 最近最少使用的:移除最長時間不被使用的對象(默認)。

FIFO - 先進先出:按對象進入緩存的順序來移除他們。

SOFT - 軟引用:移除基於垃圾回收器狀態和軟引用規則的對象。

WEAK - 弱引用:更積極地移除基於垃圾收集器狀態和弱引用規則的對象。

flushInterval 定時自動清空緩存間隔 自動刷新時間,單位ms,未配置時只有調用時刷新
readOnly 是否只讀

true:只讀緩存:會給所有調用者返回緩存對象的相同實例。因此這些對象不能被修改。這提供了很重要的性能優勢。

false:讀寫緩存:會返回緩存對象的拷貝(通過序列化),不會共享。這會慢一些,但是安全,因此默認是false。對象必須支持序列化

blocking 是否使用可重入鎖實現緩存的並發控制 true,會使用BlockingCache對Cache進行裝飾。默認是false。

 

 

 

 

 

 

 

 

 

Mapper.xml中配置了<cache/>之后,select()會被緩存,insert()、update()、delete()會刷新緩存。

如果mybatis-config.xml中配置了cacheEnabled=true,Mapper.xml中沒有配置<cache/>,還有二級緩存嗎?

只要cacheEnabled設置為true,BaseExecutor就會被裝飾為CachingExecutor。Mapper.xml中有沒有配置<cache/>,決定了在啟動的時候會不會創建這個mapper的Cache對象,最終會影響到CachingExecutor的query()方法里面的判斷:

 1 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
 2       throws SQLException {
 3     Cache cache = ms.getCache();
 4     if (cache != null) {
 5       flushCacheIfRequired(ms);
 6       if (ms.isUseCache() && resultHandler == null) {
 7         ensureNoOutParams(ms, boundSql);
 8         @SuppressWarnings("unchecked")
 9         List<E> list = (List<E>) tcm.getObject(cache, key);
10         if (list == null) {
11           list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
12           tcm.putObject(cache, key, list); // issue #578 and #116
13         }
14         return list;
15       }
16     }
17     return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
18 }

 

如果某些查詢方法對數據的實時性要求很高,不需要二級緩存,可以在單個StatementId上顯示關閉二極緩存useCache="false"(默認是true):

1 <select id="selectByPk" resultMap="BaseResultMap" useCache="false">
2     select * from emp where empno = #{empNo}
3 </select>

 

什么時候開啟二級緩存

1、因為所有的增刪改都會刷新二級緩存,導致二級緩存失效,所以適合在查詢為主的應用中使用,比如歷史交易、歷史訂單的查詢。否則緩存就是去了意義 。

2、如果多個namespace中有針對於同一個表的操作,比如emp表,如果在一個namespace中刷新了緩存,另一個namespace中沒有刷新,就會出現讀到臟數據的情況。所以,推薦在一個Mapper里面只操作單表的情況使用。

跨namespace的緩存共享問題,可以使用<cache-ref>標簽來解決:

<cache-ref namespace="test.EmployeeMapper"/>

 

cache-ref代表引用別的命名空間的Cache配置,兩個命名空間的操作使用的是用一個Cache。在關聯的表比較少,或者按照業務可以對表進行分組的時候可以使用。

注意:在這種情況下,多個Mapper的操作都會引起緩存的刷新,緩存的意義已經不大了。

第三方緩存做二級緩存

除了MyBatis自帶的二級緩存之外,我們也可以通過實現Cache接口來自定義二級緩存。

MyBatis官方提供了一些第三方緩存集成方式,比如ehcache和redis:

https://github.com/mybatis/redis-cache

 pom文件中引入依賴:

1 <dependency>
2     <groupId>org.mybatis.caches</groupId>
3     <artifactId>mybatis-redis</artifactId>
4     <version>1.0.0-beta2</version>
5 </dependency>

 

Mapper.xml配置,type使用RedisCache:

1 <cache type="org.mybatis.caches.redis.RedisCache"
2    size="1024"
3    eviction="LRU"
4    flushInterval="120000"
5    readOnly="false"/>

 

redis.properties配置:

host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
database=0

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM