Mybatis引出的一系列问题-Mybatis缓存机制的探究

Mybatis 使用到了两种缓存:本地缓存(local cache)和二级缓存(second level cache)。

一级缓存默认是开启的,而且不能关闭,MyBatis的一些关键特性(例如通过<association><collection>建立级联映射、避免循环引用(circular references)、加速重复嵌套查询等)都是基于MyBatis一级缓存实现的,而且MyBatis结果集映射相关代码重度依赖CacheKey,所以目前MyBatis不支持关闭一级缓存。

虽然我们不能关闭一级缓存,但是我们可以更改他的作用范围: MyBatis提供了一个配置参数localCacheScope,用于控制一级缓存的级别,该参数的取值为SESSIONSTATEMENT当指定localCacheScope参数值为SESSION时,缓存对整个SqlSession有效,只有执行DML语句(更新语句)时,缓存才会被清除当localCacheScope值为STATEMENT时,缓存仅对当前执行的语句有效,当语句执行完毕后,缓存就会被清空

<settings>
  <setting name="localCacheScope" value="STATEMENT"/>
</settings>

能更改一级缓存的作用范围这一点很重要后面我们讲解中会用到这一特性。

一级缓存默认是SqlSession级别的: 在操作数据库时需要构造 sqlSession 对象,在对象中有一个(内存区域)数据结构(HashMap)用于存储缓存数据。不同的 sqlSession 之间的缓存数据区域(HashMap)是互相不影响的。用一张图来表示一下一级缓存,其中每一个 SqlSession 的内部都会有一个一级缓存对象。
在这里插入图片描述
1、一级缓存同一个会话共享数据 模拟思路:打开一个会话,进行两次查询通过日志查看第二次是否走数据库。

  @Test
   public void testSession() throws IOException {
    
    
       try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
    
    
           OrderMapper orderMapper = sqlSession.getMapper(OrderMapper.class);
           List<Order> orders = orderMapper.queryById(620898339119480832L);
           System.out.println("第一次查询:" + JSON.toJSONString(orders));
           OrderMapper orderMapper2 = sqlSession.getMapper(OrderMapper.class);
           List<Order> orders2 = orderMapper2.queryById(620898339119480832L);
           System.out.println("第二次查询:" + JSON.toJSONString(orders2));
      }
  }

在这里插入图片描述
分析:第一次查询打印了 sql 日志信息,说明是通过数据库获取到数据,第二次也查询到数据但是没有打印日志信息,说明走了缓存。
结论:一级缓存同一个会话共享数据。

2、同一个会话如果有更新操作则缓存清除 模拟思路:打开一个会话,先进行查询,然后进行更新操作,最后再次查询刚才的语句,看是否打印查询数据库的 sql 日志(这些操作都是对一条数据的操作)。

@Test
public void testUpdate() throws IOException {
    
    
  try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
    
    
      // 同一个会话 第一次查询
      System.out.println("第一次会会话的 第一次查询");
      OrderMapper orderMapper = sqlSession.getMapper(OrderMapper.class);
      List<Order> orders = orderMapper.queryById(620898339119480832L);
      Order order = orders.get(0);
      order.setAmount(12L);
      // 进行更新
      orderMapper.updateByPrimaryKey(order);
      // 同一个会话 第二次查询
      System.out.println("第一次会会话的 第二次查询");
      List<Order> orders2 = orderMapper.queryById(620898339119480832L);
      System.out.println(JSON.toJSONString(orders2));
  }
}

在这里插入图片描述
分析:第一次查询打印了 sql 日志,然后进行数据更新,最后进行第二次查询发现仍旧查询数据库,说明缓存已经失效。
结论:同一个会话如果有更新操作则缓存清除。

3、导致脏数据 模拟思路:打开两个会话,第一个会话查询数据库获取数据后 ,接着第二个会话修改数据,最后第一个会话再查询数据,那最后这次查询如果和第一次查询相同,那说明一级缓存会导致脏数据问题。

