Mybatis的缓存机制

为什么要使用缓存?

在计算机的世界里面缓存无处不在,操作系统有操作系统的缓存、数据库有数据库的缓存,各种中间件如Redis也用来充当缓存的作用,编程语言又利用内存作为缓存。

计算机CPU的处理速度可谓是一马当先,远远甩开了其他操作,尤其是I/O操作,除了那种CPU密集型的系统,其余大部分的业务系统性能瓶颈最后或多或少都会出现在I/O操作上,所以为了减少磁盘的I/O次数,那么缓存是必不可少的,通过缓存的使用我们可以大大减少I/O操作次数,从而在一定程度上弥补了I/O操作和CPU处理速度之间的鸿沟。而在我们ORM框架中引入缓存的目的就是为了减少读取数据库的次数,从而提升查询的效率。

那mybatis作为一款优秀的ORM框架,也不会少了缓存机制,那作为框架的使用者,我们有必要了解一下mybatis的缓存是如何实现的。

mybatis的缓存

mybatis的缓存相关类都在cache包里面,在包里定义了一个顶级接口Cache,这个接口默认有一个实现类PerpetualCache,而PerpetualCache中是维护了一个HashMap来实现缓存。

这里需要注意的是,虽然decorators包下的类也实现了Cache接口,那为什么还要说Cache接口只有一个默认实现类呢?
其实从包名decorator就可以得知,这个包里的类全都是装饰器,也就是说这是装饰器模式的实现。

随意打开一个装饰器,可以最终都是调用delegate来实现,只是将部分功能做了增强,其本身都需要依赖Cache的唯一实现类PerpetualCache(因为装饰器内需要传入Cache对象,故而只能传入PerpetualCache对象,因为接口是无法直接new出来传进去的)。

在mybatis中,缓存分为一级缓存和二级缓存

一级缓存

一级缓存也叫本地缓存,在mybatis中,一级缓存是在会话(sqlSession)层面实现的,这也表明一级缓存只能在同一个会话中有效,跨sqlSession是无效的。

myBatis中一级缓存是默认开启的,不需要任何配置。

我们先来看一个例子验证一下一级缓存是不是真的存在,作用范围又是不是真的只是对同一个SqlSession有效。

@Test
  public void test(){
    try {
      //1.
      InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
      //2.
      SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
      SqlSessionFactory factory = builder.build(in);
      //3.
      SqlSession sqlSession = factory.openSession();
      //4.
      //UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
      //List<User> users = userMapper.findAll();
      //users.forEach(System.out::println);

      //String s = "loong.mapper.UserMapper.findAll";
      //List<Object> objects = sqlSession.selectList(s);
      //objects.forEach(System.out::println);

      //UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
      //List<User> page = userMapper.findByPage(1, 2);
      //page.forEach(System.out::println);

      String s = "loong.mapper.UserMapper.findByPage";
      Map<String,Integer> paraMap = new HashMap<>();
      paraMap.put("curPage",  2);
      paraMap.put("pageSize", 2);
      List<Object> list = sqlSession.selectList(s, paraMap);

      System.out.println("================第一次查询的结果=================");
      list.forEach(System.out::println);
      System.out.println("================同一个sqlSession做第二次相同的查询=================");
      List<Object> page = sqlSession.selectList(s, paraMap);
      page.forEach(System.out::println);

      System.out.println(list == page);//true,是同一个对象

      System.out.println("================不同的sqlSession做相同的查询=================");
      SqlSession sqlSession1 = factory.openSession();
      List<Object> page1 = sqlSession1.selectList(s, paraMap);
      page1.forEach(System.out::println);
      //false,不同的sqlSession做相同的查询得到的对象不同
      System.out.println(page1 == page);

      System.out.println("================再查一次=================");
      List<Object> page2 = sqlSession1.selectList(s, paraMap);
      page2.forEach(System.out::println);
      //true,相同的sqlSession对象做相同的查询查出的对象是同一个
      System.out.println(page1 == page2);

      sqlSession.close();
      sqlSession1.close();
      in.close();
    } catch (IOException e) {
      e.printStackTrace()
    }
  }

一级缓存的刷新

清空sqlSession的所有缓存,而不仅仅是只清空某个键和值。

