MyBatis 示例-緩存

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 示例-插件

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章