MyBatis 一級緩存、二級緩存全詳解(一)


MyBatis 一級緩存、二級緩存全詳解(一)

什么是緩存

緩存就是內存中的一個對象,用於對數據庫查詢結果的保存,用於減少與數據庫的交互次數從而降低數據庫的壓力,進而提高響應速度。

什么是MyBatis中的緩存

MyBatis 中的緩存就是說 MyBatis 在執行一次SQL查詢或者SQL更新之后,這條SQL語句並不會消失,而是被MyBatis 緩存起來,當再次執行相同SQL語句的時候,就會直接從緩存中進行提取,而不是再次執行SQL命令。

MyBatis中的緩存分為一級緩存和二級緩存,一級緩存又被稱為 SqlSession 級別的緩存,二級緩存又被稱為表級緩存。

SqlSession是什么?SqlSession 是SqlSessionFactory會話工廠創建出來的一個會話的對象,這個SqlSession對象用於執行具體的SQL語句並返回給用戶請求的結果。

SqlSession級別的緩存是什么意思?SqlSession級別的緩存表示的就是每當執行一條SQL語句后,默認就會把該SQL語句緩存起來,也被稱為會話緩存

MyBatis 中的一級緩存

一級緩存是 SqlSession級別 的緩存。在操作數據庫時需要構造 sqlSession 對象,在對象中有一個(內存區域)數據結構(HashMap)用於存儲緩存數據。不同的 sqlSession 之間的緩存數據區域(HashMap)是互相不影響的。用一張圖來表示一下一級緩存,其中每一個 SqlSession 的內部都會有一個一級緩存對象。

在應用運行過程中,我們有可能在一次數據庫會話中,執行多次查詢條件完全相同的SQL,MyBatis 提供了一級緩存的方案優化這部分場景,如果是相同的SQL語句,會優先命中一級緩存,避免直接對數據庫進行查詢,提高性能。

初探一級緩存

