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 實用篇