带你学习Mybatis之mybatis缓存机制

mybatis缓存机制

mybatis包含缓存机制,可以方便的配置和定制。

默认定义了一级缓存和二级缓存。

  • 默认情况下,只有一级缓存开启(sqlSession级别的缓存,也称本地缓存)
  • 二级缓存需要手动开启和配置,是基于namespace级别的缓存(全局缓存)
  • 为了提高扩展性。Mybatis定义了缓存接口Cache,可以通过实现Cache接口来自定义二级缓存

根据ExecutorType的不同来创建不同的执行器

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  Executor executor;
  if (ExecutorType.BATCH == executorType) {
    executor = new BatchExecutor(this, transaction);
  } else if (ExecutorType.REUSE == executorType) {
    executor = new ReuseExecutor(this, transaction);
  } else {
    executor = new SimpleExecutor(this, transaction);
  }
  // 开启二级缓存
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

一级缓存

一级缓存是sqlSession级别的缓存,默认开启,在同一次数据库会话期间查询到的数据会放在本地缓存中,以后获取相同的数据,只需要从缓存中取,没必要查数据库,减少数据库的访问,在commit时,会清空sqlSession的缓存

sqlSession中有一个HashMap,不同sqlSession键缓存数据互相不影响

在参数和sql完全相同的情况下,使用同一个sqlSession对象调用,就可以直接从一级缓存中获取

如何判断两次查询相同

根据cacheKey是否相同来进行判断

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  CacheKey cacheKey = new CacheKey();
  // MappedStatement的id
  cacheKey.update(ms.getId());
  // 分页参数
  cacheKey.update(rowBounds.getOffset());
  cacheKey.update(rowBounds.getLimit());
  // sql语句
  cacheKey.update(boundSql.getSql());
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
  // 所传入的参数
  for (ParameterMapping parameterMapping : parameterMappings) {
    if (parameterMapping.getMode() != ParameterMode.OUT) {
      Object value;
      String propertyName = parameterMapping.getProperty();
      if (boundSql.hasAdditionalParameter(propertyName)) {
        value = boundSql.getAdditionalParameter(propertyName);
      } else if (parameterObject == null) {
        value = null;
      } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
        value = parameterObject;
      } else {
        MetaObject metaObject = configuration.newMetaObject(parameterObject);
        value = metaObject.getValue(propertyName);
      }
      cacheKey.update(value);
    }
  }
  // 配置的环境
  if (configuration.getEnvironment() != null) {
    // issue #176
    cacheKey.update(configuration.getEnvironment().getId());
  }
  return cacheKey;
}
清空一级缓存的方式
public void commit(boolean required) throws SQLException {
  if (closed) {
    throw new ExecutorException("Cannot commit, transaction is already closed");
  }
  // 清除一级缓存
  clearLocalCache();
  flushStatements();
  if (required) {
    transaction.commit();
  }
}


  public void close(boolean forceRollback) {
    try {
      try {
        rollback(forceRollback);
      } finally {
        if (transaction != null) {
          transaction.close();
        }
      }
    } catch (SQLException e) {
      // Ignore. There's nothing that can be done at this point.
      log.warn("Unexpected exception on closing transaction.  Cause: " + e);
    } finally {
      transaction = null;
      deferredLoads = null;
      // 清空以及缓存
      localCache = null;
      localOutputParameterCache = null;
      closed = true;
    }
  }

// update、insert、delete、commit方法都会调用清空缓存
public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }
一级缓存失效的情况
  • sqlSession不同
  • sqlSession相同,查询条件不同(此时该数据在一级缓存中还没有)
  • sqlSession相同,但是在两次查询之间执行了增删改操作(这次增删改可能会对当前数据有影响)
  • sqlSession相同,手动清除了一级缓存 session.clearCache()

二级缓存

二级缓存是Mapper级别的(或者说是namespace级别的),一个namespace对应一个二级缓存,不同namespace查出的数据会放在不同的map中,namespace级别的缓存,可以跨sqlSession进行共享

开启二级缓存后,会使用CacheExecutor来装饰Executor,在查询数据时,先查询二级缓存,二级缓存没有再去查一级缓存

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  Cache cache = ms.getCache();
  if (cache != null) {
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, boundSql);
      @SuppressWarnings("unchecked")
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  // 没有在二级缓存中查到数据
  //  delegate是在构建CachingExecutor时,传过来的执行器,new CachingExecutor(executor);
  // 将会进行查询一级缓存
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
二级缓存的使用

开启全局二级缓存配置

<settings>
  <!-- 开启二级缓存,默认为true -->
  <setting name="cacheEnabled" value="true"/>
</settings>

在映射文件中配置使用二级缓存

