1 二級緩存簡介
二級緩存是在多個SqlSession在同一個Mapper文件中共享的緩存,它是Mapper級別的,其作用域是Mapper文件中的namespace,默認是不開啟的。看如下圖:
1.1 整個流程是這樣的(不考慮第三方緩存庫):
當開啟二級緩存后,在配置文件中配置<setting name="cacheEnabled" value="true"/>這行代碼,Mybatis會為SqlSession對象生成Executor對象時,還會生成一個對象:CachingExecutor,我們稱之為裝飾者,這里用到了裝飾器模式。那么CachingExecutor的作用是什么呢?就是當一個查詢請求過來時,CachingExecutor會接到請求,先進行二級緩存的查詢,如果沒命中,就交給真正的Executor(默認是SimpleExecutor,但是會調用它的父類BaseExecutor的query方法,因為要進行一級緩存的查詢)來查詢,再到一級緩存中查詢,如果還沒命中,再到數據庫中查詢。然后把查詢到的結果再返回CachingExecutor,它進行二級緩存,最后再返回給請求方。它是executor的裝飾者,增強executor的功能,具有查詢緩存的作用。當配置<setting name="cacheEnabled" value="false"/>時,請求過來時,BaseExecutor這個抽象類會接到請求,就不進行二級緩存的查詢。
1.2 如何開啟二級緩存,分三步:
一是在配置文件中開啟,這是開啟二級緩存的總開關,默認是開啟狀態的:
<setting name="cacheEnabled" value="true"/>
二是在Mapper文件中開啟緩存,默認是不開啟的,需要手動開啟:
<!-- 每個Mapper文件使用一個緩存對象 -->
<cache/>
<!-- 如果是多個Mapper文件共用一個緩存對象 -->
<cache-ref />
三是針對要查詢的statement使用緩存,即在<select>節點中配置如下屬性:
useCache="true"
對於二級緩存有以下說明:
- 映射語句文件中的所有 select 語句將會被緩存。
- 映射語句文件中的所有 insert,update 和 delete 語句會刷新緩存。
- 緩存會使用 Least Recently Used(LRU,最近最少使用的)算法來收回。
- 根據時間表(比如 no Flush Interval,沒有刷新間隔), 緩存不會以任何時間順序 來刷新。
- 緩存會存儲列表集合或對象(無論查詢方法返回什么)的 1024 個引用。
- 緩存會被視為是 read/write(可讀/可寫)的緩存,意味着對象檢索不是共享的,而 且可以安全地被調用者修改,而不干擾其他調用者或線程所做的潛在修改。
2 二級緩存存儲取出清除過程:
2.1 二級緩存相關類的講解:
在講解二級緩存的存儲取出清除的過程前,先了解下以下幾個類:
2.1.1 TransactionalCache
TransactionalCache和TransactionalCacheManager是CachingExecutor依賴的兩個組件。TransactionalCache實現了Cache接口,作用是保存某個sqlSession的某個事務中需要向某個二級緩存中添加的緩存數據,換句話說就是:某些緩存數據會先保存在這里,然后再提交到二級緩存中。源碼如下:
public class TransactionalCache implements Cache { private Cache delegate; // 底層封裝的二級緩存所對應的Cache對象,用到了裝飾器模式 如下圖1-1 private boolean clearOnCommit; // 該字段為true時,則表示當前TransactionalCache不可查詢,且提交事務時,會將底層的Cache清空 // 暫時記錄添加都TransactionalCahce中的數據,在事務提交時,會將其中的數據添加到二級緩存中
private Map<Object, AddEntry> entriesToAddOnCommit; private Map<Object, RemoveEntry> entriesToRemoveOnCommit; public TransactionalCache(Cache delegate) { this.delegate = delegate; this.clearOnCommit = false; this.entriesToAddOnCommit = new HashMap<Object, AddEntry>(); this.entriesToRemoveOnCommit = new HashMap<Object, RemoveEntry>(); } // 查詢底層的二級緩存 @Override public Object getObject(Object key) { if (clearOnCommit) return null; // issue #146 return delegate.getObject(key); } // 該方法並沒有直接將查詢的結果對象存儲到其封裝的二級緩存Cache對象中,而是暫時保存到entriesToAddOnCommit集合中,在事務提交時才會將這些結果從entriesToAddOnCommit集合中添加到二級緩存中 @Override public void putObject(Object key, Object object) { entriesToRemoveOnCommit.remove(key); entriesToAddOnCommit.put(key, new AddEntry(delegate, key, object)); } @Override public Object removeObject(Object key) { entriesToAddOnCommit.remove(key); entriesToRemoveOnCommit.put(key, new RemoveEntry(delegate, key)); return delegate.getObject(key); } @Override public void clear() { reset(); clearOnCommit = true; } // 事務提交時,先根據clearOnCommit字段的值決定是否清空二級緩存,然后將entriesToAddOnCommit集合中的結果對象保存到二級緩存中 public void commit() {
// 事務提交前,清空二級緩存 if (clearOnCommit) { delegate.clear(); } else { for (RemoveEntry entry : entriesToRemoveOnCommit.values()) { entry.commit(); } }
// 將entriesToAddOnCOmmit集合中的結果對象添加到二級緩存中 for (AddEntry entry : entriesToAddOnCommit.values()) { entry.commit(); } reset(); } private static class AddEntry { private Cache cache; private Object key; private Object value; public AddEntry(Cache cache, Object key, Object value) { this.cache = cache; this.key = key; this.value = value; } // 將entriesToAddOnCOmmit集合中的結果對象添加到二級緩存中,准確的說是PerpetualCache類的HashMap中 public void commit() { cache.putObject(key, value); } } private static class RemoveEntry { private Cache cache; private Object key; public RemoveEntry(Cache cache, Object key) { this.cache = cache; this.key = key; } public void commit() { cache.removeObject(key); } } }
(圖1-1)
2.1.2 TranactionalCacheManager
TransactionalCacheManager是用於管理二級緩存對象Cache和TransactionCache的,它定義有transactionalCaches屬性,看它的源碼部分:
private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
2.2 二級緩存的存儲和取出過程
為了說明二級緩存存儲取出的整個過程,通過下面demo中代碼的執行順序來分析源碼:
@Test public void selectGoodsTest(){
// 分三步進行源碼分析: SqlSession sqlSession = getSqlSessionFactory().openSession(true); GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class); GoodsDao goodsMapper2 = sqlSession.getMapper(GoodsDao.class);
// 第一步:第一次查詢 goodsMapper.selectGoodsById("1");
// 第二步:事務提交 sqlSession.commit(); // 第三步:第二次查詢 goodsMapper2.selectGoodsById("1"); }
2.2.1 第一步:第一次查詢
當配置二級緩存時,CachingExecutor會接到請求,調用它的query方法:先進行二級緩存的查詢,如果沒命中,再由BaseExecutor的query方法查詢。看源碼:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 進行二級緩存的查詢
// 此處的cache就是當mybatis初始化加載mapper映射文件時,如果配置了<cache/>,就會有該cache對象;下面會對MappedStatement這個類進行分析 Cache cache = ms.getCache(); if (cache != null) {
//是否需要刷新緩存,默認情況下,select不需要刷新緩存,insert,delete,update要刷新緩存 flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, parameterObject, boundSql); @SuppressWarnings("unchecked")
// 查詢二級緩存,二級緩存是存放在PerpetualCache類中的HashMap中的,使用到了裝飾器模式 分析此方法 List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) {
// 如果二級緩存沒命中,則調用這個方法:這方法中是先查詢一級緩存,如果還沒命中,則會查詢數據庫 list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 把查詢出的數據放到TransactionCache的entriesToAddOnCommit這個HashMap中,要注意,只是暫時存放到這里,只有當事務提交后,這里的數據才會真正的放到二級緩存中,后面會介紹這個 分析此方法 tcm.putObject(cache, key, list); // issue #578. Query must be not synchronized to prevent deadlocks } return list; } }
// 如果不使用緩存,則調用BaseExecutor的方法 return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
在這個過程中,有兩個方法需要分析:首先是查詢二級緩存的方法tcm.getObject,通過跟蹤源碼最終發現,查詢二級緩存是從PerpetualCache類的HashMap中獲取數據的,也就是說二級緩存真正存放到了這個地方。另外一個處理查詢出的數據tcm.putObject這個方法,這個方法最終是把查詢出的數據存放到了TransactionalCache這個類中的HashMap中,以Cache接口的對象為key,查詢結果集的映射對象為value。到這里,需要明白一點:在執行查詢操作時,查詢二級緩存的地點和存儲查詢數據的地點是不相同的。為什么是這樣呢?這就引出了第二步,sqlSession.commit事務提交這個過程。
2.2.2 第二步:事務提交
現在我們知道,在第一次查詢的時候,會把從數據庫中查詢的數據放到TransactionCache中,但這里並不是二級緩存存放數據的地方,那么二級緩存的數據什么時候怎么來的呢?這就要分析sqlSession.commit()這個方法了,這個方法就是把之前存放在TransactionCache中的數據提交到二級緩存中,然后清空該數據。通過源碼,看下commit方法到底做了哪些事情?進入CachingExecutor的commit方法:
public void commit(boolean required) throws SQLException {
// 清除一級緩存,執行緩存的SQL delegate.commit(required);
// 將存放在TransactionCache中的數據對象提交到PerpetualCache中 進入此方法 tcm.commit(); }
進入TransactionalCacheManager類的commit方法:
public void commit() {
// 把涉及到的TransactionCache都進行處理:提交到二級緩存,並清空數據 for (TransactionalCache txCache : transactionalCaches.values()) { txCache.commit(); // 進入該方法 } }
public void commit() { if (clearOnCommit) { delegate.clear(); } else { for (RemoveEntry entry : entriesToRemoveOnCommit.values()) { entry.commit(); } }
// 把之前存放到entiriesToAddOnCommit中的數據提交到二級緩存中,具體的說是存放到PerpetualCache類的一個HashMap中 for (AddEntry entry : entriesToAddOnCommit.values()) {
// 進入該方法 entry.commit(); }
//清空該TransactionCache中的數據 reset(); }
進入entry.commit()方法:
public void commit() { cache.putObject(key, value); //放到PerpetualCache類中的HashMap中 }
到這里二級緩存的原理應該理解個大概了,總結下:當第一次從數據庫中查出數據后,會放到TransactionCache類中;當調用sqlSession.commit()方法,進行事務提交后,TransactionCache中的數據會提交到PerpetualCache中,查詢二級緩存的數據就是在這個類中,同時,TransactionCache中的數據會清空。
2.2.3 第三步:第二次查詢
在事務提交之后,數據結果集對象就存放在了二級緩存中,所以第二次查詢時,就可以從二級緩存中查詢到數據了。進入TransactionalCache的getObject方法:
@Override public Object getObject(Object key) { if (clearOnCommit) return null; // issue #146 // 用到了裝飾器模式,從PerpetualCache中取出數據
return delegate.getObject(key); }
2.3 二級緩存的清除過程
先運行以下demo:
public class GoodsDaoTest { private static SqlSessionFactory sqlSessionFactory = null; @Test public void selectGoodsTest(){ SqlSession sqlSession = getSqlSessionFactory().openSession(true); SqlSession sqlSession2 = getSqlSessionFactory().openSession(true); SqlSession sqlSession3 = getSqlSessionFactory().openSession(true); GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class) ; GoodsDao goodsMapper2 = sqlSession2.getMapper(GoodsDao.class) ; GoodsDao goodsMapper3 = sqlSession3.getMapper(GoodsDao.class) ; goodsMapper.selectGoodsById("1"); sqlSession.commit(); Goods goods = new Goods(); goods.setName("java1"); goods.setId("1"); goodsMapper3.updateGoodsById(goods); // 第一步 更新操作 sqlSession3.commit(); // 第二步 提交事務 goodsMapper2.selectGoodsById("1"); } public static SqlSessionFactory getSqlSessionFactory() { String resource = "spring-ibatis.xml"; if(sqlSessionFactory == null){ try { sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources .getResourceAsReader(resource)); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return sqlSessionFactory; } }
看日志:
==> Preparing: select * from goods WHERE id = ? ==> Parameters: 1(String) <== Columns: id, name, price, detail, remark <== Row: 1, java1, 30.00, null, null <== Total: 1 Opening JDBC Connection Created connection 1998228836. ==> Preparing: update goods set name = ? where id = ? ==> Parameters: java1(String), 1(String) <== Updates: 1 Cache Hit Ratio [com.yht.mybatisTest.dao.GoodsDao]: 0.0 Opening JDBC Connection Created connection 1945928717. ==> Preparing: select * from goods WHERE id = ? ==> Parameters: 1(String) <== Columns: id, name, price, detail, remark <== Row: 1, java1, 30.00, null, null <== Total: 1
總結:在更新操作,並提交事務后,清除了二級緩存,所以第二次查詢時,是從數據庫中查詢的數據。接下來,就針對更新操作和提交事務這兩個過程作分析。
2.3.1 第一步 更新操作
進入CachingExecutor類的update方法:
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
// 進入該方法,可知:清空了TransactionalCache中entriesToAddOnCommit和entriesToRemoveOnCommit的數據,同時clearOnCommit設置為true flushCacheIfRequired(ms); return delegate.update(ms, parameterObject);
但是二級緩存中的數據對象並未清除,所以進入第二步事務提交。
2.3.2 第二步 事務提交
最終進入TransactionalCache的commit方法:
public void commit() { if (clearOnCommit) {
// 由於在上一步更新操作中,clearOnCommit設置為了true,所以進入此方法:清除二級緩存中的數據 delegate.clear(); } else { for (RemoveEntry entry : entriesToRemoveOnCommit.values()) { entry.commit(); } } for (AddEntry entry : entriesToAddOnCommit.values()) { entry.commit(); } reset(); }
這就是清除二級緩存的過程。
總結一下:其實主要就是把這幾個類之間的關系及其作用搞清楚就行了:CachingExecutor,BaseExecutor,SimpleExecutor和TransactionalCache,PerpetualCache。這幾個類也是整個請求過程中比較重要的類。