mybatis緩存分為一級緩存,二級緩存和自定義緩存。本文重點講解一級緩存
一:前言
在介紹緩存之前,先了解下mybatis的幾個核心概念:
* SqlSession:代表和數據庫的一次會話,向用戶提供了操作數據庫的方法
* MapperedStatement:代表要往數據庫發送的要執行的指令,可以理解為sql的抽象表示
* Executor:用來和數據庫交互的執行器,接收MapperedStatement作為參數
二:一級緩存
1.一級緩存的介紹:
mybatis一級緩存有兩種:一種是SESSION級別的,針對同一個會話SqlSession中,執行多次條件完全相同的同一個sql,那么會共享這一緩存,默認是SESSION級別的緩存;一種是STATEMENT級別的,緩存只針對當前執行的這一statement有效。
對於一級緩存的流程,看下圖:

整個流程是這樣的:
* 針對某個查詢的statement,生成唯一的key
* 在Local Cache 中根據key查詢數據是否存在
* 如果存在,則命中,跳過數據庫查詢,繼續往下走
* 如果沒命中:
* 去數據庫中查詢,得到查詢結果
* 將key和查詢結果放到Local Cache中
* 將查詢結果返回
* 判斷是否是STATEMENT級別緩存,如果是,則清除緩存
接下來針對一級緩存的幾種情況,來進行驗證。
情況1:SESSION級別緩存,同一個Mapper代理對象執行條件相同的同一個查詢sql
SqlSession sqlSession = getSqlSessionFactory().openSession(); GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class); goodsMapper.selectGoodsById("1"); goodsMapper.selectGoodsById("1");
結果:
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@3dd44d5e] ==> Preparing: select * from goods where id = ? ==> Parameters: 1(String) <== Columns: id, name, detail, remark <== Row: 1, title1, null, null <== Total: 1
總結:只向數據庫進行了一次查詢,第二次用了緩存
情況2:SESSION級別緩存,同一個Mapper代理對象執行條件不同的同一個查詢sql
public void selectGoodsTest(){ SqlSession sqlSession = getSqlSessionFactory().openSession(); GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class); goodsMapper.selectGoodsById("1"); goodsMapper.selectGoodsById("2"); }
結果:
==> Preparing: select * from goods where id = ? ==> Parameters: 1(String) <== Columns: id, name, detail, remark <== Row: 1, title1, null, null <== Total: 1
==> Preparing: select * from goods where id = ? ==> Parameters: 2(String) <== Columns: id, name, detail, remark <== Row: 2, title2, null, null <== Total: 1
總結:因為查詢條件不同,所以是兩個不同的statement,生成了兩個不同key,緩存中是沒有的
情況3:SESSION級別緩存,針對同一個Mapper接口生成兩個代理對象,然后執行查詢條件完全相同的同一條sql
public void selectGoodsTest(){ SqlSession sqlSession = getSqlSessionFactory().openSession(); GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class); GoodsDao goodsMapper2 = sqlSession.getMapper(GoodsDao.class); goodsMapper.selectGoodsById("1"); goodsMapper2.selectGoodsById("1"); }
結果:
==> Preparing: select * from goods where id = ?
==> Parameters: 1(String)
<== Columns: id, name, detail, remark
<== Row: 1, title1, null, null
<== Total: 1
總結:這種情況滿足:同一個SqlSession會話,查詢條件完全相同的同一條sql。所以,第二次查詢是從緩存中查找的。
情況4:SESSION級別緩存,在同一次會話中,對數據庫進行了修改操作,一級緩存是否是失效。
// 這里對id=2的數據進行了upate操作,發現id=1的一級緩存也被清除,因為它們是在同一個SqlSession中
@Test public void selectGoodsTest(){ SqlSession sqlSession = getSqlSessionFactory().openSession(); GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class); Goods goods = new Goods(); goods.setId("2"); goods.setName("籃球"); goodsMapper.selectGoodsById("1"); goodsMapper.updateGoodsById(goods); goodsMapper.selectGoodsById("1"); }
結果:
==> Preparing: select * from goods where id = ? ==> Parameters: 1(String) <== Columns: id, name, detail, remark <== Row: 1, title1, null, null <== Total: 1
==> Preparing: update goods set name = ? where id = ? ==> Parameters: 籃球(String), 2(String) <== Updates: 1
==> Preparing: select * from goods where id = ? ==> Parameters: 1(String) <== Columns: id, name, detail, remark <== Row: 1, title1, null, null <== Total: 1
總結:在同一個SqlSession會話中,如果對數據庫進行了修改操作,那么該會話中的緩存都會被清除。但是,並不會影響其它會話中的緩存。
情況5:SESSION級別緩存,開啟兩個SqlSession,在SqlSession1中查詢操作,在SqlSession2中執行修改操作,那么SqlSession1中的一級緩存是否仍然有效?
@Test public void selectGoodsTest(){ SqlSession sqlSession = getSqlSessionFactory().openSession(); GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class); SqlSession sqlSession2 = getSqlSessionFactory().openSession(); GoodsDao goodsMapper2 = sqlSession2.getMapper(GoodsDao.class); Goods goods = new Goods(); goods.setId("1"); goods.setName("籃球"); Goods goods1 = goodsMapper.selectGoodsById("1"); System.out.println("name="+goods1.getName()); System.out.println("******************************************************"); goodsMapper2.updateGoodsById(goods); Goods goodsResult = goodsMapper.selectGoodsById("1"); System.out.println("******************************************************"); System.out.println("name="+goodsResult.getName()); }
結果:
==> Preparing: select * from goods where id = ? ==> Parameters: 1(String) <== Columns: id, name, detail, remark <== Row: 1, title1, null, null <== Total: 1 name=title1 ****************************************************** Opening JDBC Connection Created connection 644010817. Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@2662d341] ==> Preparing: update goods set name = ? where id = ? ==> Parameters: 籃球(String), 1(String) <== Updates: 1 ****************************************************** name=title1
總結:在SqlSession2中對id=1的數據做了修改,但是在SqlSession1中的最后一次查詢中,仍然是從一級緩存中取得數據,說明了一級緩存只在SqlSession內部共享,SqlSession對數據庫的修改操作不影響其它SqlSession中的一級緩存。
情況6:SqlSession的緩存級別設置為STATEMENT,即在配置文件中添加如下代碼:
<settings>
<setting name="localCacheScope" value="STATEMENT"/>
</settings>
執行代碼:
@Test public void selectGoodsTest(){ SqlSession sqlSession = getSqlSessionFactory().openSession(); GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class); goodsMapper.selectGoodsById("1"); System.out.println("****************************************"); goodsMapper.selectGoodsById("1"); }
結果:
==> Preparing: select * from goods where id = ? ==> Parameters: 1(String) <== Columns: id, name, detail, remark <== Row: 1, title1, null, null <== Total: 1 **************************************** ==> Preparing: select * from goods where id = ? ==> Parameters: 1(String) <== Columns: id, name, detail, remark <== Row: 1, title1, null, null <== Total: 1
總結:STATEMENT級別的緩存,只針對當前執行的這一statement有效
2.一級緩存是如何被存取的?
我們知道,當與數據庫建立一次連接,就會創建一個SqlSession對象,默認是DefaultSqlSession這個實現,這個對象給用戶提供了操作數據庫的各種方法,與此同時,也會創建一個Executor執行器,緩存信息就是維護在Executor中,Executor有一個抽象子類BaseExecutor,這個類中有個屬性PerpetualCache類,這個類就是真正用於維護一級緩存的地方。通過看源碼,可以知道如何根據cacheKey,取出和存放緩存的。
在查詢數據庫前,先從緩存中查找,進入BaseExecutor類的query方法:
//這是BaseExecutor的一個屬性,用於存放一級緩存
protected PerpetualCache localCache;
@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."); if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } List<E> list; try { queryStack++;
// 根據CacheKey作為key,查詢HashMap中的value值,也就是緩存,這就是取出緩存的過程 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--; } if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } deferredLoads.clear(); // issue #601 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { clearLocalCache(); // issue #482 } } return list; }
當從數據庫中查詢到數據后,需要把數據存放到緩存中的,然后再返回數據,這個就是存放緩存的過程,進入queryFromDatabase方法:
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; }
3.CacheKey是如何確定唯一的?
我們知道,如果兩次查詢完全相同,那么第二次查詢就從緩存中取數據,換句話說,怎么判斷兩次查詢是不是相同的?是否相同是根據CacheKey來判斷的,那么看下CacheKey的生成過程,就知道影響CacheKey是否相同的元素有哪些了。
進入BaseExecutor類的createCacheKey方法:
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { if (closed) throw new ExecutorException("Executor was closed."); CacheKey cacheKey = new CacheKey();
// statement id cacheKey.update(ms.getId());
// rowBounds.offset cacheKey.update(rowBounds.getOffset()); // rowBounds.limit
cacheKey.update(rowBounds.getLimit()); // sql語句
cacheKey.update(boundSql.getSql()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); for (int i = 0; i < parameterMappings.size(); i++) { // mimic DefaultParameterHandler logic ParameterMapping parameterMapping = parameterMappings.get(i); 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); } } return cacheKey; }
所以影響Cachekey是否相同的因素有:statementId,offset,limit,sql語句,參數
接下來進入cacheKey.update方法,看它如何處理以上這五個元素的:
private void doUpdate(Object object) {
// 獲取對象的HashCode int baseHashCode = object == null ? 1 : object.hashCode(); // 計數器+1 count++; checksum += baseHashCode;
// baseHashCode擴大count倍 baseHashCode *= count; // 對HashCode進一步做處理 hashcode = multiplier * hashcode + baseHashCode; // 把以上五個元素存放到集合中 updateList.add(object); }
CahceKey的屬性和構造方法:
private int multiplier;
private int hashcode;
private long checksum;
private int count;
private List<Object> updateList;
public CacheKey() { this.hashcode = DEFAULT_HASHCODE; this.multiplier = DEFAULT_MULTIPLYER; this.count = 0; this.updateList = new ArrayList<Object>(); }
CacheKey中最重要的一個方法來了,如何判斷兩個CacheKey是否相等?
public boolean equals(Object object) { if (this == object) return true; if (!(object instanceof CacheKey)) return false; final CacheKey cacheKey = (CacheKey) object; // 判斷HashCode是否相等 if (hashcode != cacheKey.hashcode) return false;
// 判斷checksum是否相等 if (checksum != cacheKey.checksum) return false;
// 判斷count是否相等 if (count != cacheKey.count) return false; // 逐一判斷以上五個元素是否相等 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; }
// 只有以上所有的判斷都相等時,兩個CacheKey才相等
4.一級緩存的生命周期是多長?
開始:mybatis建立一次數據庫會話時,就會生成一系列對象:SqlSession--->Executor--->PerpetualCache,也就開啟了對一級緩存的維護。
結束:
* 會話結束,會釋放掉以上生成的一系列對象,緩存也就不可用了。
* 調用sqlSession.close方法,會釋放掉PerpetualCache對象,一級緩存不可用
* 調用sqlSession.clearCache方法,會清空PerpetualCache對象中的緩存數據,該對象可用,一級緩存不可用
* 調用sqlSession的update,insert,delete方法,會清空PerpetualCache對象中的緩存數據,該對象可用,一級緩存不可用
參考資料:https://blog.csdn.net/luanlouis/article/details/41280959