<!--
    eviction: 缓存回收策略
        - LRU  最近最少使用:移除最长时间不被使用的,默认
        - FIFO  先进先出,按照对象进入缓存的顺序移除
        - SOFT  软引用,移除基于垃圾回收器状态和软引用规则的对象
        - WEAK  弱引用,积极地移除基于垃圾收集器状态和弱引用规则的对象

     flushInterval: 缓存刷新间隔
        缓存多长时间清空一次,默认不清空,单位毫秒
     readOnly   默认false
        - true  只读,mybatis认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。mybatis为了加快获取速度,
                直接将数据在缓存中的引用交给用户,速度快,但是不安全
        - false 非只读,mybatis会认为获取到的数据可能会被修改,会利用序列化和反序列化机制克隆一份新的数据
     size: 缓存多少元素
     type: 指定自定义缓存的全类名,需要实现Cache接口
		 blocking: 若缓存中找不到对应的key,是否会一直阻塞,知道对应的数据进入缓存
 -->
<cache eviction="FIFO" flushInterval="60000" readOnly="true" size="1024"/>

<!-- 可以使用useCache=false来禁用二级缓存,默认是true -->
<select id="findOrderList" resultMap="baseMap" useCache="false">

由于可能会用到序列化和反序列化,所以使用缓存的对象要实现序列化接口(readOnly为false的时候需要用到序列化和反序列化)

否则会报java.io.NotSerializableException异常

注意:一定要在同一个sqlSessionFactory下的不同sqlSession下使用二级缓存,如果为不同的sqlSessionFactory,永远不可能命中二级缓存的(我测试的时候就犯糊涂了,找了半天配置的问题才反应过来)

@Test
public void testTwoLevelCache(){
    SqlSession session = sqlSessionFactory.openSession();
    // mybatis为接口创建代理对象
    UserMapper userMapper = session.getMapper(UserMapper.class);
    User user = userMapper.selectUser(8);
    System.out.println(user);
    userMapper.updateUser(user);
    session.close();
    SqlSession session1 = sqlSessionFactory.openSession();

    UserMapper userMapper1 = session1.getMapper(UserMapper.class);
    User user1 = userMapper1.selectUser(8);
    System.out.println(user1);
    System.out.println(user == user1);

    session1.close();
}
二级缓存失效的情况
  • 如果第一个sqlSession没有提交,第二个sqlSeesion是无法命中二级缓存中该数据的,(sqlSession提交的时候才会将数据存入到二级缓存)
  • 两次查询之间包含了增删改操作(在增删改操作时默认会刷新缓存,导致缓存失效)
自定义缓存

实现 org.apache.ibatis.cache.Cache 接口

public interface Cache {

  String getId();

  
  void putObject(Object key, Object value);

  
  Object getObject(Object key);

  
  Object removeObject(Object key);

  
  void clear();

  
  int getSize();

  
  default ReadWriteLock getReadWriteLock() {
    return null;
  }

}

以FifoCache为例

public class FifoCache implements Cache {

  private final Cache delegate;
  private final Deque<Object> keyList;
  private int size;

  public FifoCache(Cache delegate) {
    this.delegate = delegate;
    this.keyList = new LinkedList<>();
    this.size = 1024;
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    return delegate.getSize();
  }

  public void setSize(int size) {
    this.size = size;
  }

  @Override
  public void putObject(Object key, Object value) {
    cycleKeyList(key);
    delegate.putObject(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return delegate.getObject(key);
  }

  @Override
  public Object removeObject(Object key) {
    return delegate.removeObject(key);
  }

  @Override
  public void clear() {
    delegate.clear();
    keyList.clear();
  }

  private void cycleKeyList(Object key) {
    keyList.addLast(key);
    if (keyList.size() > size) {
      Object oldestKey = keyList.removeFirst();
      delegate.removeObject(oldestKey);
    }
  }

}

缓存相关配置总结

  • 全局配置文件settings中配置 cacheEnabled=true 该配置只影响二级缓存,对于一级缓存没有影响

  • 每个select标签都有useCache=“true” 默认为true,该配置只影响二级缓存,对于一级缓存没有影响

  • 每个增删改标签都有flushCache=“true”,增删改操作执行后清除缓存,该清除会清除一级和二级缓存,默认true

    如果在select上使用flushCache=“true”,则查询不会使用缓存,默认false

  • sqlSession.clearCache() 只是清除一级缓存,不会清除二级缓存

  • 全局配置文件settings中配置localCacheScope 本地缓存作用域(只针对一级缓存),有两个取值SESSION|STATEMENT,默认是SESSION

    可以使用STATEMENT来禁用一级缓存

https://zhhll.icu/2021/框架/mybatis/基础/6.mybatis缓存/

本文由mdnice多平台发布

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拾光师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值