我們繼續使用 MyBatis基礎搭建以及配置詳解中的例子(https://mp.weixin.qq.com/s/Ys03zaTSaOakdGU4RlLJ1A)進行 一級緩存的探究。

在對應的 resources 根目錄下加上日志的輸出信息 log4j.properties

##define an appender named console
log4j.appender.console=org.apache.log4j.ConsoleAppender
#The Target value is System.out or System.err
log4j.appender.console.Target=System.out
#set the layout type of the apperder
log4j.appender.console.layout=org.apache.log4j.PatternLayout
#set the layout format pattern
log4j.appender.console.layout.ConversionPattern=[%-5p] %m%n

##define a logger
log4j.rootLogger=debug,console

模擬思路: 既然每個 SqlSession 都會有自己的一個緩存,那么我們用同一個 SqlSession 是不是就能感受到一級緩存的存在呢?調用多次 getMapper 方法,生成對應的SQL語句,判斷每次SQL語句是從緩存中取還是對數據庫進行操作,下面的例子來證明一下

@Test
public void test(){
  DeptDao deptDao = sqlSession.getMapper(DeptDao.class);
  Dept dept = deptDao.findByDeptNo(1);
  System.out.println(dept);

  DeptDao deptDao2 = sqlSession.getMapper(DeptDao.class);
  Dept dept2 = deptDao2.findByDeptNo(1);
  System.out.println(dept2);
  System.out.println(deptDao2.findByDeptNo(1));
}

輸出:

可以看到,上面代碼執行了三條相同的SQL語句,但是只有一條SQL語句進行了輸出,其他兩條SQL語句都是從緩存中查詢的,所以它們生成了相同的 Dept 對象。

探究一級緩存是如何失效的

上面的一級緩存初探讓我們感受到了 MyBatis 中一級緩存的存在,那么現在你或許就會有疑問了,那么什么時候緩存失效呢? 這個問題也就是我們接下來需要詳細討論的議題之一。

探究更新對一級緩存失效的影響

上面的代碼執行了三次相同的查詢操作,返回了相同的結果,那么,如果我在第一條和第二條SQL語句之前插入更新的SQL語句,是否會對一級緩存產生影響呢?代碼如下:

@Test
public void testCacheLose(){
  DeptDao deptDao = sqlSession.getMapper(DeptDao.class);
  Dept dept = deptDao.findByDeptNo(1);
  System.out.println(dept);

  // 在兩次查詢之間使用 更新 操作,是否會對一級緩存產生影響
  deptDao.insertDept(new Dept(7,"tengxun","shenzhen"));
  //        deptDao.updateDept(new Dept(1,"zhongke","sjz"));
  //        deptDao.deleteByDeptNo(7);

  DeptDao deptDao2 = sqlSession.getMapper(DeptDao.class);
  Dept dept2 = deptDao2.findByDeptNo(1);
  System.out.println(dept2);
}

為了演示效果,就不貼出 insertDept 的代碼了,就是一條簡單的插入語句。

分別放開不同的更新語句,發現執行效果如下

輸出結果:

如圖所示,在兩次查詢語句中使用插入,會對一級緩存進行刷新,會導致一級緩存失效。

探究不同的 SqlSession 對一級緩存的影響

如果你看到這里了,那么你應該知道一級緩存就是 SqlSession 級別的緩存,而同一個 SqlSession 會有相同的一級緩存,那么使用不同的 SqlSession 是不是會對一級緩存產生影響呢? 顯而易見是的,那么下面就來演示並且證明一下

private SqlSessionFactory factory; // 把factory設置為全局變量

@Test
public void testCacheLoseWithSqlSession(){
  DeptDao deptDao = sqlSession.getMapper(DeptDao.class);
  Dept dept = deptDao.findByDeptNo(1);
  System.out.println(dept);

  SqlSession sqlSession2 = factory.openSession();
  DeptDao deptDao2 = sqlSession2.getMapper(DeptDao.class);
  Dept dept2 = deptDao2.findByDeptNo(1);
  System.out.println(dept2);

}

輸出:

上面代碼使用了不同的 SqlSession 對同一個SQL語句執行了相同的查詢操作,卻對數據庫執行了兩次相同的查詢操作,生成了不同的 dept 對象,由此可見,不同的 SqlSession 是肯定會對一級緩存產生影響的。

同一個 SqlSession 使用不同的查詢操作

使用不同的查詢條件是否會對一級緩存產生影響呢?可能在你心里已經有這個答案了,再來看一下代碼吧

@Test
public void testWithDifferentParam(){
  DeptDao deptDao = sqlSession.getMapper(DeptDao.class);
  Dept dept = deptDao.findByDeptNo(1);
  System.out.println(dept);

  DeptDao deptDao2 = sqlSession.getMapper(DeptDao.class);
  Dept dept2 = deptDao2.findByDeptNo(5);
  System.out.println(dept2);
}

輸出結果

我們在兩次查詢SQL分別使用了不同的查詢條件,查詢出來的數據不一致,那就肯定會對一級緩存產生影響了。

手動清理緩存對一級緩存的影響

我們在兩次查詢的SQL語句之間使用 clearCache 是否會對一級緩存產生影響呢?下面例子證實了這一點

@Test
public void testClearCache(){
  DeptDao deptDao = sqlSession.getMapper(DeptDao.class);
  Dept dept = deptDao.findByDeptNo(1);
  System.out.println(dept);

	//在兩次相同的SQL語句之間使用查詢操作,對一級緩存的影響。
  sqlSession.clearCache();

  DeptDao deptDao2 = sqlSession.getMapper(DeptDao.class);
  Dept dept2 = deptDao2.findByDeptNo(1);
  System.out.println(dept2);
}

輸出:

我們在兩次查詢操作之間,使用了 sqlSession 的 clearCache() 方法清除了一級緩存,所以使用 clearCache 也會對一級緩存產生影響。

一級緩存原理探究

一級緩存到底是什么?一級緩存的工作流程是怎樣的?一級緩存何時消失?相信你現在應該會有這幾個疑問,那么我們本節就來研究一下一級緩存的本質

嗯。。。。。。該從何處入手呢?

你可以這樣想,上面我們一直提到一級緩存,那么提到一級緩存就繞不開 SqlSession,所以索性我們就直接從 SqlSession ,看看有沒有創建緩存或者與緩存有關的屬性或者方法

調研了一圈,發現上述所有方法中,好像只有 clearCache() 和緩存沾點關系,那么就直接從這個方法入手吧,分析源碼時,我們要看它(此類)是誰,它的父類和子類分別又是誰,對如上關系了解了,你才會對這個類有更深的認識,分析了一圈,你可能會得到如下這個流程圖

再深入分析,流程走到Perpetualcache 中的 clear() 方法之后,會調用其 cache.clear() 方法,那么這個cache 是什么東西呢? 點進去發現,cache 其實就是 private Map<Object, Object> cache = new HashMap<Object, Object>(); 也就是一個Map,所以說 cache.clear() 其實就是 map.clear() ,也就是說,緩存其實就是本地存放的一個 map 對象,每一個SqlSession 都會存放一個 map 對象的引用,那么這個 cache 是何時創建的呢?

你覺得最有可能創建緩存的地方是哪里呢? 我覺得是 Executor,為什么這么認為? 因為 Executor 是執行器,用來執行SQL請求,而且清除緩存的方法也在 Executor 中執行,所以很可能緩存的創建也很有可能在 Executor 中,看了一圈發現 Executor 中有一個 createCacheKey 方法,這個方法很像是創建緩存的方法啊,跟進去看看,你發現 createCacheKey 方法是由 BaseExecutor 執行的,代碼如下

CacheKey cacheKey = new CacheKey();
//MappedStatement的id
// id 就是Sql語句的所在位置 包名 + 類名 + SQL名稱
cacheKey.update(ms.getId());
// offset 就是 0
cacheKey.update(rowBounds.getOffset());
// limit 就是 Integer.MAXVALUE
cacheKey.update(rowBounds.getLimit());
// 具體的SQL語句
cacheKey.update(boundSql.getSql());
//后面是update了sql中帶的參數
cacheKey.update(value);
...
if (configuration.getEnvironment() != null) {
  // issue #176
  cacheKey.update(configuration.getEnvironment().getId());
}

創建緩存key會經過一系列的 update 方法,update 方法由一個 CacheKey 這個對象來執行的,這個 update 方法最終由 updateList 的 list 來把五個值存進去,對照上面的代碼和下面的圖示,你應該能理解這五個值都是什么了

這里需要注意一下最后一個值, configuration.getEnvironment().getId() 這是什么,這其實就是定義在 mybatis-config.xml 中的 標簽,見如下。

<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
 <property name="driver" value="${jdbc.driver}"/>
 <property name="url" value="${jdbc.url}"/>
 <property name="username" value="${jdbc.username}"/>
 <property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>

那么我們回歸正題,那么創建完緩存之后該用在何處呢?總不會憑空創建一個緩存不使用吧?絕對不會的,經過我們對一級緩存的探究之后,我們發現一級緩存更多是用於查詢操作畢竟一級緩存也叫做查詢緩存吧,為什么叫查詢緩存我們一會兒說。我們先來看一下這個緩存到底用在哪了,我們跟蹤到 query 方法如下:

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameter);
  // 創建緩存
  CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
  return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  ...
  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);
  }
  ...
}

