MyBatis缓存体系

337 阅读8分钟

MyBatis缓存体系

1.我们想一想MyBatis为什么要用缓存?

首先我们打开百度百科,搜索一波缓存的定义以及工作原理,缓存到底是个啥?

  • 缓存的定义

缓存是指可以进行高速数据交换的存储器,它先于内存与CPU交换数据,因此速率很快。L1 Cache(一级缓存)是CPU第一层高速缓存。内置的L1高速缓存的容量和结构对CPU的性能影响较大,不过高速缓冲存储器均由静态RAM组成,结构较复杂,在CPU管芯面积不能太大的情况下,L1级高速缓存的容量不可能做得太大。一般L1缓存的容量通常在32—256KB。L2 Cache(二级缓存)是CPU的第二层高速缓存,分内部和外部两种芯片。内部的芯片二级缓存运行速率与主频相同,而外部的二级缓存则只有主频的一半。L2高速缓存容量也会影响CPU的性能,原则是越大越好,普通台式机CPU的L2缓存一般为128KB到2MB或者更高,笔记本、服务器和工作站上用CPU的L2高速缓存最高可达1MB-3MB。由于高速缓存的速度越高价格也越贵,故有的计算机系统中设置了两级或多级高速缓存。紧靠内存的一级高速缓存的速度最高,而容量最小,二级高速缓存的容量稍大,速度也稍低

缓存的定义有点抽象,了解下就好

  • 缓存的工作原理

缓存的工作原理是当CPU要读取一个数据时,首先从CPU缓存中查找,找到就立即读取并送给CPU处理;没有找到,就从速率相对较慢的内存中读取并送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。正是这样的读取机制使CPU读取缓存的命中率非常高(大多数CPU可达90%左右),也就是说CPU下一次要读取的数据90%都在CPU缓存中,只有大约10%需要从内存读取。这大大节省了CPU直接读取内存的时间,也使CPU读取数据时基本无需等待。总的来说,CPU读取数据的顺序是先缓存后内存。

简单地说缓存就是把内存当中的一小块区域,把数据加载到这个区域,CPU可以重复利用,减少磁盘IO,减少查找数据的时间。

So,从这一点我们可以理解,MyBatis的缓存也是为了提高查询效率

2.MyBatis的一级缓存

1.在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。

2.一级缓存是 SqlSession级别 的缓存。在操作数据库时需要构造 sqlSession 对象,在对象中有一个(内存区域)数据结构(HashMap)用于存储缓存数据。不同的 sqlSession 之间的缓存数据区域(HashMap)是互相不影响的。

3.一级缓存执行更新或者删除操作,缓存会被清除。

1.接下来说下一级缓存命中的几个条件

缓存命中:就是第一次执行的sql,查询的数据被加载到缓存区,再次执行相同的SQL,直接从内存当中的这块缓存区域读取的数据这一流程。

下面我们用mybatis项目源码test包下的org.apache.ibatis.autoconstructor.AutoConstructorTest来执行演示一级缓存命中的几个条件

1.第一种情况:sql、参数必须相同
@Test
  public void testFirstCache1(){
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      final AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      PrimitiveSubject subject=mapper.getSubject(1);
      PrimitiveSubject subject1=mapper.getSubject(1);
      System.out.println(subject==subject1);
    }
  }
//返回true
2.第二种情况:statementID必须一样
@Test
  public void testFirstCache2(){
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      final AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      PrimitiveSubject subject=mapper.getSubject(1);
      PrimitiveSubject subject1=mapper.getSubject2(1);
      System.out.println(subject==subject1);
    }
  }
//返回false
3.第三种情况:sqlSession必须一样
 @Test
  public void testFirstCache3(){
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      final AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      PrimitiveSubject subject=mapper.getSubject(1);
      PrimitiveSubject subject1=sqlSessionFactory.openSession().getMapper(AutoConstructorMapper.class).getSubject(1);
      System.out.println(subject==subject1);
    }
  }
//返回false
4.第四种情况:RowBounds必须相同
 @Test
  public void testFirstCache4(){
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      final AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      RowBounds rowBounds =  new RowBounds(0,10);
      PrimitiveSubject subject=mapper.getSubject(10);
      PrimitiveSubject subject1= (PrimitiveSubject) sqlSession.selectList("org.apache.ibatis.autoconstructor.AutoConstructorMapper.getSubject",10,rowBounds);
      System.out.println(subject==subject1);
    }
  }
//返回false

2.缓存过期的情况

1.第一种情况:通过同一个SqlSession执行更新操作时,这个更新操作不仅仅指代update操作,还指插入和删除操作
  /**
   * 更新操作
   * @param id
   * @return
   */
  @Update("update subject set age=30 WHERE id = #{id}")
  boolean setSubject(final int id);

  /**
   * 缓存失效:1.更新操作
   */
  @Test
  public void testFirstCache5(){
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      final AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      PrimitiveSubject subject=mapper.getSubject(1);
      //更新操作
      Boolean subject2=mapper.setSubject(1);
      PrimitiveSubject subject3=mapper.getSubject(1);
      System.out.println(subject==subject3);
    }
  }

//返回false
2.第二种情况:事务提交时会删除一级缓存
  /**
   * 缓存失效:2.sqlSession提交
   */
  @Test
  public void testFirstCache6(){
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      final AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      PrimitiveSubject subject=mapper.getSubject(1);
      //sqlSession提交
      sqlSession.commit();
      PrimitiveSubject subject3=mapper.getSubject(1);
      System.out.println(subject==subject3);
    }
  }
  
  //返回false
3.第三种情况:事务回滚时也会删除一级缓存
  /**
   * 缓存失效:3.sqlSession回滚
   */
  @Test
  public void testFirstCache7(){
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      final AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      PrimitiveSubject subject=mapper.getSubject(1);
      //sqlSession回滚
      sqlSession.rollback();
      PrimitiveSubject subject3=mapper.getSubject(1);
      System.out.println(subject==subject3);
    }
  }
  
  //返回false

3.MyBatis集成Spring造成一级缓存失效问题

很多人发现,MyBatis集成Spring造成一级缓存失效,以为是Spring的bug,其实是Spring对SqlSession进行了封装,通过SqlSessionTemplate,使得每次调用Sql,都会重新创建一个SqlSession。一级缓存必须是同一会话才能命中,所以这些场景不能命中。

解决办法:给Spring添加事务即可,添加事务之后,SqlSessionInterceptor(会话拦截器)就会判断两次请求是否在同一事务当中,如果是就会用同一个SqlSession来解决。

3.MyBatis的二级缓存

1.二级缓存是应用级别的缓存,与一级缓存不同的是,它的作用范围是整个应用,并且可以跨线程使用。所以二级缓存有更高的命中率,适合缓存一些较少的数据。在流程上是先访问二级缓存,在访问一级缓存。之所以称之为“二级缓存”,是相对于“一级缓存”而言的。既然有了一级缓存,那么为什么要提供二级缓存呢?我们知道,在一级缓存中,不同session进行相同SQL查询的时候,是查询两次数据库的。显然这是一种浪费,既然SQL查询相同,就没有必要再次查库了,直接利用缓存数据即可,这种思想就是MyBatis二级缓存的初衷。

2.另外,Spring和MyBatis整合时,每次查询之后都要进行关闭sqlsession,关闭之后数据被清空。所以MyBatis和Spring整合之后,一级缓存是没有意义的。如果开启二级缓存,关闭sqlsession后,会把该sqlsession一级缓存中的数据添加到mapper namespace的二级缓存中。这样,缓存在sqlsession关闭之后依然存在。

1.开启二级缓存

在mybatis-config.xml中加入如下配置,开启二级缓存

<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

mapper.xml文件

<cache/>

2.使用二级缓存

  @Test
  public void testFirstCache8(){
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    AutoConstructorMapper mapper1 = sqlSession1.getMapper(AutoConstructorMapper.class);
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    AutoConstructorMapper mapper2 = sqlSession2.getMapper(AutoConstructorMapper.class);
    PrimitiveSubject subject1=mapper1.getSubject(1);
    sqlSession1.commit();
    PrimitiveSubject subject2=mapper2.getSubject(1);
    System.out.println(subject1==subject2);
  }

3.二级缓存清除策略

cache标签下面有下面几种可选项

eviction: 缓存回收策略,支持的策略有下面几种

  • LRU - 最近最少回收,移除最长时间不被使用的对象(默认是这个策略)

  • FIFO - 先进先出,按照缓存进入的顺序来移除它们

  • SOFT - 软引用,移除基于垃圾回收器状态和软引用规则的对象

  • WEAK - 弱引用,更积极的移除基于垃圾收集器和弱引用规则的对象

    flushinterval:缓存刷新间隔,缓存多长时间刷新一次,默认不清空,设置一个毫秒值;

    readOnly: 是否只读;true 只读 ,MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。MyBatis 为了加快获取数据,直接就会将数据在缓存中的引用交给用户。不安全,速度快。读写(默认):MyBatis 觉得数据可能会被修改

    size: 缓存存放多少个元素

    type: 指定自定义缓存的全类名(实现Cache 接口即可)

    blocking:若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。

    cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。

4.二级缓存使用建议

MyBatis的二级缓存实用性不是很大。一个原因就是Spring环境下,一本只有一个SqlSession,不存在sqlSession之间共享缓存;还有就是MyBatis的缓存都不能做到分布式,所以对于MyBatis的二级缓存以了解为主。

总结

一级缓存与二级缓存的不同之处:

  • 1、Mybatis的一级缓存默认开启,而二级缓存默认关闭。
  • 2、Mybatis的一级缓存指的是Mybaits中SqlSession对象的缓存,而二级缓存指的是SqlSessionFactory对象的缓存。一个SqlSessionFactory对象包括多个SqlSession对象。
  • 3、SqlSession对象中存放的是返回数据的对象,而SqlSessionFactory对象中存放的是数据,不是对象。
  • 4、Mybatis和Spring整合的时候,一级缓存与事务有关,而二级缓存与事务无关。