@Test
  public void clearCache(){
    try {
      //1.
      InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
      //2.
      SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
      SqlSessionFactory factory = builder.build(in);
      //3.
      SqlSession sqlSession = factory.openSession();

      UserMapper mapper = sqlSession.getMapper(UserMapper.class);
      List<User> users = mapper.findAll();
      System.out.println("=====第一次查询=====");
      users.forEach(System.out::println);
      System.out.println("=====同一个sqlSession相同的第二次查询=====");
      List<User> list = mapper.findAll();
      list.forEach(System.out::println);
      //因为mybatis一级缓存的存在,所以结果是true
      //当在Mapper配置刷新缓存(flushCache=true)后,结果将是false
      System.out.println("是否同一个对象" + (users == list));
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

mybatis一级缓存总结

  1. 在同一个 SqlSession 中, Mybatis 会把执行的方法和参数通过算法生成缓存的键值, 将键值和结果存放在一个 Map 中, 如果后续的键值一样, 则直接从 Map 中获取数据

  2. 不同的 SqlSession 之间的缓存是相互隔离的;

  3. 用一个 SqlSession, 可以通过配置使得在查询前清空缓存;

  4. 任何的 UPDATE, INSERT, DELETE 语句都会清空缓存。

二级缓存

一级缓存因为只能在同一个SqlSession中共享,所以会存在一个问题,在分布式或者多线程的环境下,不同会话之间对于相同的数据可能会产生不同的结果,因为跨会话修改了数据是不能互相感知的,所以就有可能存在脏数据的问题,正因为一级缓存存在这种不足,所以我们需要一种作用域更大的缓存,这就是二级缓存。

二级缓存的作用范围

一级缓存的作用域是在同一个sqlSession级别,所以它是存储在SqlSession中的BaseExecutor之中。

二级缓存设计的目的就是要使缓存的作用范围更广,就肯定是要实现跨会话共享的,在Mybatis中二级缓存的作用域是namespace,即作用范围在同一个命名空间。

在MyBatis中为了实现二级缓存,专门用了一个装饰器来维护:CachingExecutor。

使用二级缓存

二级缓存的配置有3个地方:

  1. mybatis-config中有一个全局配置属性,这个不配置也行,因为默认就是true
<settings>
	<!--开启Mybatis的缓存机制-->
    <setting name="cacheEnabled" value="true"/>
  </settings>
  1. 在Mapper映射文件内需要配置缓存标签:
<!--开启二级缓存-->
  <cache/>
  或者
  <cache-ref namespace="loong.mapper.UserMapper"/>

以上配置第1点是默认开启的,也就是说我们只要配置第2点就可以打开二级缓存了,当我们需要针对某一条语句来配置二级缓存时候则可以使用下面的第3点。

  1. 在select查询语句标签上配置useCache属性,如下:
<select id="findByPage" resultMap="userMap" useCache="true">
    select * from student limit ${(curPage - 1)*pageSize}, #{pageSize}
  </select>

注意事项

  1. 需要commit事务或关闭会话之后才会生效
  2. 如果使用的是默认缓存,那么结果集对象需要实现序列化接口(Serializable)

验证二级缓存的存在

@Test
  public void testSecondLevelCache(){
    try {
      //1.
      InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
      //2.
      SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
      SqlSessionFactory factory = builder.build(in);
      //3.
      SqlSession sqlSession1 = factory.openSession();
      SqlSession sqlSession2 = factory.openSession();

      UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
      List<User> page1 = mapper1.findByPage(1, 3);
      page1.forEach(System.out::println);
      page1.get(0).setName("这是改过的名字");
      sqlSession1.commit();

      //这个会话会从二级缓存去取数据
      UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
      List<User> page2 = mapper2.findByPage(1, 3);
      page2.forEach(System.out::println);
      sqlSession2.commit();

      sqlSession2.close();
      sqlSession1.close();
      in.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

二级缓存的刷新

@Test
  public void testSecondLevelCache(){
    try {
      //1.
      InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
      //2.
      SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
      SqlSessionFactory factory = builder.build(in);
      //3.
      SqlSession sqlSession1 = factory.openSession();
      SqlSession sqlSession2 = factory.openSession();
      SqlSession sqlSession3 = factory.openSession();

      UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
      List<User> page1 = mapper1.findByPage(1, 3);
      page1.forEach(System.out::println);
      page1.get(0).setName("这是改过的名字");
      sqlSession1.commit();

      //这个会话会从二级缓存去取数据
      UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
      List<User> page2 = mapper2.findByPage(1, 3);
      page2.forEach(System.out::println);
      sqlSession2.commit();

      //sqlSession3执行删除操作
      UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
      int i = mapper3.deleteById(1);
      System.out.println(i);
      sqlSession3.commit();
      sqlSession3.close();

      //sqlSession2去做上次相同的查询
      List<User> users = mapper2.findByPage(1,3);
      users.forEach(System.out::println);
      sqlSession2.commit();

      sqlSession2.close();
      sqlSession1.close();
      in.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

注意需要在执行修改操作后把该sqlSession关闭二级缓存才会刷新

二级缓存实现的原理

肝不动了...

二级缓存应该开启吗

既然一级缓存默认是开启的,而二级缓存是需要我们手动开启的,那么我们什么时候应该开启二级缓存呢?

  1. 因为所有的update操作(insert,delete,uptede)都会触发缓存的刷新,从而导致二级缓存失效,所以二级缓存适合在读多写少的场景中开启。

  2. 因为二级缓存针对的是同一个namespace,所以建议是在单表操作的Mapper中使用,或者是在相关表的Mapper文件中共享同一个缓存。

一级缓存可能存在脏读情况,那么二级缓存是否也可能存在呢?

是的,默认的二级缓存毕竟也是存储在本地缓存,所以对于微服务下是可能出现脏读的情况的,所以这时候我们可能会需要自定义缓存,比如利用redis来存储缓存,而不是存储在本地内存当中。

未完待续。。。

posted @ 2021-06-03 18:22  codeloong  阅读(235)  评论(0)    收藏  举报