// 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;
}

如果查不到的話,就從數據庫查,在queryFromDatabase中,會對localcache進行寫入。localcache 對象的put 方法最終交給 Map 進行存放

private Map<Object, Object> cache = new HashMap<Object, Object>();

@Override
public void putObject(Object key, Object value) {
  cache.put(key, value);
}

那么再說一下為什么一級緩存也叫做查詢緩存呢?

我們先來看一下 update 更新方法,先來看一下 update 的源碼

@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
  ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  clearLocalCache();
  return doUpdate(ms, parameter);
}

BaseExecutor 在每次執行 update 方法的時候,都會先 clearLocalCache() ,所以更新方法並不會有緩存,這也就是說為什么一級緩存也叫做查詢緩存了,這也就是為什么我們沒有探究多次執行更新方法對一級緩存的影響了。

還有其他要補充的嗎?

我們上面分析了一級緩存的執行流程,為什么一級緩存要叫查詢緩存以及一級緩存組成條件

那么,你可能看到這感覺這些知識還是不夠連貫,那么我就幫你把 一級緩存的探究 小結中的原理說一下吧,為什么一級緩存會失效

  1. 探究更新對一級緩存失效的影響: 由上面的分析結論可知,我們每次執行 update 方法時,都會先刷新一級緩存,因為是同一個 SqlSession, 所以是由同一個 Map 進行存儲的,所以此時一級緩存會失效
  2. 探究不同的 SqlSession 對一級緩存的影響: 這個也就比較好理解了,因為不同的 SqlSession 會有不同的Map 存儲一級緩存,然而 SqlSession 之間也不會共享,所以此時也就不存在相同的一級緩存
  3. 同一個 SqlSession 使用不同的查詢操作: 這個論點就需要從緩存的構成角度來講了,我們通過 cacheKey 可知,一級緩存命中的必要條件是兩個 cacheKey 相同,要使得 cacheKey 相同,就需要使 cacheKey 里面的值相同,也就是

看出差別了嗎?第一個SQL 我們查詢的是部門編號為1的值,而第二個SQL我們查詢的是編號為5的值,兩個緩存對象不相同,所以也就不存在緩存。

  1. 手動清理緩存對一級緩存的影響: 由程序員自己去調用clearCache方法,這個方法就是清除緩存的方法,所以也就不存在緩存了。

總結

所以此文章到底寫了點什么呢?拋給你幾個問題了解一下

  1. 什么是緩存?什么是 MyBatis 緩存?
  2. 認識MyBatis緩存,MyBatis 一級緩存的失效方式
  3. MyBatis 一級緩存的執行流程,MyBatis 一級緩存究竟是什么?

文章參考:

mybatis的緩存機制(一級緩存二級緩存和刷新緩存)和mybatis整合ehcache

https://blog.csdn.net/u012373815/article/details/47069223

[聊聊MyBatis緩存機制](

公眾號提供 優質Java資料 以及CSDN免費下載 權限,歡迎你關注我


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM