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 文件配置
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 的二级缓存。
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/de0i7i 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。