Mybatis一级缓存和二级缓存避坑小记

1.场景&问题解决

MyBatis框架提供了一级缓存和二级缓存,其中一级缓存基于SqlSession实现,而二级缓存基于Mapper实现。MyBatis提供的缓存机制是为了提高性能,但有些场景下我们必须关闭,否则会获取错误的数据,造成逻辑错误。常见的一种场景就是跑批处理一些业务时,比如:

    @Transactional(rollbackFor = Exception.class)
    public void runOnTrigger(String batchNo) {
       
        //分页处理数据
       while (true) {
            //获取该批次未处理的第一条数据id
            Integer startIndex = originDataService.getFirstUnprocessedItemId(batchNo);
            log.info("startIndex: {}", startIndex);
            if (Objects.isNull(startIndex)) {
                log.info("当前批次数据处理结束 batchNo: {}", batchNo);
                break;
            }

            //获取未处理的数据
            List<DemoPO> logPOList = originDataService.getUnprocessedItemsByBatchNo(startIndex, BATCH_SIZE, batchNo);
            try {
                log.info("logPOList: {}", JSON.toJSONString(logPOList));
                originDataCollector.doCollect(batchNo, logPOList);
            } catch (Exception e) {
                log.error("数据处理失败", e);
                //更新数据明细处理状态为:处理失败
                List<Integer> targetIds = logPOList.stream().map(m -> m.getId()).collect(Collectors.toList());
                originDataService.updateItemOnFail(targetIds);
            }
       }
    }

上述代码中,代码段:
Integer startIndex = originDataService.getFirstUnprocessedItemId(batchNo);对应的Mapper为:

@Select("SELECT id FROM seller_trans_log WHERE data_date = #{batchNo} AND process_status = 0 ORDER BY id ASC LIMIT 1;")
Integer getFirstUnprocessedItemId(@Param("batchNo") String batchNo);

如果我们不关闭缓存,那么这条SQL在同一SqlSession中多次执行获取的id将会和第一次查询时的结果一样,始终不变(Mybatis缓存的原因);这显然是不能接受的(造成死循环或者业务处理失败)。怎么解决呢?既然是Mybatis的缓存原因,那我们关闭他就行了。我们可以通过Mybatis的Options注解关闭缓存,如下:

    @Options(flushCache = Options.FlushCachePolicy.TRUE)
    @Select("SELECT id FROM seller_trans_log WHERE data_date = #{batchNo} AND process_status = 0 ORDER BY id ASC LIMIT 1;")
    Integer getFirstUnprocessedItemId(@Param("batchNo") String batchNo);

若sql在xml文件中,我们可以这样关闭缓存

<select id="getIdByFoo" flushCache="true" useCache="false" resultType="java.lang.Integer">
        SELECT id
        FROM table_a
        WHERE foo = #{foo,jdbcType=VARCHAR}
</select>

2.Mybatis缓存概述

一级缓存

MyBatis的一级缓存是SqlSession级别的缓存,SqlSession在执行用户定义的sql语句时会交由Executor组件去执行,一级缓存的实现就是在Executor的子类BaseExecutor中实现的。

public abstract class BaseExecutor implements Executor {
  ...
  //一级缓存的缓存类(内部维护了一个HashMap保存缓存内容)
  protected PerpetualCache localCache;
  ...

以query方法为例,简述如下:

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //获取要执行的sql
    BoundSql boundSql = ms.getBoundSql(parameter);
    //Mybatis中描述缓存值的对象,如果两次查询操作的CacheKey对象相同,就会认为这两次查询执行的是相同的SQL语句
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

createCacheKey:

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    //更新缓存key
    CacheKey cacheKey = new CacheKey();
    //Mapper的Id,即Mapper命名空间与<select|update|insert|delete>标签的Id组成的全局限定名
    cacheKey.update(ms.getId());
    //查询结果的偏移量
    cacheKey.update(rowBounds.getOffset());
    //查询结果的的条数
    cacheKey.update(rowBounds.getLimit());
    //具体的SQL语句
    cacheKey.update(boundSql.getSql());
    //SQL语句中需要传递的所有参数
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    ...
    if (configuration.getEnvironment() != null) {
      //MyBatis主配置文件中,通过<environment>标签配置的环境信息对应的Id属性值
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

执行查询时,只有上面的信息完全相同时,才会判定执行的是相同的SQL,才会使用缓存
具体query的逻辑:

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    //判断是否刷新缓存
    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) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

如上即为一级缓存的主要内容,感兴趣的可以详细看源码。

二级缓存

Mybatis3.x中默认开启了session级别的二级缓存(基于Mapper实现)
在这里插入图片描述

若要启用全局的二级缓存,需要在 SQL 映射文件中添加一行, 具体可参考官方文档:
在这里插入图片描述

sql的具体执行方仍然是Executor组件,类Configuration提供了创建Executor的工厂方法:

  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) {
    //如果开启二级缓存,则创建CachingExecutor
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

Executor的UML关系如下:
在这里插入图片描述
CachingExecutor类实现了二级缓存的功能,类信息如下:

public class CachingExecutor implements Executor {

  private final Executor delegate;
  //二级缓存
  //二级缓存是事务性的,也就是说,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存也会获得更新。
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

事务性缓存管理器:TransactionalCacheManager用于管理所有的二级缓存对象,类信息如下:

public class TransactionalCacheManager {
  //通过HashMap维护二级缓存对应的TransactionalCache
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
  //清空二级缓存
  public void clear(Cache cache) {
    getTransactionalCache(cache).clear();
  }

  //二级缓存名称的由来
  //查询缓存 先通过cache获取二级缓存TransactionalCache,然后再从TransactionalCache 中查询缓存key对应的值
  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }
  
  //添加缓存
  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }

  //提交时更新缓存
  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }

  public void rollback() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.rollback();
    }
  }

//
  private TransactionalCache getTransactionalCache(Cache cache) {
    //从HashMap中获取二级缓存TransactionalCache
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
      //如果Map中没有获取到,创建,放入Map中
      txCache = new TransactionalCache(cache);
      transactionalCaches.put(cache, txCache);
    }
    return txCache;
  }

了解二级缓存后,我们可以看下CachingExecutor的query操作,可以直观看到二级缓存的作用
CachingExecutor:

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    //创建缓存key 逻辑和一级缓存一样
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
      //获取MappedStatement 中维护的缓存对象
    Cache cache = ms.getCache();
    if (cache != null) {
    //校验是否刷新二级缓存
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        //从MappedStatement对象中的二级缓存中获取缓存的内容
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          //缓存不存在,查库
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          //将查询结果放到MappedStatement 对象对应的二级缓存中
          tcm.putObject(cache, key, list); 
        }
        return list;
      }
    }
    //如果MappedStatement 中没有配置二级缓存,直接查库
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

如果执行的是update语句,则同一命名空间下的二级缓存将会被清空

  @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
  }

  private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) { 
      //判断<select|update|delete|insert>标签的flushCache属性,如果属性值为true,就清空缓存
      //<select>标签的flushCache属性值默认为false,而<update|delete|insert>标签的flushCache属性值默认为true 
      tcm.clear(cache);
    }
  }

  public boolean isFlushCacheRequired() {
    //类MapperBuilderAssistant中维护
    return flushCacheRequired;
  }

综上我们可以发下,Mybatis的一级缓存和二级缓存的默认实现底层都是通过HashMap实现的,也就是说默认都是本地缓存即堆缓存;二级缓存也可以指定到外部缓存中间件中,如Redis,有兴趣的可以去研究下

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值