1. 驗證二級緩存
在上一篇帖子中的 User 和 Department 實體類依然要用,這里就不再贅述了,要啟用二級緩存,需要在 Mapper.xml 文件中指定 cache 標簽,如下:
UserMapper.xml select * from user /select?>
Department.xml select * from department; /select?>
在 Department.xml 中的 cache 標簽指定了 readOnly 屬性,因為該配置相對比較重要,所以我們在這里把它講解一下:
readOnly 默認為 false,這種情況下通過二級緩存查詢出來的數據會進行一次 序列化深拷貝。在這里大家需要回想一下介紹一級緩存時舉的例子:一級緩存查詢出來返回的是 該對象的引用,若我們對它修改,再查詢 時觸發一級緩存獲得的便是 被修改過的數據。但是,二級緩存的序列化機制則不同,它獲取到的是 緩存深拷貝的對象,這樣對二級緩存進行修改操作不影響后續查詢結果。
如果將該屬性配置為 true 的話,那么它就會變得和一級緩存一樣,返回的是對象的引用,這樣做的好處是 避免了深拷貝的開銷。
為什么會有這種機制呢?
因為二級緩存是 Mapper級別 的,不能保證其他 SqlSession 不對二級緩存進行修改,所以這也是一種保護機制。
我們驗證一下這個例子,Department 和 User 的查詢都執行了兩遍(注意 事務提交之后 才能使二級緩存生效):
public static void main(String[] args) { InputStream xml = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); // 開啟二級緩存需要在同一個SqlSessionFactory下,二級緩存存在于 SqlSessionFactory 生命周期,如此才能命中二級緩存 SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(xml); SqlSession sqlSession1 = sqlSessionFactory.openSession(); UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class); DepartmentMapper departmentMapper1 = sqlSession1.getMapper(DepartmentMapper.class); System.out.println("----------department第一次查詢 ↓------------"); List departments1 = departmentMapper1.findAll(); System.out.println("----------user第一次查詢 ↓------------"); List users1 = userMapper1.findAll(); // 提交事務,使二級緩存生效 sqlSession1.commit(); SqlSession sqlSession2 = sqlSessionFactory.openSession(); UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class); DepartmentMapper departmentMapper2 = sqlSession2.getMapper(DepartmentMapper.class); System.out.println("----------department第二次查詢 ↓------------"); List departments2 = departmentMapper2.findAll(); System.out.println("----------user第二次查詢 ↓------------"); List users2 = userMapper2.findAll(); sqlSession1.close(); sqlSession2.close(); }
Department 和 User 的同一條查詢語句都執行了兩遍,因為 Department 指定了 readOnly 為true,那么 兩次查詢返回的對象均為同一個引用,而 User 則反之,Debug 試一下:
cache 的其他屬性
屬性 | 描述 | 備注 |
---|---|---|
eviction | 緩存回收策略 | 默認 LRU |
type | 二級緩存的實現類 | 默認實現 PerpetualCache |
size | 緩存引用數量 | 默認1024 |
flushInterval | 定時清除時間間隔 | 默認無 |
blocking | 阻塞獲取緩存數據 | 若緩存中找不到對應的 key ,是否會一直阻塞,直到有對應的數據進入緩存。默認 false |
接下來我們測試驗證下二級緩存的生效:
SqlSession sqlSession1 = sqlSessionFactory.openSession(); DepartmentMapper departmentMapper1 = sqlSession1.getMapper(DepartmentMapper.class); System.out.println("----------department第一次查詢 ↓------------"); List departments1 = departmentMapper1.findAll(); // 使二級緩存生效 sqlSession1.commit(); SqlSession sqlSession2 = sqlSessionFactory.openSession(); DepartmentMapper departmentMapper2 = sqlSession2.getMapper(DepartmentMapper.class); System.out.println("----------department第二次查詢 ↓------------"); List departments2 = departmentMapper2.findAll();
第一次 Query,會去數據庫中查
第二次 Query,直接從二級緩存中取
2. 二級緩存的原理
二級緩存對象 Cache
在加載 Mapper 文件(org.apache.ibatis.builder.xml.XMLConfigBuilder#mappersElement 方法)時,定義了加載 cache 標簽的步驟(org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement 方法),代碼如下:
public class XMLMapperBuilder extends BaseBuilder { // ... private void configurationElement(XNode context) { try { // 若想要在多個命名空間中共享相同的緩存配置和實例,可以使用 cache-ref 元素來引用另一個緩存 cacheRefElement(context.evalNode("cache-ref")); // 配置二級緩存 cacheElement(context.evalNode("cache")); // ... } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); } } }
具體解析邏輯如下:
public class XMLMapperBuilder extends BaseBuilder { // ... private void cacheElement(XNode context) { if (context != null) { // 二級緩存實現類,默認 PerpetualCache,我們在一級緩存也提到過 String type = context.getStringAttribute("type", "PERPETUAL"); Class? extends Cache?> typeClass = typeAliasRegistry.resolveAlias(type); // 緩存清除策略,默認 LRU String eviction = context.getStringAttribute("eviction", "LRU"); Class? extends Cache?> evictionClass = typeAliasRegistry.resolveAlias(eviction); // 定時清除間隔 Long flushInterval = context.getLongAttribute("flushInterval"); // 緩存引用數量 Integer size = context.getIntAttribute("size"); // readOnly上文我們提到過,默認 false boolean readWrite = !context.getBooleanAttribute("readOnly", false); // blocking 默認 false boolean blocking = context.getBooleanAttribute("blocking", false); Properties props = context.getChildrenAsProperties(); // 創建緩存對象 builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); } } }
我們繼續看創建二級緩存對象的邏輯 org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache,可以發現,創建 Cache 對象使用了 建造者模式:
建造者 CacheBuilder 并沒有被組合在任意一種緩存的實現類中,而是根據如下代碼中 implementation(valueOrDefault(typeClass, PerpetualCache.class)) 邏輯指定了要創建的緩存類型,并在 build 方法中使用反射創建對應實現類:
public class MapperBuilderAssistant extends BaseBuilder { // ... public Cache useNewCache(Class? extends Cache?> typeClass, Class? extends Cache?> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) { // 建造者模式,將標簽屬性賦值 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(); // 添加到全局配置中 configuration.addCache(cache); currentCache = cache; return cache; } }
其中 addDecorator(valueOrDefault(evictionClass, LruCache.class)) 邏輯添加了 裝飾器,使用了 裝飾器模式,將 LruCache 類型的裝飾器添加到 decorators 中:
public class CacheBuilder { private final List> decorators; public CacheBuilder addDecorator(Class? extends Cache?> decorator) { // 將 LruCache 裝飾器添加到 decorators if (decorator != null) { this.decorators.add(decorator); } return this; } // ... }
在 CacheBuilder#build 方法中,如下為封裝裝飾器的邏輯:
public class CacheBuilder { // ... public Cache build() { setDefaultImplementations(); // 反射創建 PerpetualCache 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 = setStandardDecorators(cache); } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) { cache = new LoggingCache(cache); } return cache; } private Cache setStandardDecorators(Cache cache) { try { MetaObject metaCache = SystemMetaObject.forObject(cache); if (size != null && metaCache.hasSetter("size")) { metaCache.setValue("size", size); } // 定時清空二級緩存 if (clearInterval != null) { cache = new ScheduledCache(cache); ((ScheduledCache) cache).setClearInterval(clearInterval); } // readOnly屬性相關的讀寫緩存 if (readWrite) { cache = new SerializedCache(cache); } // 日志緩存和同步緩存(借助 ReentrantLock 實現) cache = new LoggingCache(cache); cache = new SynchronizedCache(cache); // 阻塞屬性的緩存 if (blocking) { cache = new BlockingCache(cache); } return cache; } catch (Exception e) { throw new CacheException("Error building standard cache decorators. Cause: " + e, e); } } }
所有裝飾器都在 org.apache.ibatis.cache.decorators 包下,唯獨 PerpetualCache 在org.apache.ibatis.cache.impl 包下:
PerpetualCache 中不包含 delegate 屬性表示裝飾器,說明它將作為最基礎的實現類被其他裝飾器裝飾,而其他裝飾器中均含有 delegate 屬性來裝飾其他實現。
默認創建的二級緩存類型如下:
類關系圖如下:
query 方法對二級緩存的應用
org.apache.ibatis.executor.CachingExecutor#query 方法使用了二級緩存,如下代碼所示:
public class CachingExecutor implements Executor { // 事務緩存管理器 private final TransactionalCacheManager tcm = new TransactionalCacheManager(); @Override public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // 先獲取二級緩存,該對象便是上文中創建的被裝飾器裝飾的 PerpetualCache Cache cache = ms.getCache(); if (cache != null) { // 判斷是否需要清除緩存 flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); // 從二級緩存中取 @SuppressWarnings("unchecked") List list = (List) tcm.getObject(cache, key); if (list == null) { // 沒取到二級緩存,嘗試取一級緩存或去數據庫查詢 list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // “添加二級緩存” tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } // 沒有二級緩存的話,執行的是我們在一級緩存中介紹的方法,要么取一級緩存,否則去數據庫查 return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } // ... }
上述邏輯比較清晰,我們在上文中提到過,只有 事務提交的時候才會將二級緩存保存,但是其中有 tcm.putObject(cache, key, list); 邏輯,似乎在這里保存了二級緩存,而此時事務還未提交,這便需要我們一探究竟。它會執行到 TransactionalCacheManager#putObject 方法:
public class TransactionalCacheManager { private final Map transactionalCaches = new HashMap?>(); public void putObject(Cache cache, CacheKey key, Object value) { getTransactionalCache(cache).putObject(key, value); } private TransactionalCache getTransactionalCache(Cache cache) { return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new); } }
TransactionalCacheManager 事務緩存管理器會創建并管理 TransactionalCache 對象,TransactionalCache 同樣是 Cache 裝飾器,它將裝飾在 SynchronizedCache 上:
public class TransactionalCache implements Cache { // 被裝飾對象,默認是 SynchronizedCache private final Cache delegate; // 該元素將保存在事務 commit 時被保存的鍵值對緩存 private final Map entriesToAddOnCommit; @Override public void putObject(Object key, Object object) { entriesToAddOnCommit.put(key, object); } // ... }
putObject 執行時便是向 entriesToAddOnCommit 添加元素,記錄二級緩存鍵值對,并沒有真正添加到二級緩存 PerpetualCache 對象中。此外,entriesToAddOnCommit 的命名,也暗示了在事務提交時緩存才會被保存。那么接下來,便需要看一下事務提交邏輯。
在上文測試二級緩存的代碼中,有 sqlSession1.commit(); 邏輯。在事務提交時,它會走到 CachingExecutor#commit 方法,其中會調用到 TransactionalCacheManager#commit 方法,如下:
public class CachingExecutor implements Executor { // ... private final TransactionalCacheManager tcm = new TransactionalCacheManager(); @Override public void commit(boolean required) throws SQLException { // ... tcm.commit(); } }
在該方法中,會遍歷所有的事務緩存 TransactionalCache,并逐一調用它們的 commit 方法,
public class TransactionalCacheManager { private final Map transactionalCaches = new HashMap?>(); public void commit() { for (TransactionalCache txCache : transactionalCaches.values()) { txCache.commit(); } } // ...
commit 方法會調用 delegate.commit 方法,而 delegate 為被裝飾對象,最后便會將二級緩存記錄:
public class TransactionalCache implements Cache { private final Map entriesToAddOnCommit; public void commit() { if (clearOnCommit) { delegate.clear(); } flushPendingEntries(); reset(); } private void flushPendingEntries() { // 事務提交,將 entriesToAddOnCommit 中所有待添加的二級緩存添加 for (Map.Entry entry : entriesToAddOnCommit.entrySet()) { delegate.putObject(entry.getKey(), entry.getValue()); } for (Object entry : entriesMissedInCache) { if (!entriesToAddOnCommit.containsKey(entry)) { delegate.putObject(entry, null); } } } private void reset() { clearOnCommit = false; entriesToAddOnCommit.clear(); entriesMissedInCache.clear(); } // ... }
緩存失效
事務回滾是不是會使本次事務中相關的二級緩存失效呢?
public class TransactionalCache implements Cache { public void rollback() { unlockMissedEntries(); reset(); } private void reset() { clearOnCommit = false; entriesToAddOnCommit.clear(); entriesMissedInCache.clear(); } private void unlockMissedEntries() { for (Object entry : entriesMissedInCache) { try { delegate.removeObject(entry); } catch (Exception e) { log.warn("Unexpected exception while notifying a rollback to the cache adapter. " + "Consider upgrading your cache adapter to the latest version. Cause: " + e); } } } // ... }
的確如此,它會將未被緩存的元素清除 reset(),也會把在本次事務中操作過的數據在二級緩存中移除 unlockMissedEntries()。
那數據發生新增、修改或刪除呢?同樣會清除緩存
public class CachingExecutor implements Executor { @Override public int update(MappedStatement ms, Object parameterObject) throws SQLException { flushCacheIfRequired(ms); return delegate.update(ms, parameterObject); } private void flushCacheIfRequired(MappedStatement ms) { Cache cache = ms.getCache(); // 默認 flushCacheRequired 為 true if (cache != null && ms.isFlushCacheRequired()) { tcm.clear(cache); } }
它將調用 TransactionalCache#clear 方法,將待生效的 entriesToAddOnCommit 二級緩存清除,并標記 clearOnCommit 為 true,在事務提交時,二級緩存會執行清除緩存的 clear 方法:
@Override public void clear() { clearOnCommit = true; entriesToAddOnCommit.clear(); } public void commit() { if (clearOnCommit) { delegate.clear(); } flushPendingEntries(); reset(); }
緩存生效范圍
到這里,我們已經基本弄清楚二級緩存生效的原理了,那么接下來我們需要解釋“為什么二級緩存是 Mapper 級別的?”其實也非常簡單,看如下代碼:
public class CachingExecutor implements Executor { @Override public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // 先獲取二級緩存,該對象便是上文中創建的被裝飾器裝飾的 PerpetualCache Cache cache = ms.getCache(); // ... } // ... }
在執行查詢時,二級緩存 Cache 是在 MappedStatement 中獲取的,Mapper 中每個 SQL 聲明都對應唯一的 MappedStatement,當同一條 SQL 被執行時,它們都會去取同樣的緩存,所以可以說它是 Mapper 級別的,說成 MappedStatement 級別更準確,二級緩存支持多個 SqlSession 共享。
為什么要在事務提交后才生效?
在這里我們討論一個問題:為什么二級要在事務提交后才能生效呢?
因為二級緩存可以在不同的 SqlSession 間生效,畫個圖你就明白了:
如果 SqlSession1先修改了數據,再查詢數據,如果二級緩存在事務未提交時就生效,那么 SqlSession2 調用同樣的查詢時便會從 二級緩存中獲取數據,但是此時 SqlSession1回滾了事務,那么此時就會導致 SqlSession2 從二級緩存獲取的數據 變成臟數據,這就是為什么二級緩存要在事務提交后才能生效的原因。
3. 為什么要擴展二級緩存?
MyBatis 中設計一級緩存和二級緩存的目的是為了提高數據庫訪問的效率,但它們的作用范圍和使用場景有所不同,各自有其特定的用途和優勢。
一級緩存 默認開啟,是基于 SqlSession 的,也就是說,它的作用范圍僅限于一次數據庫會話,所以當會話關閉后,緩存就會被清除。這意味著不同會話之間無法共享緩存數據。而 二級緩存 是基于 Mapper 級別的,需要顯式配置開啟,可以在多個 SqlSession 之間共享。當然也由于二級緩存的作用范圍更廣,因此需要更復雜的緩存失效策略和數據一致性管理,以避免數據不一致的問題。二級緩存的引入是為了在更大范圍內(多個會話之間)提高數據訪問的效率,特別是在讀多寫少的應用場景。
4. 總結
二級緩存本質上是 HashMap,在 PerpetualCache 實現類中
二級緩存是 Mapper 級別的,可以在不同 SqlSession 間共享
特殊的 readOnly 標簽,默認為 false,表示二級緩存中是被深拷貝的對象
二級緩存需要在事務提交后才能生效
執行 Insert、Delete、Update 語句會使 當前 Mapper 下的二級緩存失效
審核編輯 黃宇
-
源碼
+關注
關注
8文章
669瀏覽量
30239 -
mybatis
+關注
關注
0文章
64瀏覽量
6891
發布評論請先 登錄
蔡司培訓|提升技能必看——AUKOM 一級/二級課程培訓

如何一眼定位SQL的代碼來源:一款SQL染色標記的簡易MyBatis插件

由 Mybatis 源碼暢談軟件設計(九):“能用就行” 其實遠遠不夠
MediaTek發布旗艦級天璣8400處理器
SSM框架的源碼解析與理解
CDCE62005第二級PLL無法鎖住的原因?如何解決?
二級配電箱的作用介紹
物聯網系統中如何增強GNSS的信號_GNSS二級放大電路研發測試方案

INA128兩級放大,第二級放大測不出來的原因?如何解決?
THS3001級聯組成放大電路,實際接通后第二級有明顯發熱,為什么?
一級浪涌保護器和二級浪涌保護器怎么區分
二級浪涌保護器型號如何選擇
使用mybatis切片實現數據權限控制

評論