Mybatis应用篇(三): 缓存

219 阅读4分钟

前言

Mybatis 提供了一个非常强大的查询缓存特性 ,可以非常方便地配置和使用。合理利用缓存,可以极大地提升查询效率,但同样地不合理使用缓存可能出现数据不一致的问题。今天我们就来谈一谈如何使用缓存。


缓存机制

mybatis 分为两级缓存:一级缓存 , 二级缓存

一级缓存二级缓存
默认设置开启关闭
作用域SqlSession 级别全局(跨 SqlSession共享)
作用范围全局具体的mapper
  • 缓存 key

mybatis的查询缓存存储的类型是 HashMap的结构,结果集当map, 那么key是什么呢?

缓存key = statementId (比如xxx.mapper.select) + offset + limit + sql + param

缓存流程图

流程图.png

一级缓存

**默认开启 **

开启缓存

不在一个事务里,缓存失效

image.png

从上图,我们明显地看到,虽然两次查询条件一样,但第二次查询时依然发生了数据库查询操作,因此未使用缓存

在同一个事务里,缓存起作用

image.png

可以很明显的看到,第二次没有执行数据库操作,直接返回对象了。当然了,也可能通过断点的方式,在执行完第一次之后进入断点,此时手动修改数据库中的值,此时再执行第二次查询。这时候会发现,第二次查询出来的结果跟第一次的一样,跟数据库中的不一致,这就是我们前面说的可能存在的问题,二级缓存同样有这个问题,所以我们要根据业务场景合理使用。

关闭缓存

设置 mybatis.configuration.local-cache-scope: statement 及缓存为语句级别,即可让一级缓存失效

即使在同一个事务里,缓存依然不起作用

不在同一个事务里,开启缓存时不能使用缓存,关闭的时候肯定也不能使用缓存。这里就不再贴那种情况的测试情况了。

image.png

二级缓存

二级缓存是指不同 sqlSession之间共享查询结果集,默认情况下是关闭的。

开启方式:

设置 mybatis.configuration.cache-enabled: true 全局开启

在对应的mapper文件中,添加 <cache/> 标签

在实测中,只要在 mapper文件中添加了 <cache/> 标签即可直接开启对应的二级缓存。

二级缓存需要两个sqlSession的来同时查询,来观察两个 sqlSession 是否可能用到缓存。

@Test
void testTwoLevelCache(){
   countDownLatch = new CountDownLatch(2);
   Thread t = new Thread(() -> {
      this.selectEO();
      countDownLatch.countDown();
   });

   Thread t1 = new Thread(() -> {
      this.selectEO2();
      countDownLatch.countDown();
   });
   t.start();
   t1.start();
   try {
      countDownLatch.await();
   } catch (InterruptedException e) {
      e.printStackTrace();
   }
   System.out.println("全部执行完毕");
}
    @Test
   @Transactional
   void selectEO() {
//    Page<Object> page = PageMethod.startPage(1, 10);
      StudentEO studentEO = studentMapper.selectEO(2L);
//    System.out.println("总记录数 " + page.getTotal());
      System.out.println(JSON.toJSONString(studentEO));
      StudentEO studentEO2 = studentMapper.selectEO(2L);
      System.out.println(JSON.toJSONString(studentEO2));

//    StudentDO studentDO = new StudentDO();
//    studentDO.setId(2L);
//    studentDO.setAddress("地址999");
//    studentMapper.updateStudentById(studentDO);
//    StudentEO studentEO3 = studentMapper.selectEO(2L);
//    System.out.println(JSON.toJSONString(studentEO3));
      log.info("完成");
   }
@Test
   @Transactional
   void selectEO2() {
//    Page<Object> page = PageMethod.startPage(1, 10);
      StudentEO studentEO = studentMapper.selectEO(2L);
//    System.out.println("总记录数 " + page.getTotal());
      System.out.println(JSON.toJSONString(studentEO));
      studentEO.setAge(13);
      StudentEO studentEO2 = studentMapper.selectEO(2L);
      System.out.println(JSON.toJSONString(studentEO2));
//    StudentDO studentDO = new StudentDO();
//    studentDO.setId(2L);
//    studentDO.setAddress("上海市浦东新区999");
//    studentMapper.updateStudentById(studentDO);
//    StudentEO studentEO3 = studentMapper.selectEO(2L);
//    System.out.println(JSON.toJSONString(studentEO3));
      log.info("完成");
   }

我们在 t 线程查询到数据,完成缓存后,让 t卡在当前线程,然后去t1线程再执行查询,看此时t1是去查数据库 还是去查缓存。

  • t 完成查询 ,进入断点等待

image.png

  • 切换到 t1 , 执行查询 ,命中缓存

image.png

从日志中,我们可以明显发现,没有再往数据库中去查询,直接命中缓存,且命中几率为:0.5

到此,我们已经验证了,在开启二级缓存的情况下,不同事务的sqlsession 会用到缓存

推荐配置

官方推荐的配置

<cache eviction="FIFO"
       flushInterval = "60000"
       size="512"
       readOnly="false"
/>

eviction=“FIFO”:缓存回收策略

  • LRU – 最近最少使用的:移除最长时间不被使用的对象。
  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
  • SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
  • WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
  • 默认的是 LRU。

flushInterval:刷新间隔,单位毫秒

默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。

size:引用数目,正整数

代表缓存最多可以存储多少个对象,太大容易导致内存溢出。

readOnly:只读,true/false

  • true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。速度快,性能有优势,但是不安全。
  • false:读写缓存;会返回缓存对象的拷贝(通过序列化)。速度慢,但是安全,因此默认是 false。

我们重点讲一下这个 readOnly 默认是 false ,这是为了安全的保证来考虑的。每次获取的结果都是一个新对象,如果是 true的话,实际上每次获取的都是同一个对象。

接下来,我们进行验证:

  • readOnly="false"

image.png

studentEO 对象的地址是:@7596

image.png

studentEO 对象的地址是:@7569

可以看到,查询返回的对象虽然属性值是一样的,但对象地址不一样,也就是新对象。

  • readOnly="true"

image.png

studentEO 对象的地址是:@7546

image.png

studentEO 对象的地址是:@7546

可以看到,这时候缓存返回的是同一个对象

缓存失效

在mybatis中执行 update delete 语句时会导致缓存失效