緩存體系結構
緩存一般是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的同一個方法的相同參數),也不能使用到一級緩存。
查看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-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