1. 程式人生 > >mybatis原始碼學習:一級快取和二級快取分析

mybatis原始碼學習:一級快取和二級快取分析

[toc] 前文傳送門:[mybatis原始碼學習:從SqlSessionFactory到代理物件的生成](https://www.cnblogs.com/summerday152/p/12773121.html) # 零、一級快取和二級快取的流程 > 以這裡的查詢語句為例。 ## 一級快取總結 - 以下兩種情況會直接在一級快取中查詢資料 - 主配置檔案或對映檔案沒有配置二級快取開啟。 - 二級快取中不存在資料。 - 根據statetment,生成一個CacheKey。 - 判斷是否需要清空本地快取。 - 根據cachekey從localCache中獲取資料。 - 如果快取未命中,走接下來三步並向下 - 從資料庫查詢結果。 - 將cachekey:資料存入localcache中。 - 將資料返回。 - 如果快取命中,直接從快取中獲取資料。 - localCache的範圍如果為statement,清空一級快取。 ## 二級快取總結 - 判斷主配置檔案是否設定了enabledCache,預設是開啟的,建立CachingExecutor。 - 根據statetment,生成一個CacheKey。 - 判斷對映檔案中是否有cache標籤,如果沒有則跳過以下針對二級快取的操作,從一級快取中查,查不到就從資料庫中查。 - 否則即開啟了二級快取,獲取cache。 - 判斷是否需要清空二級快取。 - 判斷該語句是否需要使用二級快取isUserCache。 - 如果二級快取命中,則直接返回該資料。 - 如果二級快取未命中,則將cachekey存入未命中set,然後進行一下的操作: - 從一級快取中查,如果命中就返回,沒有命中就從資料庫中查。 - 將查到的資料返回,並將cachekey和資料(物件的拷貝)存入待加入二級快取的map中。 - 最後commit和close操作都會使二級快取真正地更新。 # 一、快取介面Cache及其實現類 快取類的頂級介面Cache,裡面定義了加入資料到快取,從快取中獲取資料,清楚快取等操作,通常mybatis會將namespace作為id,將CacheKey作為Map中的鍵,而map中的值也就是儲存在快取中的物件。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200419175757601.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NreV9RaWFvQmFfU3Vt,size_16,color_FFFFFF,t_70) 而通過裝飾器設計模式,將Cache的功能進行加強,在它的實現類中有著明顯的體現: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200419175811663.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NreV9RaWFvQmFfU3Vt,size_16,color_FFFFFF,t_70) PerpetualCache:是最基礎的快取類,採用HashMap實現,同時一級快取使用的localCache就是該型別。 LruCache:Lru(least recently used),採用Lru演算法可以實現移除最長時間沒有使用的key/value。 SerializedCache:提供了序列化功能,將值序列化後存入快取,用於快取返回一份例項的Copy,保證執行緒安全。 LoggingCache:提供日誌功能,如果開啟debugEnabled為true,則列印快取命中日誌。 SynchronizedCache:同步的Cache,用synchronized關鍵字修飾所有方法。 > 下圖可以得知其執行鏈:SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200419175830150.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NreV9RaWFvQmFfU3Vt,size_16,color_FFFFFF,t_70) # 二、cache標籤解析原始碼 XMLMapperBuilder中的configurationElement負責解析mappers對映檔案中的標籤元素,其中有個cacheElement方法,負責解析cache標籤。 ```java private void cacheElement(XNode context) throws Exception { if (context != null) { //獲取type屬性,預設為perpetual String type = context.getStringAttribute("type", "PERPETUAL"); //獲取type類物件 Class typeClass = typeAliasRegistry.resolveAlias(type); //獲取eviction策略,預設為lru,即最近最少使用,移除最長時間不被使用的物件 String eviction = context.getStringAttribute("eviction", "LRU"); Class evictionClass = typeAliasRegistry.resolveAlias(eviction); //獲取flushInterval重新整理間隔 Long flushInterval = context.getLongAttribute("flushInterval"); //獲取size引用數目 Integer size = context.getIntAttribute("size"); //獲取是否只讀 boolean readWrite = !context.getBooleanAttribute("readOnly", false); //獲取是否blocking boolean blocking = context.getBooleanAttribute("blocking", false); //這一步是另外一種設定cache的方式,即cache子元素中用property,name,value定義 Properties props = context.getChildrenAsProperties(); builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); } } ``` getStringAttribute方法,這個方法的作用就是獲取指定的屬性值,如果沒有設定的話,就採用預設的值: ```java public String getStringAttribute(String name, String def) { //獲取name引數對應的屬性 String value = attributes.getProperty(name); if (value == null) { //如果沒有設定,預設為def return def; } else { return value; } } ``` resolveAlias方法,從原始碼中我們就可以猜測,我們之前通過`
`起別名其實也就是將裡面的內容解析,並存入map之中,而每次處理型別的時候,都比較的是小寫的形式,這也是我們起別名之後不用關心大小寫的原因。 ```java // throws class cast exception as well if types cannot be assigned public Class resolveAlias(String string) { try { if (string == null) { return null; } //首先將傳入的引數轉換為小寫形式 String key = string.toLowerCase(Locale.ENGLISH); Class value; //到TypeAliasRegistry維護的Map,TYPE_ALIASES中找有無對應的鍵 if (TYPE_ALIASES.containsKey(key)) { //找到就直接返回:class類物件 value = (Class) TYPE_ALIASES.get(key); } else { //找不到就通過反射獲取一個 value = (Class) Resources.classForName(string); } return value; } catch (ClassNotFoundException e) { throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e); } } ``` 根據獲取的屬性,通過裝飾器模式,層層裝飾,最後建立了一個SynchronizedCache,並新增到configuration中。因此我們可以知道,一旦我們在對映檔案中設定了``,就會建立一個SynchronizedCache快取物件。 ```java public Cache useNewCache(Class typeClass, Class evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) { //把當前的namespace當作快取的id Cache cache = new CacheBuilder(currentNamespace) .implementation(valueOrDefault(typeClass, PerpetualCache.class)) .addDecorator(valueOrDefault(evictionClass, LruCache.class)) .clearInterval(flushInterval) .size(size) .readWrite(readWrite) .blocking(blocking) .properties(props) .build(); //將cache加入configuration configuration.addCache(cache); currentCache = cache; return cache; } ``` # 三、CacheKey快取項的key 預設情況下,enabledCache的全域性設定是開啟的,所以Executor會建立一個CachingExecutor,以查詢為例,當執行Executor實現類的時候,會獲取boundsql,並根據當前資訊建立快取項的key。 ```java @Override public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { //從MappedStatement中獲取boundsql BoundSql boundSql = ms.getBoundSql(parameterObject); //Cachekey類表示快取項的key CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } ``` > 每一個SqlSession中持有了自己的Executor,每一個Executor中有一個Local Cache。當用戶發起查詢時,Mybatis會根據當前執行的MappedStatement生成一個key,去Local Cache中查詢,如果快取命中的話,返回。如果快取沒有命中的話,則寫入Local Cache,最後返回結果給使用者。 boundsql物件的詳細資訊: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200419175903908.png) CacheKey物件的CreateKey操作: - 首先建立一個cachekey,預設hashcode=17,multiplier=37,count=0,updateList初始化。 - update操作:count++,對checksum,hashcode進行賦值,最後將引數新增到updatelist中。 ```java //根據傳入資訊,建立chachekey @Override public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { //執行器關閉就丟擲異常 if (closed) { throw new ExecutorException("Executor was closed."); } //建立一個cachekey,預設hashcode=17,multiplier=37,count=0,updateList初始化 CacheKey cacheKey = new CacheKey(); //新增操作:sql的id,邏輯分頁偏移量,邏輯分頁起始量,sql語句。 cacheKey.update(ms.getId()); cacheKey.update(rowBounds.getOffset()); cacheKey.update(rowBounds.getLimit()); cacheKey.update(boundSql.getSql()); List parameterMappings = boundSql.getParameterMappings(); TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); // mimic DefaultParameterHandler logic for (ParameterMapping parameterMapping : parameterMappings) { if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; //引數名 String propertyName = parameterMapping.getProperty(); //根據引數名獲取值 if (boundSql.hasAdditionalParameter(propertyName)) { value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } //新增引數值 cacheKey.update(value); } } //新增environment的id名,如果它不為空的話 if (configuration.getEnvironment() != null) { // issue #176 cacheKey.update(configuration.getEnvironment().getId()); } //返回cachekey return cacheKey; } ``` 所以快取項的key最後表示為:`hashcode:checknum:遍歷updateList,以:間隔`。 `2020122321:657338105:com.smday.dao.IUserDao.findById:0:2147483647:select * from user where id = ?:41:mysql`。 --- 接著,呼叫同類中的query方法,針對是否開啟二級快取做不同的決斷。(需要注意的是,這一部分是建立在cacheEnabled設定為true的前提下,當然預設是true。如果為false,Executor將會建立BaseExecutor,並不會判斷mappers對映檔案中二級快取是否存在,而是直接執行`delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql)`) ```java //主配置檔案已經開啟二級快取 @Override public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); //對映檔案配置已經開啟二級快取 if (cache != null) { //如果cache不為空,且需要清快取的話(insert|update|delete),執行tcm.clear(cache); flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, parameterObject, boundSql); @SuppressWarnings("unchecked") //從快取中獲取 List list = (List) tcm.getObject(cache, key); if (list == null) { //快取中沒有就執行查詢,BaseExecutor的query list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //存入快取 tcm.putObject(cache, key, list); // issue #578 and #116 } //如果快取中有,就直接返回 return list; } } //對映檔案沒有開啟二級快取,需要進行查詢,delegate其實還是Executor物件 return delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } ``` > 除了select操作之外,其他的的操作都會清空二級快取。XMLStatementBuilder中配置屬性的時候:`boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);` ```java private void flushCacheIfRequired(MappedStatement ms) { Cache cache = ms.getCache(); if (cache != null && ms.isFlushCacheRequired()) { //tcm後面會總結,清空二級快取 tcm.clear(cache); } } ``` # 四、二級快取TransactionCache 這裡學習一下二級快取涉及的快取類:TransactionCache,同樣也是基於裝飾者設計模式,對傳入的Cache進行裝飾,構建二級快取事務緩衝區: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200419175922957.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NreV9RaWFvQmFfU3Vt,size_16,color_FFFFFF,t_70) CachingExecutor維護了一個TransactionCacheManager,即tcm,而這個tcm其實維護的就是一個key為Cache,value為TransactionCache包裝過的Cache。而`tcm.getObject(cache, key)`的意思我們可以通過以下原始碼得知: ```java public Object getObject(Cache cache, CacheKey key) { //將傳入的cache包裝為TransactionalCache,並根據key獲取值 return getTransactionalCache(cache).getObject(key); } ``` 需要注意的是,getObject方法中將會把獲取值的職責一路向後傳遞,直到最基礎的perpetualCache,根據cachekey獲取。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200419175945160.png) 最終獲取到的值,如果為null,就需要把key加入未命中條目的快取。 ```java @Override public Object getObject(Object key) { //根據職責一路向後傳遞 Object object = delegate.getObject(key); if (object == null) { //沒找到值就將key存入未命中的set entriesMissedInCache.add(key); } // issue #146 if (clearOnCommit) { return null; } else { return object; } } ``` 如果快取中沒有找到,將會從資料庫中查詢,查詢到之後,將會進行新增操作,也就是:`tcm.putObject(cache, key, list);`。我們可以發現,其實它並沒有直接將資料加入快取,而是將資料新增進待提交的map中。 ```java @Override public void putObject(Object key, Object object) { entriesToAddOnCommit.put(key, object); } ``` 也就是說,一定需要某種手段才能讓他真正地存入快取,沒錯了,commit是可以的: ```java //CachingExecutor.java @Override public void commit(boolean required) throws SQLException { //清除本地快取 delegate.commit(required); //呼叫tcm.commit tcm.commit(); } ``` 最終呼叫的是TransactionCache的commit方法: ```java public void commit() { if (clearOnCommit) { delegate.clear(); } flushPendingEntries(); reset(); } ``` 最後的最後,我們可以看到將剛才的未命中和待提交的資料都進行了相應的處理,這才是最終影響二級快取中資料的操作,當然這中間也存在著職責鏈,就不贅述了。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200419175959649.png) 當然,除了commit,close也是一樣的,因為最終呼叫的其實都是commit方法,同樣也會操作快取。 # 五、二級快取測試 ```xml
``` ```xml ``` ```java /** * 測試二級快取 */ @Test public void testFirstLevelCache2(){ SqlSession sqlSession1 = factory.openSession(); IUserDao userDao1 = sqlSession1.getMapper(IUserDao.class); User user1 = userDao1.findById(41); System.out.printf("==>
%s\n", user1); sqlSession1.commit(); //sqlSession1.close(); SqlSession sqlSession2 = factory.openSession(); IUserDao userDao2 = sqlSession2.getMapper(IUserDao.class); User user2 = userDao2.findById(41); System.out.printf("==> %s\n", user2); sqlSession2.close(); System.out.println("user1 == user2:"+(user1 == user2)); SqlSession sqlSession3 = factory.openSession(); IUserDao userDao3 = sqlSession3.getMapper(IUserDao.class); User user3 = userDao3.findById(41); System.out.printf("==> %s\n", user3); sqlSession2.close(); System.out.println("user2 == user3:"+(user2 == user3)); } ``` ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200419180009239.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NreV9RaWFvQmFfU3Vt,size_16,color_FFFFFF,t_70) 二級快取實現了SqlSession之間快取資料的共享,是mapper對映級別的快取。 有時快取也會帶來資料讀取正確性的問題,如果資料更新頻繁,會導致從快取中讀取到的資料並不是最新的,可以關閉二級快取。 # 六、一級快取原始碼解析 主配置檔案或對映檔案沒有配置二級快取開啟,或者二級快取中不存在資料,最終都會執行BaseExecutor的query方法,如果queryStack為空或者不是select語句,就會先清空本地的快取。 ```java if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } ``` 檢視本地快取(一級快取)是否有資料,如果有直接返回,如果沒有,則呼叫queryFromDatabase從資料庫中查詢。 ```java list = resultHandler == null ? (List) localCache.getObject(key) : null; if (list != null) { //處理儲存過程 handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { //從資料庫中查詢 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } ``` 判斷本地快取的級別是否為STATEMENT級別,如果是的話,清空快取,因此STATEMENT級別的一級快取無法共享localCache。 ```java if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } ``` # 七、測試一級快取 ```java /** * 測試一級快取 */ @Test public void testFirstLevelCache1(){ SqlSession sqlSession1 = factory.openSession(); IUserDao userDao1 = sqlSession1.getMapper(IUserDao.class); User user1 = userDao1.findById(41); System.out.printf("==> %s\n", user1); IUserDao userDao2 = sqlSession1.getMapper(IUserDao.class); User user2 = userDao2.findById(41); System.out.printf("==> %s\n", user2); sqlSession1.close(); System.out.println("user1 == user2:"+(user1 == user2)); } ``` ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200419180021970.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NreV9RaWFvQmFfU3Vt,size_16,color_FFFFFF,t_70) 一級快取預設是sqlSession級別地快取,insert|delete|update|commit()和close()的操作的執行都會清空一級快取。 怎麼說呢,分析原始碼的過程讓我對Mybatis有了更加深刻的認識,可能有些理解還是沒有很到位,或許是經驗不足,很多東西還是浮於表面,但一翻debug下來,看到自己之前一個又一個的迷惑被非常確切地解開,真的爽! [https://www.jianshu.com/p/c553169c5921](https://www.jianshu.com/p/c5531