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 文件配置 元素就可以开启缓存了。

MyBatis 二级缓存是基于 namespace 的,缓存的内容是根据 namespace 存放的,可以认为 namespace 就是缓存的 KEY 值 。

这样的一条语句里面,很多设置是默认的,如果我们只是这样配置,那么就意味着:

  • 映射语句文件中的所有 select 语句将会被缓存;
  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存;
  • 缓存会使用默认的 Least Recently Used(LRU,最近最少使用的)算法来收回;
  • 根据时间表,比如 No Flush Interval,(CNFI,没有刷新间隔),缓存不会以任何时间顺序来刷新;
  • 缓存会存储列表集合或对象(无论查询方法返回什么)的1024个引用;
  • 缓存会被视为是 read/write(可读/可写)的缓存,意味着对象检索不是共享的,而且可以安全地被调用者修改,不干扰其他调用者或线程所做的潜在修改。

另外我们还可以通过配置实现多个 namespace 共用同一个二级缓存,即同一个 Cache 对象。

MyBatis 示例-缓存_第1张图片

 

如上图所示,namespace2 共用了 namespace1 的 Cache 对象。

二级缓存可以和一级缓存共存,通过下图来理解 MyBatis 的两层缓存结构。

MyBatis 示例-缓存_第2张图片

 

 

当应用程序通过 SqlSession2 执行定义在命名空间 namespace2 中的查询操作时,SqlSession2 首先到 namespace2 对应的二级缓存中查找是否缓存了相应的结果对象。如果没有,则继续到 SqlSession2 对应的一级缓存中查找是否缓存了相应的结果对象,如果依然没有,则访问数据库获取结果集并映射成结果对象返回。 最后,该结果对象会记录到 SqlSession 对应的一级缓存以及 namespace2 对应的二级缓存中,等待后续使用。另外需要注意的是,上图中的命名空间 namespace2 和 namespace3 共享了同一个二级缓存对象,所以通过 SqlSession3 执行命名空间 namespace3 中的完全相同的查询操作(只要该查询生成的 CacheKey 对象与上述 SqlSession2 中的查询生成 CacheKey 对象相同即可)时,可以直接从二级缓存中得到相应的结果对象。 

案例:

我们通过案例测试一下二级缓存,首先实体类必须实现 Serializable 接口,在 StudentMapper 文件中添加如下配置:


测试方法:

/**
 * 二级缓存
 */
@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 执行是从缓存中获取的数据。

二级缓存详细配置介绍:

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

你可能感兴趣的:(MyBatis 示例-缓存)