MyBatis 示例-快取

  • 2019 年 10 月 29 日
  • 筆記

MyBatis 提供兩種類型的快取,一種是一級快取,另一種是二級快取,本章通過例子的形式描述 MyBatis 快取的使用。

測試類:com.yjw.demo.CacheTest

一級快取

MyBatis 默認開啟一級快取。一級快取是相對於同一個 SqlSession 而言的,所以在參數和 SQL 完全一樣的情況下,我們使用同一個 SqlSession 對象調用同一個 Mapper 的方法,往往只執行一次 SQL,因為使用 SqlSession 第一次查詢後,MyBatis 會將其放在快取中,以後再查詢的時候,如果沒有聲明需要刷新,並且快取沒超時的情況下,SqlSession 都只會取出當前快取的數據,而不會再次發送 SQL 到資料庫。

測試方法:

/**   * 一級快取   */  @Test  public void l1Cache() {      SqlSession sqlSession = sqlSessionFactory.openSession();      long startTime1 = System.currentTimeMillis();      sqlSession.selectList("com.yjw.demo.mybatis.biz.dao.StudentDao.listByConditions");      LOGGER.info("第一次查詢執行時間:" + (System.currentTimeMillis() - startTime1));      long startTime2 = System.currentTimeMillis();      sqlSession.selectList("com.yjw.demo.mybatis.biz.dao.StudentDao.listByConditions");      LOGGER.info("第二次查詢執行時間:" + (System.currentTimeMillis() - startTime2));      sqlSession.close();  }
2019-09-16 10:16:02.133  INFO 26268 --- [           main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited  2019-09-16 10:16:02.148 DEBUG 26268 --- [           main] c.y.d.m.b.d.StudentDao.listByConditions  : ==>  Preparing: select id, name, sex, selfcard_no, note from t_student  2019-09-16 10:16:02.210 DEBUG 26268 --- [           main] c.y.d.m.b.d.StudentDao.listByConditions  : ==> Parameters:  2019-09-16 10:16:02.242 DEBUG 26268 --- [           main] c.y.d.m.b.d.StudentDao.listByConditions  : <==      Total: 3  2019-09-16 10:16:02.243  INFO 26268 --- [           main] com.yjw.demo.CacheTest                   : 第一次查詢執行時間:825  2019-09-16 10:16:02.244  INFO 26268 --- [           main] com.yjw.demo.CacheTest                   : 第二次查詢執行時間:1

對比兩次查詢的日誌內容,第二次查詢沒有執行 SQL 語句,顯然第二次查詢是從快取中獲取的數據。

二級快取(不建議使用)

MyBatis 默認不開啟二級快取。二級快取是 SqlSessionFactory 層面上的 ,二級快取的開啟需要進行配置,實現二級快取的時候,MyBatis 要求返回的 POJO 必須是可序列化的,也就是要求實現 Serializable 介面,配置的方法很簡單,只需要在映射 XML 文件配置 <cache /> 元素就可以開啟快取了。

MyBatis 二級快取是基於 namespace 的,快取的內容是根據 namespace 存放的,可以認為 namespace 就是快取的 KEY 值 。

<cache />

這樣的一條語句裡面,很多設置是默認的,如果我們只是這樣配置,那麼就意味著:

  • 映射語句文件中的所有 select 語句將會被快取;
  • 映射語句文件中的所有 insert、update 和 delete 語句會刷新快取;
  • 快取會使用默認的 Least Recently Used(LRU,最近最少使用的)演算法來收回;
  • 根據時間表,比如 No Flush Interval,(CNFI,沒有刷新間隔),快取不會以任何時間順序來刷新;
  • 快取會存儲列表集合或對象(無論查詢方法返回什麼)的1024個引用;
  • 快取會被視為是 read/write(可讀/可寫)的快取,意味著對象檢索不是共享的,而且可以安全地被調用者修改,不干擾其他調用者或執行緒所做的潛在修改。

另外我們還可以通過<cache-res />配置實現多個 namespace 共用同一個二級快取,即同一個 Cache 對象。

 

如上圖所示,namespace2 共用了 namespace1 的 Cache 對象。

二級快取可以和一級快取共存,通過下圖來理解 MyBatis 的兩層快取結構。

 

 

當應用程式通過 SqlSession2 執行定義在命名空間 namespace2 中的查詢操作時,SqlSession2 首先到 namespace2 對應的二級快取中查找是否快取了相應的結果對象。如果沒有,則繼續到 SqlSession2 對應的一級快取中查找是否快取了相應的結果對象,如果依然沒有,則訪問資料庫獲取結果集並映射成結果對象返回。 最後,該結果對象會記錄到 SqlSession 對應的一級快取以及 namespace2 對應的二級快取中,等待後續使用。另外需要注意的是,上圖中的命名空間 namespace2 和 namespace3 共享了同一個二級快取對象,所以通過 SqlSession3 執行命名空間 namespace3 中的完全相同的查詢操作(只要該查詢生成的 CacheKey 對象與上述 SqlSession2 中的查詢生成 CacheKey 對象相同即可)時,可以直接從二級快取中得到相應的結果對象。 

案例:

我們通過案例測試一下二級快取,首先實體類必須實現 Serializable 介面,在 StudentMapper 文件中添加如下配置:

<!-- 二級快取 -->  <cache eviction="LRU" flushInterval="100000" size="1024" readOnly="true" />

測試方法:

/**   * 二級快取   */  @Test  public void l2Cache() {      SqlSession sqlSession1 = sqlSessionFactory.openSession();      long startTime1 = System.currentTimeMillis();      sqlSession1.selectList("com.yjw.demo.mybatis.biz.dao.StudentDao.listByConditions",              new StudentQuery());      LOGGER.info("第一個SqlSession查詢執行時間:" + (System.currentTimeMillis() - startTime1));      sqlSession1.commit();      sqlSession1.close();        SqlSession sqlSession2 = sqlSessionFactory.openSession();      long startTime2 = System.currentTimeMillis();      sqlSession2.selectList("com.yjw.demo.mybatis.biz.dao.StudentDao.listByConditions",              new StudentQuery());      LOGGER.info("第二個SqlSession查詢執行時間:" + (System.currentTimeMillis() - startTime2));      sqlSession2.commit();      sqlSession2.close();  }
2019-09-16 14:33:13.848 DEBUG 22372 --- [           main] com.yjw.demo.mybatis.biz.dao.StudentDao  : Cache Hit Ratio [com.yjw.demo.mybatis.biz.dao.StudentDao]: 0.0  2019-09-16 14:33:15.748  INFO 22372 --- [           main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited  2019-09-16 14:33:15.764 DEBUG 22372 --- [           main] c.y.d.m.b.d.StudentDao.listByConditions  : ==>  Preparing: select id, name, sex, selfcard_no, note from t_student  2019-09-16 14:33:15.844 DEBUG 22372 --- [           main] c.y.d.m.b.d.StudentDao.listByConditions  : ==> Parameters:  2019-09-16 14:33:15.885 DEBUG 22372 --- [           main] c.y.d.m.b.d.StudentDao.listByConditions  : <==      Total: 3  2019-09-16 14:33:15.887  INFO 22372 --- [           main] com.yjw.demo.CacheTest                   : 第一個SqlSession查詢執行時間:2304  2019-09-16 14:33:15.890 DEBUG 22372 --- [           main] com.yjw.demo.mybatis.biz.dao.StudentDao  : Cache Hit Ratio [com.yjw.demo.mybatis.biz.dao.StudentDao]: 0.5  2019-09-16 14:33:15.891  INFO 22372 --- [           main] com.yjw.demo.CacheTest                   : 第二個SqlSession查詢執行時間:1

從日誌中可以看出,第二次查詢沒有執行 SQL 語句,日誌中還列印了快取命令率:Cache Hit Ratio,所以第二次 Session 執行是從快取中獲取的數據。

二級快取詳細配置介紹:

<cache eviction="LRU" flushInterval="100000" size="1024" readOnly="true" />
  • eviction:快取回收策略,目前 MyBatis 提供一下策略;
  • LRU:最近最少使用的,移除最長時間不用的對象;
  • FIFO:先進先出,按對象進入快取的順序來移除它們;
  • SOFT:軟引用,移除基於垃圾回收器狀態和軟引用規則的對象;
  • WEAK:弱引用,更積極地移除基於垃圾回收器狀態和弱引用規則的對象。這裡採用的是 LRU,移除最長時間不用的對象;
  • flushInterval:刷新間隔時間,單位為毫秒,這裡配置的是100秒刷新,如果不配置它,那麼當 SQL 被執行的時候才會去刷新快取;
  • size:引用數目,一個正整數,代表快取最多可以存儲多少個對象,不宜設置過大,設置過大會導致記憶體溢出,這裡配置的是1024個對象;
  • readOnly:只讀,意味著快取數據只能讀取而不能修改,這樣設置的好處是我們可以快速讀取快取,缺點是我們沒有辦法修改快取。

二級快取的問題:

  • 臟數據:因為二級快取是基於 namespace 的,比如在 StudentMapper 中存在一條查詢 SQL,它關聯查詢了學生證件資訊,這個時候開啟了二級快取,在 StudentMapper 對應的快取中就會存在學生證件的數據,如果更新了學生證件資訊的數據,那麼在 StudentMapper 中就存在了臟數據;
  • 全部失效:insert、update 和 delete 語句會刷新同一個 namespace 下的所有快取數據,參考如下例子;
/**   * 測試二級快取全部失效問題,只要執行了insert、update、delete   * 就會刷新同一個 namespace 下的所有快取數據   */  @Test  public void l2CacheInvalid() {      // 快取listByConditions的數據      SqlSession sqlSession1 = sqlSessionFactory.openSession();      long startTime1 = System.currentTimeMillis();      sqlSession1.selectList("com.yjw.demo.mybatis.biz.dao.StudentDao.listByConditions",              new StudentQuery());      LOGGER.info("第一個SqlSession查詢執行時間:" + (System.currentTimeMillis() - startTime1));      sqlSession1.commit();      sqlSession1.close();        // 快取getByPrimaryKey的數據      SqlSession sqlSession2 = sqlSessionFactory.openSession();      long startTime2 = System.currentTimeMillis();      sqlSession2.selectList("com.yjw.demo.mybatis.biz.dao.StudentDao.getByPrimaryKey",              1L);      LOGGER.info("第二個SqlSession查詢執行時間:" + (System.currentTimeMillis() - startTime2));      sqlSession2.commit();      sqlSession2.close();        // 執行insert語句使上面所有快取失效      SqlSession sqlSession3 = sqlSessionFactory.openSession();      StudentDO studentDO = new StudentDO();      studentDO.setName("趙六");      studentDO.setSex(Sex.MALE);      studentDO.setSelfcardNo(4444L);      studentDO.setNote("zhaoliu");      sqlSession3.insert("com.yjw.demo.mybatis.biz.dao.StudentDao.insertByAutoInc", studentDO);      sqlSession3.commit();      sqlSession3.close();        // 再次執行上面快取的數據,查看快取是否已經失效      SqlSession sqlSession4 = sqlSessionFactory.openSession();      long startTime4 = System.currentTimeMillis();      sqlSession4.selectList("com.yjw.demo.mybatis.biz.dao.StudentDao.listByConditions",              new StudentQuery());      LOGGER.info("第四個SqlSession查詢執行時間:" + (System.currentTimeMillis() - startTime4));      sqlSession4.commit();      sqlSession4.close();        // 快取getByPrimaryKey的數據      SqlSession sqlSession5 = sqlSessionFactory.openSession();      long startTime5 = System.currentTimeMillis();      sqlSession5.selectList("com.yjw.demo.mybatis.biz.dao.StudentDao.getByPrimaryKey",              1L);      LOGGER.info("第五個SqlSession查詢執行時間:" + (System.currentTimeMillis() - startTime5));      sqlSession5.commit();      sqlSession5.close();  }
2019-09-16 14:47:43.489 DEBUG 14940 --- [           main] com.yjw.demo.mybatis.biz.dao.StudentDao  : Cache Hit Ratio [com.yjw.demo.mybatis.biz.dao.StudentDao]: 0.0  2019-09-16 14:47:44.258  INFO 14940 --- [           main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited  2019-09-16 14:47:44.274 DEBUG 14940 --- [           main] c.y.d.m.b.d.StudentDao.listByConditions  : ==>  Preparing: select id, name, sex, selfcard_no, note from t_student  2019-09-16 14:47:44.328 DEBUG 14940 --- [           main] c.y.d.m.b.d.StudentDao.listByConditions  : ==> Parameters:  2019-09-16 14:47:44.369 DEBUG 14940 --- [           main] c.y.d.m.b.d.StudentDao.listByConditions  : <==      Total: 3  2019-09-16 14:47:44.371  INFO 14940 --- [           main] com.yjw.demo.CacheTest                   : 第一個SqlSession查詢執行時間:1015  2019-09-16 14:47:44.377 DEBUG 14940 --- [           main] com.yjw.demo.mybatis.biz.dao.StudentDao  : Cache Hit Ratio [com.yjw.demo.mybatis.biz.dao.StudentDao]: 0.0  2019-09-16 14:47:44.378 DEBUG 14940 --- [           main] c.y.d.m.b.d.StudentDao.getByPrimaryKey   : ==>  Preparing: select id, name, sex, selfcard_no, note from t_student where id = ?  2019-09-16 14:47:44.380 DEBUG 14940 --- [           main] c.y.d.m.b.d.StudentDao.getByPrimaryKey   : ==> Parameters: 1(Long)  2019-09-16 14:47:44.382 DEBUG 14940 --- [           main] c.y.d.m.b.d.StudentDao.getByPrimaryKey   : <==      Total: 1  2019-09-16 14:47:44.383  INFO 14940 --- [           main] com.yjw.demo.CacheTest                   : 第二個SqlSession查詢執行時間:7  2019-09-16 14:47:44.383 DEBUG 14940 --- [           main] c.y.d.m.b.d.StudentDao.insertByAutoInc   : ==>  Preparing: insert into t_student (name, sex, selfcard_no, note) values ( ?, ?, ?, ? )  2019-09-16 14:47:44.388 DEBUG 14940 --- [           main] c.y.d.m.b.d.StudentDao.insertByAutoInc   : ==> Parameters: 趙六(String), 1(Integer), 4444(Long), zhaoliu(String)  2019-09-16 14:47:44.474 DEBUG 14940 --- [           main] c.y.d.m.b.d.StudentDao.insertByAutoInc   : <==    Updates: 1  2019-09-16 14:47:44.476 DEBUG 14940 --- [           main] com.yjw.demo.mybatis.biz.dao.StudentDao  : Cache Hit Ratio [com.yjw.demo.mybatis.biz.dao.StudentDao]: 0.0  2019-09-16 14:47:44.477 DEBUG 14940 --- [           main] c.y.d.m.b.d.StudentDao.listByConditions  : ==>  Preparing: select id, name, sex, selfcard_no, note from t_student  2019-09-16 14:47:44.477 DEBUG 14940 --- [           main] c.y.d.m.b.d.StudentDao.listByConditions  : ==> Parameters:  2019-09-16 14:47:44.481 DEBUG 14940 --- [           main] c.y.d.m.b.d.StudentDao.listByConditions  : <==      Total: 4  2019-09-16 14:47:44.481  INFO 14940 --- [           main] com.yjw.demo.CacheTest                   : 第四個SqlSession查詢執行時間:5  2019-09-16 14:47:44.482 DEBUG 14940 --- [           main] com.yjw.demo.mybatis.biz.dao.StudentDao  : Cache Hit Ratio [com.yjw.demo.mybatis.biz.dao.StudentDao]: 0.0  2019-09-16 14:47:44.483 DEBUG 14940 --- [           main] c.y.d.m.b.d.StudentDao.getByPrimaryKey   : ==>  Preparing: select id, name, sex, selfcard_no, note from t_student where id = ?  2019-09-16 14:47:44.483 DEBUG 14940 --- [           main] c.y.d.m.b.d.StudentDao.getByPrimaryKey   : ==> Parameters: 1(Long)  2019-09-16 14:47:44.485 DEBUG 14940 --- [           main] c.y.d.m.b.d.StudentDao.getByPrimaryKey   : <==      Total: 1  2019-09-16 14:47:44.486  INFO 14940 --- [           main] com.yjw.demo.CacheTest                   : 第五個SqlSession查詢執行時間:4

從上面的日誌資訊可以看出,四次查詢操作,都執行了 SQL 語句,第四個和第五個查詢沒有從快取中獲取數據,因為第三個執行語句(insert)把當前 namespace 下的所有快取都失效了。

鑒於二級快取存在如上兩個問題,所以在項目中不建議使用 MyBatis 的二級快取。

 

MyBatis 實用篇

MyBatis 概念

MyBatis 示例-簡介

MyBatis 示例-類型處理器

MyBatis 示例-傳遞多個參數

MyBatis 示例-主鍵回填

MyBatis 示例-動態 SQL

MyBatis 示例-聯合查詢

MyBatis 示例-快取

MyBatis 示例-插件

Tags: