相關文章sql
前言數據庫
緩存的相關接口數組
一級緩存的實現過程緩存
二級緩存的實現過程安全
如何保證緩存的線程安全mybatis
緩存的裝飾器app
Mybatis Mapper.xml 配置文件中 resultMap 節點的源碼解析ide
Mybatis Mapper 接口源碼解析(binding包)
Mybatis 數據源和數據庫鏈接池源碼解析(DataSource)
在使用諸如 Mybatis 這種 ORM 框架的時候,通常都會提供緩存功能,用來緩存從數據庫查詢到的結果,當下一次查詢條件相同的時候,只需從緩存中進行查找返回便可,若是緩存中沒有,再去查庫;一方面是提升查詢速度,另外一方面是減小數據庫壓力;Mybatis 也提供了緩存,它分爲一級緩存和二級緩存,接下來就來看看它的緩存系統是如何實現的。
緩存系統的實現使用了 模板方法模式 和 裝飾器模式
接下來先來看下和緩存相關的接口
Mybatis 使用 Cache 來表示緩存,它是一個接口,定義了緩存須要的一些方法,以下所示:
public interface Cache { //獲取緩存的id,即 namespace String getId(); // 添加緩存 void putObject(Object key, Object value); //根據key來獲取緩存對應的值 Object getObject(Object key); // 刪除key對應的緩存 Object removeObject(Object key); // 清空緩存 void clear(); // 獲取緩存中數據的大小 int getSize(); //取得讀寫鎖, 從3.2.6開始沒用了 ReadWriteLock getReadWriteLock(); }
對於每個 namespace 都會建立一個緩存的實例,Cache 實現類的構造方法都必須傳入一個 String 類型的ID,Mybatis自身的實現類都使用 namespace 做爲 ID
Mybatis 爲 Cache 接口提供的惟一一個實現類就是 PerpetualCache,這個惟一併非說 Cache 只有一個實現類,只是緩存的處理邏輯,Cache 還有其餘的實現類,可是隻是做爲裝飾器存在,只是對 Cache 進行包裝而已。
PerpetualCache 的實現比較簡單,就是把對應的 key-value 緩存數據存入到 map 中,以下所示:
public class PerpetualCache implements Cache { // id,通常對應mapper.xml 的namespace 的值 private String id; // 用來存放數據,即緩存底層就是使用 map 來實現的 private Map<Object, Object> cache = new HashMap<Object, Object>(); public PerpetualCache(String id) { this.id = id; } //......其餘的getter方法..... // 添加緩存 @Override public void putObject(Object key, Object value) { cache.put(key, value); } // 獲取緩存 @Override public Object getObject(Object key) { return cache.get(key); } // 刪除緩存 @Override public Object removeObject(Object key) { return cache.remove(key); } // 清空緩存 @Override public void clear() { cache.clear(); } }
從上面的代碼邏輯能夠看到,mybatis 提供的緩存底層就是使用一個 HashMap 來實現的,可是咱們知道,HashMap 不是線程安全的,它是如何來保證緩存中的線程安全問題呢?在後面講到 Cache 的包裝類就知道,它提供了一個 SynchronizedCache 的裝飾器類,就是用來包裝線程安全的,在該類中全部方法都加上了 synchronized 關鍵字。
Mybatis 的緩存使用了 key-value 的形式存入到 HashMap 中,而 key 的話,Mybatis 使用了 CacheKey 來表示 key,它的生成規則爲:mappedStementId + offset + limit + SQL + queryParams + environment生成一個哈希碼.
public class CacheKey implements Cloneable, Serializable { private static final int DEFAULT_MULTIPLYER = 37; private static final int DEFAULT_HASHCODE = 17; // 參與計算hashcode,默認值爲37 private int multiplier; // CacheKey 對象的 hashcode ,默認值 17 private int hashcode; // 檢驗和 private long checksum; // updateList 集合的個數 private int count; // 由該集合中的全部對象來共同決定兩個 CacheKey 是否相等 private List<Object> updateList; public int getUpdateCount() { return updateList.size(); } // 調用該方法,向 updateList 集合添加對應的對象 public void update(Object object) { if (object != null && object.getClass().isArray()) { // 若是是數組,則循環處理每一項 int length = Array.getLength(object); for (int i = 0; i < length; i++) { Object element = Array.get(object, i); doUpdate(element); } } else { doUpdate(object); } } // 計算 count checksum hashcode 和把對象添加到 updateList 集合中 private void doUpdate(Object object) { int baseHashCode = object == null ? 1 : object.hashCode(); count++; checksum += baseHashCode; baseHashCode *= count; hashcode = multiplier * hashcode + baseHashCode; updateList.add(object); } // 判斷兩個 CacheKey 是否相等 @Override public boolean equals(Object object) { if (this == object) { return true; } if (!(object instanceof CacheKey)) { return false; } final CacheKey cacheKey = (CacheKey) object; if (hashcode != cacheKey.hashcode) { return false; } if (checksum != cacheKey.checksum) { return false; } if (count != cacheKey.count) { return false; } // 若是前幾項都不知足,則循環遍歷 updateList 集合,判斷每一項是否相等,若是有一項不相等則這兩個CacheKey不相等 for (int i = 0; i < updateList.size(); i++) { Object thisObject = updateList.get(i); Object thatObject = cacheKey.updateList.get(i); if (thisObject == null) { if (thatObject != null) { return false; } } else { if (!thisObject.equals(thatObject)) { return false; } } } return true; } @Override public int hashCode() { return hashcode; } }
若是須要進行緩存,則如何建立 CacheKey 呢?下面這個就是建立 一個 CacheKey 的方法:
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { //cacheKey 對象 CacheKey cacheKey = new CacheKey(); // 向 updateList 存入id cacheKey.update(ms.getId()); // 存入offset cacheKey.update(rowBounds.getOffset()); // 存入limit cacheKey.update(rowBounds.getLimit()); // 存入sql cacheKey.update(boundSql.getSql()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); for (ParameterMapping parameterMapping : parameterMappings) { if (parameterMapping.getMode() != ParameterMode.OUT) { String propertyName = parameterMapping.getProperty(); MetaObject metaObject = configuration.newMetaObject(parameterObject); Object value = metaObject.getValue(propertyName); // 存入每個參數 cacheKey.update(value); } } if (configuration.getEnvironment() != null) { // 存入 environmentId cacheKey.update(configuration.getEnvironment().getId()); } return cacheKey; }
從上面 CacheKey 和建立 CacheKey 的代碼邏輯能夠看出,Mybatis 的緩存使用了 mappedStementId + offset + limit + SQL + queryParams + environment 生成的hashcode做爲 key。
瞭解了上述和緩存相關的接口後,接下來就來看看 Mybatis 的緩存系統是如何實現的,Mybatis 的緩存分爲一級緩存和二級緩存,一級緩存是在 BaseExecutor 中實現的,二級緩存是在 CachingExecutor 中實現的。
Executor 接口定義了操做數據庫的基本方法,SqlSession 的相關方法就是基於 Executor 接口實現的,它定義了操做數據庫的方法以下:
public interface Executor { ResultHandler NO_RESULT_HANDLER = null; // insert | update | delete 的操做方法 int update(MappedStatement ms, Object parameter) throws SQLException; // 查詢,帶分頁,帶緩存 <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException; // 查詢,帶分頁 <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException; // 查詢存儲過程 <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException; //刷新批處理語句 List<BatchResult> flushStatements() throws SQLException; // 事務提交 void commit(boolean required) throws SQLException; // 事務回滾 void rollback(boolean required) throws SQLException; // 建立緩存的key CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql); // 是否緩存 boolean isCached(MappedStatement ms, CacheKey key); // 清空緩存 void clearLocalCache(); // 延遲加載 void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType); // 獲取事務 Transaction getTransaction(); }
BaseExecutor 是一個抽象類,實現了 Executor 接口,並提供了大部分方法的實現,只有 4 個基本方法:doUpdate, doQuery, doQueryCursor, doFlushStatement 沒有實現,仍是一個抽象方法,由子類實現,這 4 個方法至關於模板方法中變化的那部分。
Mybatis 的一級緩存就是在該類中實現的。
Mybatis 的一級緩存是會話級別的緩存,Mybatis 每建立一個 SqlSession 對象,就表示打開一次數據庫會話,在一次會話中,應用程序極可能在短期內反覆執行相同的查詢語句,若是不對數據進行緩存,則每查詢一次就要執行一次數據庫查詢,這就形成數據庫資源的浪費。又由於經過 SqlSession 執行的操做,實際上由 Executor 來完成數據庫操做的,因此在 Executor 中會創建一個簡單的緩存,即一級緩存;將每次的查詢結果緩存起來,再次執行查詢的時候,會先查詢一級緩存,若是命中,則直接返回,不然再去查詢數據庫並放入緩存中。
一級緩存的生命週期與 SqlSession 的生命週期相同,當調用 Executor.close 方法的時候,緩存變得不可用。一級緩存是默認開啓的,通常狀況下不須要特殊的配置,若是須要特殊配置,則能夠經過插件的形式來實現
public abstract class BaseExecutor implements Executor { // 事務,提交,回滾,關閉事務 protected Transaction transaction; // 底層的 Executor 對象 protected Executor wrapper; // 延遲加載隊列 protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads; // 一級緩存,用於緩存查詢結果 protected PerpetualCache localCache; // 一級緩存,用於緩存輸出類型參數(存儲過程) protected PerpetualCache localOutputParameterCache; protected Configuration configuration; // 用來記錄嵌套查詢的層數 protected int queryStack; private boolean closed; 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; } // 4 個抽象方法,由子類實現,模板方法中可變部分 protected abstract int doUpdate(MappedStatement ms, Object parameter)throws SQLException; protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException; protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)throws SQLException; protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)throws SQLException; // 執行 insert | update | delete 語句,調用 doUpdate 方法實現,在執行這些語句的時候,會清空緩存 public int update(MappedStatement ms, Object parameter) throws SQLException { // .... // 清空緩存 clearLocalCache(); // 執行SQL語句 return doUpdate(ms, parameter); } // 刷新批處理語句,且執行緩存中還沒執行的SQL語句 @Override public List<BatchResult> flushStatements() throws SQLException { return flushStatements(false); } public List<BatchResult> flushStatements(boolean isRollBack) throws SQLException { // ... // doFlushStatements 的 isRollBack 參數表示是否執行緩存中的SQL語句,false表示執行,true表示不執行 return doFlushStatements(isRollBack); } // 查詢存儲過程 @Override public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); return doQueryCursor(ms, parameter, rowBounds, boundSql); } // 事務的提交和回滾 @Override public void commit(boolean required) throws SQLException { // 清空緩存 clearLocalCache(); // 刷新批處理語句,且執行緩存中的QL語句 flushStatements(); if (required) { transaction.commit(); } } @Override public void rollback(boolean required) throws SQLException { if (!closed) { try { // 清空緩存 clearLocalCache(); // 刷新批處理語句,且不執行緩存中的SQL flushStatements(true); } finally { if (required) { transaction.rollback(); } } } }
在上面的代碼邏輯中,執行update類型的語句會清空緩存,且執行結果不須要進行緩存,而在執行查詢語句的時候,須要對數據進行緩存,以下所示:
@Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // 獲取查詢SQL BoundSql boundSql = ms.getBoundSql(parameter); // 建立緩存的key,建立邏輯在 CacheKey中已經分析過了 CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); // 執行查詢 return query(ms, parameter, rowBounds, resultHandler, key, boundSql); } // 執行查詢邏輯 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // .... if (queryStack == 0 && ms.isFlushCacheRequired()) { // 若是不是嵌套查詢,且 <select> 的 flushCache=true 時纔會清空緩存 clearLocalCache(); } List<E> list; try { // 嵌套查詢層數加1 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--; } // ... 處理延遲加載的相關邏輯 return list; } // 從數據庫查詢數據 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 提供的二級緩存是應用級別的緩存,它的生命週期和應用程序的生命週期相同,且與二級緩存相關的配置有如下 3 個:
1. mybatis-config.xml 配置文件中的 cacheEnabled 配置,它是二級緩存的總開關,只有該配置爲 true ,後面的緩存配置纔會生效。默認爲 true,即二級緩存默認是開啓的。
2. Mapper.xml 配置文件中配置的 <cache> 和 <cache-ref>標籤,若是 Mapper.xml 配置文件中配置了這兩個標籤中的任何一個,則表示開啓了二級緩存的功能,在 Mybatis 解析 SQL 源碼分析一 文章中已經分析過,若是配置了 <cache> 標籤,則在解析配置文件的時候,會爲該配置文件指定的 namespace 建立相應的 Cache 對象做爲其二級緩存(默認爲 PerpetualCache 對象),若是配置了 <cache-ref> 節點,則經過 ref 屬性的namespace值引用別的Cache對象做爲其二級緩存。經過 <cache> 和 <cache-ref> 標籤來管理其在namespace中二級緩存功能的開啓和關閉
3. <select> 節點中的 useCache 屬性也能夠開啓二級緩存,該屬性表示查詢的結果是否要存入到二級緩存中,該屬性默認爲 true,也就是說 <select> 標籤默認會把查詢結果放入到二級緩存中。
Mybatis 的二級緩存是用 CachingExecutor 來實現的,它是 Executor 的一個裝飾器類。爲 Executor 對象添加了緩存的功能。
在介紹 CachingExecutor 以前,先來看看 CachingExecutor 依賴的兩個類,TransactionalCacheManager 和 TransactionalCache。
TransactionalCache 實現了 Cache 接口,主要用於保存在某個 SqlSession 的某個事務中須要向某個二級緩存中添加的數據,代碼以下:
public class TransactionalCache implements Cache { // 底層封裝的二級緩存對應的Cache對象 private Cache delegate; // 爲true時,表示當前的 TransactionalCache 不可查詢,且提交事務時會清空緩存 private boolean clearOnCommit; // 存放須要添加到二級緩存中的數據 private Map<Object, Object> entriesToAddOnCommit; // 存放爲命中緩存的 CacheKey 對象 private Set<Object> entriesMissedInCache; public TransactionalCache(Cache delegate) { this.delegate = delegate; this.clearOnCommit = false; this.entriesToAddOnCommit = new HashMap<Object, Object>(); this.entriesMissedInCache = new HashSet<Object>(); } // 添加緩存數據的時候,先暫時放到 entriesToAddOnCommit 集合中,在事務提交的時候,再把數據放入到二級緩存中,避免髒數據 @Override public void putObject(Object key, Object object) { entriesToAddOnCommit.put(key, object); } // 提交事務, public void commit() { if (clearOnCommit) { delegate.clear(); } // 把 entriesToAddOnCommit 集合中的數據放入到二級緩存中 flushPendingEntries(); reset(); } // 把 entriesToAddOnCommit 集合中的數據放入到二級緩存中 private void flushPendingEntries() { for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) { // 放入到二級緩存中 delegate.putObject(entry.getKey(), entry.getValue()); } for (Object entry : entriesMissedInCache) { if (!entriesToAddOnCommit.containsKey(entry)) { delegate.putObject(entry, null); } } } // 事務回滾 public void rollback() { // 把未命中緩存的數據清除掉 unlockMissedEntries(); reset(); } private void unlockMissedEntries() { for (Object entry : entriesMissedInCache) { delegate.removeObject(entry); } }
TransactionalCacheManager 用於管理 CachingExecutor 使用的二級緩存:
public class TransactionalCacheManager { //用來管理 CachingExecutor 使用的二級緩存 // key 爲對應的CachingExecutor 使用的二級緩存 // value 爲對應的 TransactionalCache 對象 private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>(); public void clear(Cache cache) { getTransactionalCache(cache).clear(); } public Object getObject(Cache cache, CacheKey key) { return getTransactionalCache(cache).getObject(key); } public void putObject(Cache cache, CacheKey key, Object value) { getTransactionalCache(cache).putObject(key, value); } public void commit() { for (TransactionalCache txCache : transactionalCaches.values()) { txCache.commit(); } } public void rollback() { for (TransactionalCache txCache : transactionalCaches.values()) { txCache.rollback(); } } // 全部的調用都會調用 TransactionalCache 的方法來實現 private TransactionalCache getTransactionalCache(Cache cache) { TransactionalCache txCache = transactionalCaches.get(cache); if (txCache == null) { txCache = new TransactionalCache(cache); transactionalCaches.put(cache, txCache); } return txCache; } }
接下來看下 二級緩存的實現 CachingExecutor :
public class CachingExecutor implements Executor { // 底層的 Executor private Executor delegate; private TransactionalCacheManager tcm = new TransactionalCacheManager(); // 查詢方法 @Override public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // 獲取 SQL BoundSql boundSql = ms.getBoundSql(parameterObject); // 建立緩存key,在CacheKey中已經分析過建立過程 CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } // 查詢 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // 獲取查詢語句所在namespace對應的二級緩存 Cache cache = ms.getCache(); // 是否開啓了二級緩存 if (cache != null) { // 根據 <select> 的屬性 useCache 的配置,決定是否須要清空二級緩存 flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { // 二級緩存不能保存輸出參數,不然拋異常 ensureNoOutParams(ms, parameterObject, boundSql); // 從二級緩存中查詢對應的值 List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { // 若是二級緩存沒有命中,則調用底層的 Executor 查詢,其中會先查詢一級緩存,一級緩存也未命中,纔會去查詢數據庫 list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // 查詢到的數據放入到二級緩存中去 tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } // 若是沒有開啓二級緩存,則直接調用底層的 Executor 查詢,仍是會先查一級緩存 return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
以上就是 Mybatis 的二級緩存的主要實現過程,CachingExecutor , TransactionalCacheManager 和 TransactionalCache 的關係以下所示,主要是經過 TransactionalCache 來操做二級緩存的。
此外,CachingExecutor 還有其餘的一些方法,主要是調用底層封裝的 Executor 來實現的。
以上就是 Mybatis 的一級緩存和二級緩存的實現過程。
在介紹 Cache 接口的時候,說到,Cache 接口由不少的裝飾器類,共 10 個,添加了不一樣的功能,以下所示:
來看看 SynchronizedCache 裝飾器類吧,在上面的緩存實現中介紹到了 Mybatis 其實就是使用 HashMap 來實現緩存的,即把數據放入到 HashMap中,可是 HashMap 不是線安全的,Mybatis 是如何來保證緩存中的線程安全問題呢?就是使用了 SynchronizedCache 來保證的,它是一個裝飾器類,其中的方法都加上了 synchronized 關鍵字:
public class SynchronizedCache implements Cache { private Cache delegate; public SynchronizedCache(Cache delegate) { this.delegate = delegate; } @Override public synchronized int getSize() { return delegate.getSize(); } @Override public synchronized void putObject(Object key, Object object) { delegate.putObject(key, object); } @Override public synchronized Object getObject(Object key) { return delegate.getObject(key); } @Override public synchronized Object removeObject(Object key) { return delegate.removeObject(key); } // ............ }
接下來看下添加 Cache 裝飾器的方法,在 CacheBuilder.build() 方法中進行添加:
public class CacheBuilder { //........... // 建立緩存 public Cache build() { // 設置緩存的實現類 setDefaultImplementations(); Cache cache = newBaseCacheInstance(implementation, id); setCacheProperties(cache); // 添加裝飾器類 if (PerpetualCache.class.equals(cache.getClass())) { for (Class<? extends Cache> decorator : decorators) { cache = newCacheDecoratorInstance(decorator, cache); setCacheProperties(cache); } // 爲 Cache 添加裝飾器 cache = setStandardDecorators(cache); } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) { cache = new LoggingCache(cache); } return cache; } // 設置 Cache 的默認實現類爲 PerpetualCache private void setDefaultImplementations() { if (implementation == null) { implementation = PerpetualCache.class; if (decorators.isEmpty()) { decorators.add(LruCache.class); } } } // 添加裝飾器 private Cache setStandardDecorators(Cache cache) { try { // 添加 ScheduledCache 裝飾器 if (clearInterval != null) { cache = new ScheduledCache(cache); ((ScheduledCache) cache).setClearInterval(clearInterval); } // 添加SerializedCache裝飾器 if (readWrite) { cache = new SerializedCache(cache); } // 添加 LoggingCache 裝飾器 cache = new LoggingCache(cache); // 添加 SynchronizedCache 裝飾器,保證線程安全 cache = new SynchronizedCache(cache); if (blocking) { // 添加 BlockingCache 裝飾器 cache = new BlockingCache(cache); } return cache; } }
還有其餘的裝飾器,這裏就不一一列出來了。
到這裏 Mybatis 的緩存系統模塊就分析完畢了。