來來來,給俏如來扎起,感謝老鐵們對俏如來的支持,2021一路有你,2022我們繼續加油!你的肯定是我最大的動力
博主在參加博客之星評比,點擊鏈接 , https://bbs.csdn.net/topics/603957267 瘋狂打Call!五星好評 ????? 感謝
前言
對于Mybatis的快取在上一章節《吃透Mybatis原始碼-Mybatis執行流程》我們有提到一部分,這篇文章我們對將詳細分析一下Mybatis的一級快取和二級快取,
一級快取
市面上流行的ORM框架都支持快取,不管是Hibernate還是Mybatis都支持一級快取和二級快取,目的是把資料快取到JVM記憶體中,減少和資料庫的互動來提高查詢速度,同時MyBatis還可以整合三方快取技術,
Mybatis一級緩默認開啟,是SqlSession級別的,也就是說需要同一個SqlSession執行同樣的SQL和引數才有可能命中快取,如:

同一個SqlSession執行同一個SQL,發現控制臺日志只執行了一次SQL記錄,說明第二次查詢是走快取了,但是要注意的是,當SqlSession執行了delete,update,insert陳述句后,快取會被清除,
那么一級快取在哪兒呢?下面給大家介紹一個類,

Mybatis中提供的快取都是Cache的實作類,但是真正實作快取的是PerpetualCache,其中維護了一個Map<Object, Object> cache = new HashMap<Object, Object>() 結構來快取資料,其他的快取類采用了裝飾模式對PerpetualCache做增強,比如:LruCache 在PerpetualCache 的基礎上增加了最近最少使用的快取清楚策略,當快取到達上限時候,洗掉最近最少使用的快取 (Least Recently Use),代碼如下
public class LruCache implements Cache {
//對 PerpetualCache 做裝飾
private final Cache delegate;
下面對其他的快取類做一個介紹
PerpetualCache: 基礎快取類- LruCache : LRU 策略的快取 當快取到達上限時候,洗掉最近最少使用的快取 (Least Recently Use),eviction=“LRU”(默 認)
- FifoCache : FIFO 策略的快取 當快取到達上限時候,洗掉最先入隊的快取,配置eviction=“FIFO”
- SoftCache WeakCache :帶清理策略的快取 通過 JVM 的軟參考和弱參考來實作快取,當 JVM 記憶體不足時,會自動清理掉這些快取,基于 SoftReference 和 WeakReference
- SynchronizedCache : 同步快取 基于 synchronized 關鍵字實作,解決并發問題
- ScheduledCache : 定時調度的快取,在進行 get/put/remove/getSize 等操作前,判斷 快取時間是否超過了設定的最長快取時間(默認是 一小時),如果是則清空快取–即每隔一段時間清 空一次快取
- SerializedCache :支持序列化的快取 將物件序列化以后存到快取中,取出時反序列化
TransactionalCache:事務快取,在二級快取中使用,可一次存入多個快取,移除多個快取 ,通過TransactionalCacheManager 中用 Map 維護對應關系,
一級快取到底存盤在哪兒?
一級快取在SimpleExecutor 的父類 BaseExecutor 執行器中,如下
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory.getLog(BaseExecutor.class);
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
//一級快取
protected PerpetualCache localCache;
PerpetualCache快取類原始碼如下
public class PerpetualCache implements Cache {
private final String id;
//快取
private Map<Object, Object> cache = new HashMap<Object, Object>();
那么一級快取在什么時候創建的?
在 BaseExecutor 中的構造器中創建了一級快取,而執行器Executor 是保存在SqlSession中的,也就是說當創建SqlSession的時候,就會創建 SimpleExecutor,而在SimpleExecutor的構造器中會呼叫BaseExecutor的構造器來創建一級快取,見:org.apache.ibatis.executor.SimpleExecutor#SimpleExecutor
public class SimpleExecutor extends BaseExecutor {
//執行器構造器
public SimpleExecutor(Configuration configuration, Transaction transaction) {
//呼叫父類構造器
super(configuration, transaction);
}
下面是 BaseExecutor 的執行器 org.apache.ibatis.executor.BaseExecutor#BaseExecutor
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory.getLog(BaseExecutor.class);
protected Transaction transaction;
protected Executor wrapper;
//一級快取
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
//創建一級快取
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
一級快取怎么存盤的?
一級快取是在執行查詢的時候會先走二級快取,二級快取么有就會走一級快取,以及快取沒有就會走資料庫查詢,然后放入一級快取和二級快取,我們來看一下原始碼流程 ,見:org.apache.ibatis.executor.CachingExecutor#query
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
//構建快取的Key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
//執行查詢
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
這里在嘗試構建Cachekey ,cachekey時由:MappedStatement的id(如:cn.xx.xx.xxMapper.selectByid) ,分頁,Sql,引數值一起構建而成的,一級二級快取都是如此,
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//開啟了二級快取才會存在Cache
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//走二級快取查詢資料
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//二級快取沒有,走資料庫查詢資料
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//寫入二級快取
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
這里我們看到,在執行org.apache.ibatis.executor.CachingExecutor#query 查詢的時候會先走二級快取,二級快取沒有會繼續呼叫 org.apache.ibatis.executor.BaseExecutor#query 查詢,而BaseExecutor#query會嘗試先走一級快取
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
//【重要】走一級快取獲取資料
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//如果一級快取中沒有,走資料庫查詢資料
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
上面代碼會先走一級快取拿資料,如果一級快取沒有,就走資料庫獲取資料,然后加入一級快取org.apache.ibatis.executor.BaseExecutor#queryFromDatabase
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//走資料庫查詢資料
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
//把資料寫入一級快取
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
到這里我們就看到了一級快取和二級快取的執行流程,注意的是:先執行二級快取再執行一級快取,
這里畫一個一級快取的圖

二級快取
第一步:二級快取需要在mybatis-config.xml 配置中開啟,如下
<setting name="cacheEnabled" value="true"/>
當然其實該配置默認是開啟的,也就是默認會使用 CachingExecutor 裝飾基本的執行器,
第二步驟:需要在mapper.xml中配置 < cache/>如下
<mapper namespace="cn.whale.mapper.StudentMapper">
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024"
eviction="LRU"
flushInterval="120000"
readOnly="false"/>
...省略...
解釋一下上面的配置,首先<cache/> 是在某個mapper.xml中指定的,也就是說二級快取作用于當前的namespace.
- type : 代表的是使用什么型別的快取,只要是實作了 Cache 介面的實作類都可以
- size :快取的個數,默認是1024 個物件
- eviction : 快取剔除策略 ,LRU – 最近最少使用的:移除最長時間不被使用的物件(默認);FIFO – 先進先出:按物件進入快取的順序來移除它們 ;SOFT – 軟參考:移除基于垃圾回收器狀態和軟參考規則的物件;WEAK – 弱參考:更積極地移除基于垃圾收集器狀態和弱參考規則的物件
- flushInterval :定時自動清空快取間隔 自動重繪時間,單位 ms,未配置時只有呼叫時重繪
- readOnly :快取時候只讀
- blocking :是否使用可重入鎖實作 快取的并發控制 true,會使用 BlockingCache 對 Cache 進行裝飾 默認 false
Mapper.xml 配置了之后,select()會被快取,update()、delete()、insert() 會重繪快取,下面是測驗案例

可以看到,這里使用了2個SqlSesion 2次執行了相同的SQL,引數相同,看控制臺日志只執行了一次SQL,說明是命中的二級快取,因為滿足條件:同一個 namespace下的相同的SQL被執行,盡管使用的SqlSession不是同一個,
但是你可能注意到一個細節,就是session.commit() 為什么要提交事務呢?這就要說到二級快取的存盤結構了,如果不執行commit是不會寫入二級快取的,在 CachingExecutor 中有一個屬性private final TransactionalCacheManager tcm = new TransactionalCacheManager(); 看名字肯能夠看出二級快取和事務有關系,結構如下
public class CachingExecutor implements Executor {
private final Executor delegate;
//二級快取,通過TransactionalCacheManager來管理
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
TransactionalCacheManager 中維護了一個 HashMap<Cache, TransactionalCache>()
public class TransactionalCacheManager {
//二級快取的HashMap
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
在TransactionCache中維護了一個 Map<Object, Object> entriesToAddOnCommit;
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
private final Cache delegate;
private boolean clearOnCommit;
//二級快取臨時存盤
private final Map<Object, Object> entriesToAddOnCommit;
...省略...
//寫入二級快取
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
當執行查詢的時候,從資料庫查詢出來資料回寫入TransactionalCache的entriesToAddOnCommit中,我們來看一下二級快取寫入的流程,見:org.apache.ibatis.executor.CachingExecutor#query
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//如果mapper.xml配置了 <cache/> 就會創建 Cache
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//從二級快取獲取
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//寫入二級快取
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
如果mapper.xml配置了 就會創建 Cache,Cache不為null,才會走到二級快取的流程,此時代碼來到org.apache.ibatis.cache.TransactionalCacheManager#putObject
public class TransactionalCacheManager {
//存盤二級快取
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
public void putObject(Cache cache, CacheKey key, Object value) {
//通過cache為key拿到 TransactionalCache ,把資料put進去
getTransactionalCache(cache).putObject(key, value);
}
存盤資料的是TransactionalCache ,見org.apache.ibatis.cache.decorators.TransactionalCache#putObject
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
//正在的二級快取存盤位置
private final Cache delegate;
private boolean clearOnCommit;
//臨時的二級快取存盤位置
private final Map<Object, Object> entriesToAddOnCommit;
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
我們看到,資料寫到了 TransactionalCache#entriesToAddOnCommit 一個Map中,只有在執行commit的時候資料才會真正寫入二級快取,
我們來看下SqlSession.commit方法是如何觸發二級快取真正的寫入的,見:org.apache.ibatis.session.defaults.DefaultSqlSession#commit()
@Override
public void commit() {
commit(false);
}
@Override
public void commit(boolean force) {
try {
//呼叫執行器提交事務
executor.commit(isCommitOrRollbackRequired(force));
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
代碼來到org.apache.ibatis.executor.CachingExecutor#commit
@Override
public void commit(boolean required) throws SQLException {
//提交事務
delegate.commit(required);
//呼叫org.apache.ibatis.cache.TransactionalCacheManager#commit提交事務
tcm.commit();
}
代碼來到org.apache.ibatis.cache.TransactionalCacheManager#commit
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
//呼叫 TransactionalCache#commit
txCache.commit();
}
}
代碼來到org.apache.ibatis.cache.decorators.TransactionalCache#commit
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
//真正的二級快取存盤位置,本質是一個 PerpetualCache
private final Cache delegate;
//臨時存盤二級快取
private final Map<Object, Object> entriesToAddOnCommit;
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
//這里在寫入快取,保存到TransactionalCache中的delegate欄位,本質是一個PerpetualCache
flushPendingEntries();
//把entriesToAddOnCommit清除掉
reset();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
//從entriesToAddOnCommit中拿到臨時的快取資料,寫入快取,最侄訓寫入PerpetualCache#cache欄位中
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
private void reset() {
clearOnCommit = false;
//清除entriesToAddOnCommit
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
所以我們總結一下二級快取的寫入流程,二級快取通過 TransactionalCacheManager中的一個Map<Cache, TransactionalCache>管理的,當執行query查詢處資料的時候,會把資料寫入TransactionalCache中的 Map<Object, Object> entriesToAddOnCommit 中臨時存盤,當執行commit的時候才會把entriesToAddOnCommit中的資料寫入TransactionalCache中的 Cache delegate ,其本質和一級快取一樣,也是一個 PerpetualCache,
當我們做第二次query的時候會嘗試通過 TransactionalCacheManager#getObject 從二級快取獲取資料
public class TransactionalCacheManager {
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
//獲取二級快取
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
然后會從 TransactionalCache中的delegate中獲取快取
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
//二級快取
private final Cache delegate;
...省略...
@Override
public Object getObject(Object key) {
// issue #116
//從二級快取獲取資料
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}
所以記得,二級快取一定要commit才會起作用,下面花了一個一級快取和二級快取的結構圖

三方快取框架
除了使用Mybatis自帶的快取,也可以使用第三方快取方式,比如:比如 ehcache 和 redis 下面以Redis為例 ,首先匯入mybatis整合redis的依賴
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
第二步驟:在mapper.xml配置快取
<cache type="org.mybatis.caches.redis.RedisCache"
eviction="FIFO"
flushInterval="60000"
size="512" readOnly="true"/>
這里type使用了RedisCache,RedisCache也是實作了Cache介面的,接著我們需要配置Redis的鏈接屬性,默認RedisCache類會讀取名字為 : redis.properties 的組態檔
host=127.0.0.1
password=123456
port=6379
connectionTimeout=5000
soTimeout=5000
database=0
再次執行測驗代碼,查看Redis效果如下

博主在參加博客之星評比,點擊鏈接 , https://bbs.csdn.net/topics/603957267 瘋狂打Call!五星好評 ????? 感謝
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/404127.html
標籤:java
