缓存key

在前面的初始时,xml配置中的每一个jdbc语句都会被封装为一个MappedStatement对象,存储在Configuration中。key就是xml中配置的namespace.id​​

查询时,从Configuration根据statement获取对应的MappedStatement,交给执行器执行

​CachingExecutor​​

首先,MappedStatement调用getBoundSql​​,获取参数值对应的完整sql语句。此处是BoundSql​​。

然后,创建缓存key,CacheKey对象

cacheKey主要由以下部分组成

//语句id
cacheKey.update(ms.getId());
//分页参数
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
//sql语句
cacheKey.update(boundSql.getSql());
//参数的值
cacheKey.update(value);
//当前环境id
 cacheKey.update(configuration.getEnvironment().getId());

由此可见,如果传递的参数一致,语句一致,那么缓存就会生效。

二级缓存

概念理解

二级缓存是namespace级别的缓存。默认关闭状态。二级缓存是可以跨SqlSession存在的。

开启二级缓存的方式:

  1. 当前xml中加入<cache/>​​,会开启当前namespace的缓存

  2. 全局开启,在mybatis配置文件中加入 <setting name="cacheEnabled" value="true"/>​​

二级缓存生效时机:

在执行语句时,二级缓存只是放入了TransactionalCache​​的entriesToAddOnCommit​​中,并没有真正放入缓存。而是当调用SqlSession.commit()或者close()时,才会放入真正缓存中。

二级缓存失效时机:

  1. 默认过期失效

  2. 执行了insert,update,delete语句,且commit或者close之后清空缓存

  3. 配置了useCache = false或者resultHandler​​不为null,不会使用二级缓存

  4. 配置了flushCache=true,会在提交事务时清除缓存。

注意事项

​entriesToAddOnCommit​​

这个是缓存中一个map,在执行语句时的缓存操作,都是操作该对象。只有提交或者关闭sqlSession的时候,才会根据该属性写入真实的缓存或者清除缓存。

生产环境不建议开启二级缓存。

源码分析

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) {
      //刷新二级缓存(存在缓存且flushCache为true)
      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;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

​ms.getCache()​​是从ms上获取的,因此二级缓存的开关位于mapper.xml中配置。每一个命名空间可以设置一个独立的二级缓存。<cache-ref>可以共享别的二级缓存

<mapper namespace="user">
    <cache/>
    <select id="findUserById" parameterType="int" resultType="com.mahaonan.test.pojo.User" flushCache="true">
        select id,username from user where id = #{id}
    </select>
</mapper>

如上所示,就开启了该xml对应的mybatis的二级缓存。

一级缓存

概念理解

一级缓存是mybatis默认开启的,且不可关闭。

有两种缓存级别,SESSION和STATEMENT,默认为SESSION级别。

SESSION:在一个sqlSession内共享缓存

STATEMENT:缓存只对当前执行的这一个statement有效。

在mybatis.config中进行配置

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

虽然mybatis不可关闭一级缓存,但是可以设置为STATEMENT级别,也相当于关闭了。或者在语句中加入flushCache=true配置


一级缓存是称为PerpetualCache​​的本地缓存,其实现就是一个简单的map缓存。在创建Executor的时候会被创建并保存在Executor中。


在默认的mybatis中,一级缓存返回的是同一个对象的引用,因此需要注意坑,如果修改了该对象,会引起问题。


一级缓存的清除时机:

  1. 一个sqlSession结束后清除

  2. insert,update,delete语句执行后

  3. 配置了flushCache=true,每次执行都会清除

在整合spring中,在没有事务的情况下,每次执行sql,都会新创建一个sqlSession,因此没有一级缓存。

开启事务的情况下,会用到一级缓存。

缓存问题

    @Transactional
    public SpeakLogDTO get(Long id) {
        LambdaQueryWrapper<SpeakLogPO> queryWrapper = Wrappers.<SpeakLogPO>lambdaQuery().eq(SpeakLogPO::getId, id);
        List<SpeakLogPO> speakLogPOS = mapper.selectList(queryWrapper);
        List<SpeakLogPO> speakLogPOS2 = mapper.selectList(queryWrapper);
        speakLogPOS.get(0).setContent("123");
        System.out.println(speakLogPOS == speakLogPOS2);
        System.out.println(speakLogPOS.get(0) == speakLogPOS2.get(0));
        return converter.PO2DTO(speakLogPOS2.get(0));
    }

如上所述:在事务中,一级缓存是生效的。因为spring会从ThreadLocal中获取同一个sqlSession。

此时speakLogPOS和speakLogPOS2是同一个对象。内部的元素也是同一个对象。因此如果修改了对象属性,会导致返回结果出问题。

因此,对于事务查询出来的结果,且两次查询之间没有增删改操作,避免去直接修改结果,而是转换为DTO再去修改。

或者设置local-cache-scope​​为statement级别

源码分析

  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
      CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      //从一级缓存中获取数据
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        //如果一级缓存中有数据,则处理本地缓存结果输出参数(存储过程)
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        //如果一级缓存中没有数据,则查询数据库并且将查询结果放入一级缓存
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
		//如果缓存是STATEMENT级别,清除缓存
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

从上面源码可以看出,当resultHandler不为null时,就不会用到一级缓存。

mybatis-plus的个别方法,例如selectOne就是利用这种方式避免了一级缓存返回同一对象导致问题。

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
      ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    //1. 首先将占位符放入缓存
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      //2. 执行查询
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      //3. 执行完成移除占位符
      localCache.removeObject(key);
    }
    //4. 将查询结果放入缓存
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }