myBatis原始碼解析-二級快取的實現方式
1. 前言
前面近一個月去寫自己的mybatis框架了,對mybatis原始碼分析止步不前,此文繼續前面的文章。開始分析mybatis一,二級快取的實現。
附上自己的專案github地址:https://github.com/xbcrh/simple-ibatis
對mybatis感興趣的同學可關注下,全手寫的一個orm框架,實現了sql的基本功能和物件關係對映。
廢話不說,開始解析mybatis快取原始碼實現。
2. mybatis中快取的實現方式
見mybatis原始碼包 org.apache.ibatis.cache
2.1 mybatis快取實現介面類:cache
public interface Cache { // 獲取快取的ID String getId(); // 放入快取 void putObject(Object key, Object value); // 從快取中獲取 Object getObject(Object key); // 移除快取 Object removeObject(Object key); // 清除快取 void clear(); // 獲取快取大小 int getSize(); // 獲取鎖 ReadWriteLock getReadWriteLock(); }
mybatis自定義了快取介面類,提供了基本的快取增刪改查的操作。在此基礎上,提供了基礎快取實現類PerpetualCache。原始碼如下:
2.2 mybatis快取基本實現類:PerpetualCache
public class PerpetualCache implements Cache { // 快取的ID private String id; // 使用HashMap充當快取(老套路,快取底層實現基本都是map) private Map<Object, Object> cache = new HashMap<Object, Object>(); // 唯一構造方法(即快取必須有ID) public PerpetualCache(String id) { this.id = id; } // 獲取快取的唯一ID public String getId() { return id; } // 獲取快取的大小,實際就是hashmap的大小 public int getSize() { return cache.size(); } // 放入快取,實際就是放入hashmap public void putObject(Object key, Object value) { cache.put(key, value); } // 從快取獲取,實際就是從hashmap中獲取 public Object getObject(Object key) { return cache.get(key); } // 從快取移除 public Object removeObject(Object key) { return cache.remove(key); } // hashmap清除資料方法 public void clear() { cache.clear(); } // 暫時沒有其實現 public ReadWriteLock getReadWriteLock() { return null; } // 快取是否相同 public boolean equals(Object o) { if (getId() == null) throw new CacheException("Cache instances require an ID."); if (this == o) return true; // 快取本身,肯定相同 if (!(o instanceof Cache)) return false; // 沒有實現cache類,直接返回false Cache otherCache = (Cache) o; // 強制轉換為cache return getId().equals(otherCache.getId()); // 直接比較ID是否相等 } // 獲取hashCode public int hashCode() { if (getId() == null) throw new CacheException("Cache instances require an ID."); return getId().hashCode(); } }
PerpetualCache 類其實是對HashMap的封裝,通過對map的put和get等操作實現快取的存取等功能。mybatis中除了基本的快取實現類外還提供了一系列的裝飾類(此處是用到裝飾者模式),此處拿較為重要的裝飾類LruCache進行分析。
2.3 Lru淘汰策略實現分析
Lru是一種快取淘汰策略,其核心思想是”如果資料最近被訪問過,那麼將來被訪問的機率也更高“,LruCache 是基於LinkedHashMap實現,LinkedHashMap繼承自HashMap,來分析下為什麼LinkedHashMap可以當做Lru快取實現。
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
LinkedHashMap繼承HashMap類,實際上就是對HashMap的一個封裝。
// 內部維護了一個自定義的Entry,整合HashMap中的node類 static class Entry<K,V> extends HashMap.Node<K,V> { // linkedHashmap用來連線節點的欄位,根據這兩個欄位可查詢按順序插入的節點 Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } }
構造方法見如下:
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { // 呼叫HashMap的構造方法 super(initialCapacity, loadFactor); // 訪問順序維護,預設false不開啟 this.accessOrder = accessOrder; }
引入兩種圖來理解HashMap與LinkedHashMap
以上是HashMap的結構,採用拉鍊法解決衝突。LinkedHashMap在HashMap基礎上增加了一個雙向連結串列來表示節點插入順序。
如上,節點上多出的紅色和藍色箭頭代表了Entry中的before和after。在put元素時,會自動在尾節點後加上該元素,維持雙向連結串列。瞭解LinkedHashMap結構後,在看看究竟什麼是維護節點的訪問順序。先說結論,當開啟accessOrder後,在對元素進行get操作時,會將該元素放在雙向連結串列的隊尾節點。原始碼如下:
public V get(Object key) { Node<K,V> e; // 呼叫HashMap的getNode方法,獲取元素 if ((e = getNode(hash(key), key)) == null) return null; // 預設為false,如果開啟維護連結串列訪問順序,執行如下方法 if (accessOrder) afterNodeAccess(e); return e.value; } // 方法實現(將e放入尾節點處) void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMap.Entry<K,V> last; // 當節點不是雙向連結串列的尾節點時 if (accessOrder && (last = tail) != e) { LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; // 將待調整的e節點賦值給p p.after = null; if (b == null) // 說明e為頭節點,將老e的下一節點值為頭節點 head = a; else b.after = a;// 否則,e的上一節點直接指向e的下一節點 if (a != null) a.before = b; // e的下一節點的上節點為e的上一節點 else last = b; if (last == null) head = p; else { p.before = last; // last和p互相連線 last.after = p; } tail = p; // 將雙向連結串列的尾節點指向p ++modCount; // 修改次數加以 } }
程式碼很簡單,如上面的圖,我訪問了節點值為3的節點,那木經過get操作後,結構變成如下:
經過如上分析我們知道,如果限制雙向連結串列的長度,每次刪除頭節點的值,就變為一個lru的淘汰策略了。舉個例子,我想限制雙向連結串列的長度為3,依次put 1 2 3,連結串列為 1 -> 2 -> 3,訪問元素2,連結串列變為 1 -> 3-> 2,然後put 4 ,發現連結串列長度超過3了,淘汰1,連結串列變為3 -> 2 ->4;
那木linkedHashMap是怎樣知道自定義的限制策略,看程式碼,因為LinkedHashMap中沒有提供自己的put方法,是直接呼叫的HashMap的put方法,檢視hashMap程式碼如下:
// hashMap final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); // 看這個方法 afterNodeInsertion(evict); return null; } // linkedHashMap重寫了此方法 void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entry<K,V> first; // removeEldestEntry預設返回fasle if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; // 移除雙向連結串列中的頭指標元素 removeNode(hash(key), key, null, false, true); } }
原來只需要重新實現removeEldestEntry就可以自定義實現lru功能了。瞭解基本的lru原理後,開始分析LruCache。
2.4 快取包裝類 - LruCache
public class LruCache implements Cache { // 被裝飾的快取類,即真實的快取類,提供真正的快取能力 private final Cache delegate; // 內部維護的一個linkedHashMap,用來實現LRU功能 private Map<Object, Object> keyMap; // 待淘汰的快取元素 private Object eldestKey; // 唯一構造方法 public LruCache(Cache delegate) { this.delegate = delegate; // 被裝飾的快取類 setSize(1024); // 設定快取大小 } .... }
經分析,LruCache還是個裝飾類。內部除了維護真正的Cache外,還維護了一個LinkedHashMap,用來實現Lru功能,檢視其構造方法。
// 唯一構造方法 public LruCache(Cache delegate) { this.delegate = delegate; // 被裝飾的快取類 setSize(1024); // 設定快取大小 } // setSize()是構造方法中方法 public void setSize(final int size) { // 初始化keyMap keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) { private static final long serialVersionUID = 4267176411845948333L; // 什麼時候自動刪除快取元素,此處是根據當快取數量超過指定的數量,在LinkedHashMap內部刪除元素 protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) { boolean tooBig = size() > size; if (tooBig) { // 將待刪除元素賦值給eldestKey,後續會根據此值是否為空在真實快取中刪除 eldestKey = eldest.getKey(); } return tooBig; } }; }
和上文分析一樣,重寫了removeEldestEntry方法。此方法返回一個boolean值,當快取的大小超過自定義大小,返回true,此時linkedHashMap中會自動刪除eldest元素。在真實快取cache中也將此元素刪除。保持真實cache和linkedHashMap元素一致。其實就是用linkedHashMap的lru特性來保證cache也具有此lru特性。
分析put方法和get方法驗證此結論.。
@Override public Object getObject(Object key) { keyMap.get(key); // 觸發linkedHashMap中get方法,將key對應的元素放入隊尾 return delegate.getObject(key); // 呼叫真實的快取get方法 } // 放入快取時,除了在真實快取中放一份外,還會在LinkedHashMap中放一份 @Override public void putObject(Object key, Object value) { delegate.putObject(key, value); // 呼叫LinkedHashMap的方法 cycleKeyList(key); } private void cycleKeyList(Object key) { // linkedHashMap中put,會觸發removeEldestEntry方法,如果快取大小超過指定大小,則將雙向連結串列對頭值賦值給eldestKey keyMap.put(key, key); // 檢查eldestKey是否為空。不為空,則代表此元素是淘汰的元素了,需要在真實快取中刪除。 if (eldestKey != null) { // 真實快取中刪除 delegate.removeObject(eldestKey); eldestKey = null; } }
介紹完Cache基本實現後,開始分析mybatis中一級快取
3. mybatis一級快取使用原始碼分析
此處是僅介紹mybatis的實現,沒有涉及到與Spring整合,先介紹mybatis最基本的sql執行語法。預設大家掌握了SqlSessionFactoryBuilder,SqlSessionFactory,SqlSession用法。後面我會寫一篇部落格分析SQL在mybatis中執行的過程,會介紹到這些基礎知識。
InputStream inputStream = Resources.getResourceAsStream("com/xiaobing/resource/mybatisConfig.xml"); // 構建位元組流 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); // 構建SqlSessionFactoryBuilder SqlSessionFactory factory = builder.build(inputStream); // 構建SqlSessionFactory SqlSession sqlSession = factory.openSession(); // 生成SqlSession List<SysUser> userList = sqlSession.selectList("com.xiaobing.mapper.SysUserMapper.getSysUser"); // 執行SysUserMapper類的getSysUser方法
前文構建SqlSession的內容大家感興趣可自行檢視,此處僅分析執行過程。檢視selectList方法,mybatis中sqlSession的預設實現為DefaultSqlSession
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { // 每個mapper檔案會解析生成一個MappedStatement MappedStatement ms = configuration.getMappedStatement(statement); // 呼叫真實的查詢方法,此處是呼叫executor的方法。executor採用了裝飾者模式,若該mapper檔案未啟用二級快取,則預設為BaseExecutor。 // 若該mapper檔案啟用了二級快取,則使用的是CachingExecutor List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); return result; } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
因為此處使用的是裝飾者模式,BaseExecutor是最基礎的執行器,使用了一級快取,CachingExecutor是對BaseExecutor進行一次封裝,若開啟二級快取開關,在使用一級快取前,先使用二級快取。後文介紹二級快取會分析這兩個Executor生成地方。先分析BaseExecutor的一級快取實現。
// BaseExecutor.java /** * 查詢,並建立好CacheKey物件 * @param ms Mapper.xml檔案的select,delete,update,insert這些DML標籤的封裝類 * @param parameter 引數物件 * @param rowBounds Mybatis的分頁物件 * @param resultHandler 結果處理器物件 */ public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); // 獲取boundSql物件 CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); // 生成快取KEY return query(ms, parameter, rowBounds, resultHandler, key, boundSql); // 執行如下方法 } @SuppressWarnings("unchecked") 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."); //如果將flushCacheRequired為true,則會在執行器執行之前就清空本地一級快取 if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } List<E> list; try { queryStack++; // 請求堆疊加一 // 如果此次查詢的resultHandler為null(預設為null),則嘗試從本地快取中獲取已經快取的的結果 list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { //如果查到localCache快取,處理localOutputParameterCache,即對儲存過程的sql進行特殊處理 handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { // 從資料庫中查詢,並將結果放入到localCache list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { // 請求堆疊減一 queryStack--; } if (queryStack == 0) { // 載入延遲載入List for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } deferredLoads.clear(); // issue #601 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { clearLocalCache(); // issue #482 } } 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一級快取很好理解,對於同一個SqlSession物件(即同一個Executor),執行同一條語句時,BaseExecutor會先從自己的快取中查詢,是否存在此條語句的結果,若能找到,則直接返回(暫且忽略儲存過程處理)。若沒有找到,則查詢資料庫,將結果放入此快取,供下次使用。mybatis預設開啟一級快取。
4. mybatis二級快取使用原始碼分析
4.1 配置方式
在全域性配置檔案中mybatis-config.xml中加入如下設定
<settings> <setting name="cacheEnabled" value="true"/> </settings>
在具體mapper.xml中配置<cache/>標籤或者<cache-ref/>標籤
<cache></cache>或者<cache-ref/>
或者採用註解配置方式,在mapper.java檔案上配置註解
@CacheNamespace 或者 @CacheNamespaceRef
4.1 mybatis解析二級快取標籤
還是採用上面sqlSession方式程式碼來debug
InputStream inputStream = Resources.getResourceAsStream("com/xiaobing/resource/mybatisConfig.xml"); // 構建位元組流 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); // 構建SqlSessionFactoryBuilder SqlSessionFactory factory = builder.build(inputStream); // 構建SqlSessionFactory
進入檢視builder.build()方法
// SqlSessionFactoryBuilder.java /**根據流構建SqlSessionFactory*/ public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { /**構建XML檔案解析器*/ XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); /**開始解析mybatis-config.xml檔案並構建全域性變數Configuration*/ return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } }
進入parser.parse()方法,,進一步分析
public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration; } private void parseConfiguration(XNode root) { try { propertiesElement(root.evalNode("properties")); //issue #117 read properties first // 讀取properties配置 typeAliasesElement(root.evalNode("typeAliases")); // 讀取別名設定 pluginElement(root.evalNode("plugins")); // 讀取外掛設定 objectFactoryElement(root.evalNode("objectFactory")); // 讀取物件工廠設定 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); // 讀取物件包裝工廠設定 settingsElement(root.evalNode("settings")); // 讀取setting設定 environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631 // 讀取環境設定 databaseIdProviderElement(root.evalNode("databaseIdProvider")); // 讀取資料庫ID提供資訊 typeHandlerElement(root.evalNode("typeHandlers")); // 讀取型別轉換處理器 mapperElement(root.evalNode("mappers")); // 解析mapper檔案 } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
此處僅分析<cache/> 和 <cache-ref/>標籤的解析,<cache/> 和 <cache-ref/>存在具體的mapper.xml檔案中,分析mapperElement()方法。因為在mybatis-config.xml檔案中關於<mapper>標籤的值可配置package,resource,url,class等配置。如
<mappers> <mapper class="com.xiaobing.mapper.SysUserMapper"/> </mappers>
分析mapperElement()方法
/** * 對映檔案支援四種配置,package,resource,url,class四種 * 如在mybatis-config.xml中配置 * <mappers> <mapper class="com.xiaobing.mapper.SysUserMapper"/> </mappers> * */ private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { // 若配置的是package,在講package下的所有mapper檔案進行解析 String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); if (resource != null && url == null && mapperClass == null) { // 若配置的是resource,在解析resource對應的mapper.xml ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource); // 獲取xml檔案位元組流 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); // 構建xml檔案構造器 mapperParser.parse(); // 解析xml檔案 } else if (resource == null && url != null && mapperClass == null) { // 若配置的是url,在解析url對應的mapper.xml ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url == null && mapperClass != null) { // 若配置的是class,在解析class對應的mapper檔案 Class<?> mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); // 分析addMapper()方法 } else { throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } } }
因為我採用的是class配置,所以分析configuration.addMapper()方法
// Configuration.java public <T> void addMapper(Class<T> type) { mapperRegistry.addMapper(type); }
繼續進入mapperRegistry.addMapper進行分析
// MapperRegistry.java public <T> void addMapper(Class<T> type) { if (type.isInterface()) { // mapper介面 if (hasMapper(type)) { // 若mapper已被註冊 throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { knownMappers.put(type, new MapperProxyFactory<T>(type)); // 註冊對映介面 // It's important that the type is added before the parser is run // otherwise the binding may automatically be attempted by the // mapper parser. If the type is already known, it won't try. MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); // 生成註解構造器 parser.parse(); // 解析mapper上的註解 loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }
knownMappers.put(type, new MapperProxyFactory<T>(type));這裡很重要,是註冊mapper檔案代理物件。此處只做快取的解釋,不做註冊詳解,後面在分析sql執行流程時單獨去分析。
parser.parse()是對mapper檔案進行解析的關鍵,繼續分析
// MapperAnnotationBuilder.java // 解析配置檔案 public void parse() { String resource = type.toString(); // 介面的全限定名 class com.test.userMapper if (!configuration.isResourceLoaded(resource)) { // 是否載入過 loadXmlResource(); // 在預設路徑下(預設和mapper介面同個包下),載入xml檔案 configuration.addLoadedResource(resource); // 設為該mapper配置檔案已解析 assistant.setCurrentNamespace(type.getName()); // 設定構建助力器當前名稱空間 com.test.userMapper parseCache(); // 解析CacheNamespace註解,構建一個Cache物件,並儲存到Mybatis全域性配置資訊中 parseCacheRef(); //解析CacheNamespace註解,引用CacheRef對應的Cache物件。 // 由此可知,當引入了<cache/>和<cacheRef/>後,該名稱空間的快取物件變為了CacheRef引用的快取物件 Method[] methods = type.getMethods(); // 獲取方法 for (Method method : methods) { try { if (!method.isBridge()) { // issue #237 若該方法不是橋接方法 parseStatement(method); //構建MapperStatement物件,並新增到Mybatis全域性配置資訊中 } } catch (IncompleteElementException e) { //當出現未完成元素時,新增構建Method時丟擲異常的MethodResolver例項,到下個Mapper的解析時再次嘗試解析 configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } parsePendingMethods(); // 解析未完成解析的Method }
通過上面的程式碼註釋,可知,當解析mapper.java檔案前,會先在同個資料夾下檢視是否存在mapper.xml檔案,若存在,則先解析mapper.xml檔案。在解析mapper.xml檔案時,若在mapper.xml中寫了快取<cache/>或<cache-ref>,也會生成二級快取。若同時還在mapper.java檔案裡寫了@CacheNamespace註解。則會進行報錯,因為出現了兩個快取。此時我們根據註解配置去分析。去分析parseCache()和parseCacheRef(),看配置了註解@CacheNamespace和CacheNamespaceRef之後快取具體怎樣生成。
// MapperAnnotationBuilder.java private void parseCache() { // 獲取是否有@CacheNamespace 註解 CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class); if (cacheDomain != null) { /* * 構建一個快取物件,具體分析 * */ assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), cacheDomain.flushInterval(), cacheDomain.size(), cacheDomain.readWrite(), null); } }
// mapperBuilderAssistant.java public Cache useNewCache(Class<? extends Cache> typeClass, // 基本快取類 Class<? extends Cache> evictionClass, // 快取裝飾類 Long flushInterval, // 快取重新整理間隔 Integer size, // 快取大小 boolean readWrite, // 快取可讀寫 Properties props) { typeClass = valueOrDefault(typeClass, PerpetualCache.class); // 沒有設定則採用預設的PerpetualCache evictionClass = valueOrDefault(evictionClass, LruCache.class); // 沒有設定則採用預設的LruCache Cache cache = new CacheBuilder(currentNamespace) // 名稱空間作為快取唯一ID .implementation(typeClass) .addDecorator(evictionClass) .clearInterval(flushInterval) .size(size) .readWrite(readWrite) .properties(props) .build(); configuration.addCache(cache); // 加入到全域性快取 currentCache = cache; // 當前快取設為cache,由此可知,快取是mapper級別 return cache; }
此處是生成了二級快取的地方,並設定當前mapper檔案的快取為這個生成的二級快取。若沒有配置@CacheNamespaceRef,那木此mapper檔案就使用了這個自己生成的二級快取。那@CacheNamespaceRef是用來幹嘛的?回到上面程式碼處進行分析。
// MapperAnnotationBuilder.java private void parseCacheRef() { // @CacheNamespaceRef 相當於<cacheRef/>標籤 CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class); if (cacheDomainRef != null) { assistant.useCacheRef(cacheDomainRef.value().getName()); // 構建快取引用,進入分析 } }
public Cache useCacheRef(String namespace) { if (namespace == null) { throw new BuilderException("cache-ref element requires a namespace attribute."); } try { unresolvedCacheRef = true; Cache cache = configuration.getCache(namespace); // 獲取被引用的快取 if (cache == null) { //被引用的快取是否存在 throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found."); } currentCache = cache; // 設定當前快取物件為被引用的快取物件 unresolvedCacheRef = false; // 標誌設定為false,代表有快取引用。 return cache; } catch (IllegalArgumentException e) { throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e); } }
由上文可知,當配置了@CacheNamespaceRef和@CacheNamespace後,該mapper檔案對應的快取以@CacheNamespaceRef引用的快取為準。這樣可是使得不同的mapper檔案有相同的快取。
4.2 快取具體使用場景
上文說了,開啟二級快取後,sqlSession中的Executor是CachingExecutor,檢視生成CachingExecutor具體位置。繼續從那段測試程式碼分析
SqlSession sqlSession = factory.openSession(); // 生成SqlSession List<SysUser> userList = sqlSession.selectList("com.xiaobing.mapper.SysUserMapper.getSysUser"); // 執行SysUserMapper類的getSysUser方法
debug進入DefaultSqlSessionfactory.openSession()方法
// DefaultSqlSessionfactory.java public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); } ... private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { final Environment environment = configuration.getEnvironment(); // 獲取當前配置設定的環境,有事務工廠,資料來源 final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); // 建立事務工廠 tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // 事務類 final Executor executor = configuration.newExecutor(tx, execType); // 生成執行器 return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } ....
分析Executor executor = configuration.newExecutor(tx, execType);此段程式碼
// Configuration.java public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; // 預設為SimpleExecutor executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; ....... if (cacheEnabled) { // 若開啟二級快取,則生成CachingExecutor executor = new CachingExecutor(executor); } ....... }
當執行查詢語句時,會執行Executor的query()方法。分析CachingExecutor中query()方法究竟是怎樣使用二級快取。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // mapper.xml設定了<cache>或者mapper.java使用了二級快取註解 Cache cache = ms.getCache(); if (cache != null) { // 若該mapper檔案中執行的上一條語句是更新語句(增刪改),則會清空該mapper檔案對應的二級快取 flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, parameterObject, 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); // 呼叫後續的Executor執行語句,後續的Executor會繼續使用一級快取。 tcm.putObject(cache, key, list); // issue #578. Query must be not synchronized to prevent deadlocks // 放入二級快取中 } return list; } } return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // 若沒開啟二級快取,則呼叫後續的Executor執行語句。後續的Executor會繼續使用一級快取。 } // 此處的update包括增刪改 public int update(MappedStatement ms, Object parameterObject) throws SQLException { // 清空二級快取 flushCacheIfRequired(ms); return delegate.update(ms, parameterObject); }
通過上面分析可知,二級快取的實現是mapper級別的。只要對這個mapper檔案使用@CacheNamespace註解或對應的xml使用<cache/>等標籤,那木該mapper在生成時就會註冊一個mapper級別的快取。在後續
對這一mapper檔案任何查詢語句程序操作的時候,都會使用到這個二級快取。二級快取就相當於在一級快取上在加入一個快取。二級快取Cache的實現是在LruCache上在封裝了一層TransactionCache,為了防止髒資料的產生。感興趣的可以自行去檢視。以上便是關於mybatis快取的內容。
4. 總結驗證
我們知道,二級快取是mapper級別的,在mybatis初始化時便生成了。當此mapper檔案中有更新語句時,才會重新整理二級快取。舉個例子,有MapperA.java和MapperB.java兩個檔案,並都開啟了二級快取,cacheA和cacheB。MapperA.java中有一條查詢語句select1,此查詢語句關聯了B的表。在第一次執行MapperA.java中select1時,會從庫中取出資料,並放入在cacheA中。當mapperB.java中如果有一條更新語句update2,執行update2,會重新整理二級快取cacheB。但不會重新整理cacheA,因為update2並不在MapperA.java中。那此時cacheA中存在的資料便是髒資料了。
其實也有解決辦法,即在MapperA.java中使用@CacheNamespaceRef = "mapperB.java".讓兩個檔案公用同一個二級快取。這樣就OK啦
若對mybatis感興趣的小夥伴,請移步我github專案,從零手寫了一個ORM框架,希望你的star和交流:https://github.com/xbcrh/simple-ibatis&n