Mybatis源码(5)-缓存

目录

一、介绍

二、一级缓存

2.1 简要介绍

2.2 查询流程

2.3 生命周期

三、二级缓存

3.1 简要介绍

3.2 流程分析


一、介绍

对DB的访问相对于对内存的访问耗时明显,缓存作为优化系统性能的常用手段,Mybatis也引入缓存以减少对DB的重复访问。Mybatis内部支持一级缓存(SqlSession级别)和二级缓存(Application级别),下面将详细介绍这两种缓存。

二、一级缓存

2.1 简要介绍

       一级缓存是SqlSession级别的缓存,一级缓存默认开启。SqlSession是使用Mybatis时主要的类,内部提供了执行sql、获取mapper、管理事务的方法。一级缓存由BaseExecutor.localCache维护,Executor会先去查询一级缓存,如果命中则返回;否则查询db并更新缓存。这些核心组件之间的流程图如下所示:

下面介绍查询流程、更新流程、为什么说一级缓存是SqlSession级别的缓存。

2.2 查询流程

       一级缓存由PerpetualCache维护,而PerpetualCache类持有了Map<String, Object>的成员变量。查询的核心流程:

  1. BaseExecutor首先会根据mappedStatement(sql配置)、parameterObject(查询参数)、rowBounds(内存分页相关)、boundSql(待执行的实际sql)构建缓存key;
  2. 从localCache中查询缓存是否存在,如果存在则返回;
  3. 缓存未命中,查询db后更新缓存;
  4. 如果一级缓存的级别是STATEMENT(见localCacheScope属性,支持SESSION/STATEMENT两种配置),则更新缓存。

代码:BaseExecutor.query():

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); // 构建缓存key
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql); // 执行查询流程
  }

// mybatis内部缓存key的构建流程,核心是怎么判断两个请求是同一个请求,感兴趣的可以自行分析
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    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 <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(); // 如果mappedStatmenet配置了flushCache为true,那么每次执行查询前清空缓存
    }
    List<E> list;
    try {
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;// 查缓存
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); // 和CallableStatement(应用于存储过程)相关,本文不做介绍
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); // 查DB,并更新缓存
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache(); // 如果LocalCacheScope是STATEMENT级别,那么执行完sql后清空缓存
      }
    }
    return list;
  }

  @Override
  public void clearLocalCache() {
    if (!closed) {
      localCache.clear(); // 清空缓存
      localOutputParameterCache.clear();
    }
  }

总结下来时序图如下:

2.3 生命周期

通过Mybatis执行sql时,首先需要获取一个新的SqlSession(多线程下BaseExecutor.query()不安全),SqlSession会初始化一个新的Executor对象。SqlSession、Executor内部的一些方法都会触发到一级缓存的清理。

缓存清理主要有以下流程:

  1. 调用SqlSession.close()关闭会话时,sqlSession.executor.localCache会被置为null;
  2. 调用SqlSession.clearCache();
  3. 调用SqlSession.commit()提交会话时,一级缓存会被清空;
  4. 执行update、delete、insert等操作时,会先清空一级缓存。

具体代码相对简单,感兴趣可以自行阅读。

三、二级缓存

3.1 简要介绍

       二级缓存是Application级别的缓存,缓存的粒度是mapper的namespace(同一个mapper使用同一个缓存),意思是服务启动成功后,除了显示地去清理二级缓存,否则二级缓存的生命周期和Application一致。二级缓存的读写和CachingExecutor相关,在Mybatis源码(3)-查询执行流程中简要介绍了CachingExecutor的执行流程,CachingExecutor代理了Executor实现类的执行,增加了二级缓存的功能。如下图所示,开启二级缓存的方法是设置属性为true,同时在mapp.xml文件中添加<cache/>元素,只有配置了这些后SqlSession持有的Executor才是CachingExecutor。

       如下图所示,SqlSession在查询数据时,如果配置了二级缓存,首先会通过CachingExecutor查询是否命中缓存,如果没有则通过DelegateExecutor查询结果并更新二级缓存。

下面介绍二级缓存的查询流程和更新流程:

3.2 流程分析

核心代码在CachingExecutor中:

 @Override
  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) { // 如果配置了<cache/>标签
      flushCacheIfRequired(ms); // 如果设置了flushCache == true则执行清空缓存操作
      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); // 查询一级缓存 -> dbn
          tcm.putObject(cache, key, list); // 将缓存k-v放置在entriesToAddOnCommit中,等sqlsession.commmit()时才会真正将k-v写入到二级缓存中
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // 查询一级缓存 -> db 
  }
 @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms); 设置clearOnCommit标志位,commit时根据该标志位判断是否清空缓存
    return delegate.update(ms, parameterObject); // 执行BaseExecutor.update()
  }
  @Override
  public void commit(boolean required) throws SQLException {
    delegate.commit(required); // 通过BaseExecutor的实现类实现commit操作(提交事务,清理一级缓存)
    tcm.commit(); // 执行TransactionCacheManager的commit操作,遍历所有namespace的二级缓存,执行其commit操作。
  }
  private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) { // 如果配置了flushCache == true
      tcm.clear(cache); // 只是设置clearOnCommit为true,并清空entriesToAddOnCommit
    }
  }

TransactionCacheManager: 基于事务的缓存管理器,mybatis的二级缓存无论新增/更新都只是设置TransactionalCache的clearOnCommit、entriesToAddOnCommit(待写入的缓存kv),entriesMissedInCache(miss的key列表)等属性,等执行sqlSession.commmit()时二级缓存才会变更。

TransactionCacheManager:

  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }
  private TransactionalCache getTransactionalCache(Cache cache) {
    return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
  }
  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit(); // 遍历各namespace的二级缓存,执行commit
    }
  }

Mybatis使用TransactionalCache管理二级缓存Cache的实现类。

TransactionalCache:

  private final Cache delegate; // 二级缓存的实现类,默认实现类是PerpetualCache,用HashMap存放缓存对象
  private boolean clearOnCommit; // commit时是否清空缓存的标志位
  private final Map<Object, Object> entriesToAddOnCommit; // commit时待写入二级缓存的k-v列表
  private final Set<Object> entriesMissedInCache; // miss的key列表
  

  @Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

  @Override
  public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
  }


  public void commit() {
    if (clearOnCommit) {
      delegate.clear(); // 如果设置了clearOnCommit,则清空二级缓存
    }
    flushPendingEntries(); // 将entriesToAddOnCommit、entriesToAddOnCommit刷到cache中
    reset(); // 重置clearOnCommit、entriesToAddOnCommit、entriesToAddOnCommit
  }

private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }

至此二级缓存的分析完毕,核心组件:CachingExecutor代理了BaseExecutor的执行,增加了二级缓存管理、查询的功能。二级缓存的管理、查询则有TransactionCachingExecutor负责,二级缓存的写入、清理时都只是首先记录待写入的缓存kv、设置清理标志位,等cachingExecutor.commit时才会真正生效。

四、总结

mybatis支持一级缓存、二级缓存两种缓存,其中一级缓存是sqlSession级别的缓存,二级缓存则是application级别、每个namespace对应一个缓存对象。一级缓存、二级缓存由于其实现方式可能导致缓存不一致的现象,在分布式环境下该问题可能更突出,可能会导致不同机器上对同一请求的响应不一致,同时为了集中式地管理缓存,我们在生产环境中一般使用Redis等开源缓存数据库管理缓存。在mybatis的配置上,一般会关闭二级缓存、同时将localCacheScope设置为STATEMENT(每执行一次query都会清理一级缓存)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值