@Test
   public void testDirtyData() throws IOException {
    
    
       SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
       SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
       try {
    
    
           // 同一个会话 第一次查询
           OrderMapper orderMapper1 = sqlSession1.getMapper(OrderMapper.class);
           OrderMapper orderMapper2 = sqlSession2.getMapper(OrderMapper.class);
           List<Order> orders = orderMapper1.queryById(620898339119480832L);
           System.out.println("第一次会会话第一次查询的结果" + orders);
           Order order1 = getOrder(orders);
           System.out.println("=========更新数据======");
           orderMapper2.updateByPrimaryKey(order1);
           sqlSession2.commit();
           List<Order> orders1 =  orderMapper1.queryById(620898339119480832L);
           System.out.println("第二次查询:"+JSON.toJSONString(orders1));
 
      }catch (Exception e){
    
    
           sqlSession1.close();
           sqlSession2.close();
      }
  }

在这里插入图片描述
分析:第一个会话第一次查询 amount 值是1212,第二会话将 amount 更改为666,第一个会话再次查询数据获取的 amount 值为1212,这说明第一个会话的第二次查询命中缓存导致了脏数据问题。
结论:一级缓存在多会话中会导致脏数据。
解决方式:在配置一级缓存作用范围的时候将其设置为 STATEMENT,那么缓存仅对当前执行的语句有效,当语句执行完毕后,缓存就会被清空。

<settings>
  <setting name="localCacheScope" value="STATEMENT"/>
</settings>

你可以随时调用以下方法来清空本地缓存:

void clearCache()

二级缓存: 二级缓存的原理和一级缓存原理一样,第一次查询,会将数据放入缓存中,然后第二次查询则会直接去缓存中取。但是一级缓存是基于sqlSession的,而二级缓存是基于mapper文件的namespace的,也就是说多个sqlSession可以共享一个mapper中的二级缓存区域,并且如果两个mapper的namespace 相同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中。
在这里插入图片描述
和一级缓存默认开启不一样,二级缓存需要我们手动开启首先在全局配置文件sqlMapConfig.xml文件中加入如下代码:

<!--开启二级缓存-->

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

其次在UserMapper.xml文件中开启缓存(如果我们是基于注解形式进行查询,可以在mapper查询接口上添加@CacheNamespace注解开启二级缓存)

方式一:在xml文件中配置
<!--开启二级缓存-->
<cache></cache>

方式二:在mapper接口上添加上
@CacheNamespace
后面可以带参数指定加载自定义的缓存实现类
@CacheNamespace(implementation = RedisCache.class)//开启二级缓存

我们可以看到mapper.xml文件中就这么一个空标签,其实这里可以配置,PerpetualCache这个类是mybatis默认实现缓存功能的类。我们不写type就使用mybatis默认的缓存,也可以去实现Cache接口来自定义缓存。
在这里插入图片描述

public class PerpetualCache implements Cache {
    
    
    private final String id;
    private MapcObject, Object> cache = new HashMapC);
    public PerpetualCache(St ring id) {
    
     this.id = id;
}

我们可以看到二级缓存底层还是HashMap结构。

public class User implements Serializable(
    //⽤户ID
    private int id;
    //⽤户姓名
    private String username;
    //⽤户性别
    private String sex;
}

注意:开启了二级缓存后,还需要将要缓存的pojo实现Serializable接口,为了将缓存数据取出执行反序列化操作,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中,如果我们要再取这个缓存的话,就需要反序列化了。所以mybatis中的pojo都去实现Serializable接口

测试⼆级缓存和sqlSession无关,可以看出下面两个不同的sqlSession,第一个关闭了,第二次查询依然不发出sql查询语句。

@Test
public void testTwoCache(){
    
    
    //根据 sqlSessionFactory 产⽣ session
    SqlSession sqlSession1 = sessionFactory.openSession();
    SqlSession sqlSession2 = sessionFactory.openSession();
    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper. class );
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper. class );
    //第⼀次查询,发出sql语句,并将查询的结果放⼊缓存中
    User u1 = userMapper1.selectUserByUserId(1);
    System.out.println(u1);
    sqlSession1.close(); //第⼀次查询完后关闭 sqlSession ,sqlsession关闭后就会清除一级缓存
    //第⼆次查询,即使sqlSession1已经关闭了,这次查询依然不发出sql语句
    User u2 = userMapper2.selectUserByUserId(1);
    System.out.println(u2);
    sqlSession2.close();
}

测试执行commit()操作,二级缓存数据清空

@Test
public void testTwoCache(){
    
    
    //根据 sqlSessionFactory 产⽣ session
    SqlSession sqlSession1 = sessionFactory.openSession();
    SqlSession sqlSession2 = sessionFactory.openSession();
    SqlSession sqlSession3 = sessionFactory.openSession();
    String statement = "com.lagou.pojo.UserMapper.selectUserByUserld" ;
    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper. class );
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper. class );
    UserMapper userMapper3 = sqlSession2.getMapper(UserMapper. class );
    
    //第⼀次查询,发出sql语句,并将查询的结果放⼊缓存中
    User u1 = userMapperl.selectUserByUserId( 1 );
    System.out.println(u1);
    sqlSessionl .close(); //第⼀次查询完后关闭sqlSession
    
    //执⾏更新操作,commit()
    u1.setUsername( "aaa" );
    userMapper3.updateUserByUserId(u1);
    sqlSession3.commit();
 
    //第⼆次查询,由于上次更新操作,缓存数据已经清空(防⽌数据脏读),这⾥必须再次发出sql语
    User u2 = userMapper2.selectUserByUserId( 1 );
    System.out.println(u2);
    sqlSession2.close();
}

在这里插入图片描述
mybatis中还可以配置userCacheflushCache等配置项,userCache是用来设置是否禁用二级缓存的,在statement中设置useCache=false可以禁⽤当前select语句的二级缓存,即每次查询都会发出sql去查询,默认情况是true,即该sql使用二级缓存,还可以使用注解方式禁用@Options(useCache=false),flushCache的注解配置方法同上 。

方法一:
<select id="selectUserByUserId" useCache="false" resultType="com.lagou.pojo.User" parameterType="int">
     select * from user where id=#{
    
    id}
</select> 

方法二:
@Options(useCache = false)
@Select({
    
    "select * from user where id = #{id}"})
public User findUserById(Integer id);

这种情况是针对每次查询都需要最新的数据sql,要设置成useCache=false,禁用二级缓存,直接从数据库中获取。

在mapper的同一个namespace中,如果有其它insert、update, delete操作数据后需要刷新缓存,如果不执行刷新缓存会出现脏读。

设置statement配置中的flushCache="true”属性,默认情况下为true,即刷新缓存,如果改成false则 不会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。

一般下执行完commit操作都需要刷新缓存,flushCache=true表示刷新缓存,这样可以避免数据库脏读。所以我们不用设置,默认即可。

一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中,如果当前会话关闭了,这个会话对应的一级缓存就没了;但是我们想要的是,会话关闭了,一级缓存中的数据被保存到二级缓存中,新的会话查询信息,就可以从二级缓存中获取内容,同的mapper查出的数据会放在自己对应的缓存(map)中。

  1. 只要开启了二级缓存,我们在同一个Mapper【namespace】中的查询,可以在二级缓存中拿到数据
  2. 查出的数据都会被默认先放在一级缓存中
  3. 只有会话提交或者关闭以后,一级缓存中的数据才会转到二级缓存中

缓存原理:

  1. 用户先查二级缓存
  2. 再查询一级缓存
  3. 最后查询数据库

猜你喜欢

转载自blog.csdn.net/zs18753479279/article/details/132081455