MyBatis中一、二级缓存机制的实现原理

一级缓存

每个SqlSession中持有了Executor,每个Executor中持有一个PerpetualCache对象。PerpetualCache中维护了一个Map集合实例cache,当发起查询时先创建CacheKey,若命中缓存则直接返回结果,如果没有命中缓存则查询数据库,结果写入PerpetualCache,最后返回结果。具体实现代码如下。

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameter);
  // 创建缓存key,CacheKey包含MappedStatement的Id、SQL的offset、SQL的limit、SQL本身以及SQL中的参数
  CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
  return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}


public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  ...
  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);
  }
  ...
  // query方法执行的最后,会判断一级缓存级别是否是STATEMENT级别,如果是的话则清空缓存,这就是STATEMENT级别的一级缓存无法共享localCache的原因
  if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      this.clearLocalCache();
  }
  ...
}


private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  List<E> list;
  localCache.putObject(key, EXECUTION_PLACEHOLDER);
  try {
    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  } finally {
    localCache.removeObject(key);
  }
  // 将查询出来的结果存入一级缓存
  localCache.putObject(key, list);
  if (ms.getStatementType() == StatementType.CALLABLE) {
    // 如果是存储过程把参数存入localOutputParameterCache
    localOutputParameterCache.putObject(key, parameter);
  }
  return list;
}

一级缓存源码分析的最后,我们确认一下缓存就会刷新的时机,SqlSession的insertdeleteupdate方法都会统一走update,每次执行update前都会执行clearLocalCache清除缓存。

 public int update(MappedStatement ms, Object parameter) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
        if (this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            this.clearLocalCache();
            return this.doUpdate(ms, parameter);
        }
    }

一级缓存总结

1、MyBatis一级缓存的生命周期和SqlSession一致。

2、MyBatis一级缓存内部设计是Executor中持有一个没有容量限定的HashMap。

3、MyBatis一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。

二级缓存

如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询。同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。当开启缓存后,数据的查询执行的流程就是:二级缓存 -> 一级缓存 -> 数据库。

CachingExecutor的query方法中首先会从MappedStatement中获得在配置初始化时赋予的Cache。看到第二个if判断时我们可以总结出开启二级缓存的条件:

1、全局配置变量参数cacheEnabled=true(在创建Executor的时候会进行该条件判断,成立则用CachingExecutor包装。)
2、该select语句所在的Mapper,配置了<cache> 或<cached-ref>节点,并且有效。
3、该select语句设置了useCache=true。

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) {
            // 根据xml配置的属性flushCache来判断是否需要刷新缓存暂存区,默认的设置中SELECT语句不会刷新缓存,insert/update/delte会刷新缓存。<select id="findUserById" useCache="true" flushCache="true">
            this.flushCacheIfRequired(ms);
            // 根据xml配置的属性useCache来判断是否使用缓存,<select id="findUserById" useCache="true">
            if (ms.isUseCache() && resultHandler == null) {
                // 确保方法没有Out类型的参数,mybatis不支持存储过程的缓存,所以如果是存储过程,这里就会报错
                this.ensureNoOutParams(ms, boundSql);
                // 根据key从TransactionalCacheManager取出缓存,如果没有缓存则执行查询,并且将查询结果放到缓存中并返回取出结果
                List<E> list = (List)this.tcm.getObject(cache, key);
                // 如果二级缓存中没有这个数据时便会利用被包装的执行器查询
                if (list == null) {
                    // 这里会先查一级缓存,没有的话便会到数据库中查找,详见上面一级缓存查询逻辑
                    list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    // TransactionalCacheManager对象会创建一个TransactionalCache对象,它对传入的Cache进行再一次的包装,它持有的entriesToAddOnCommit对象用来保存临时的缓存结果
                    // 当SqlSession执行commit或者close时都会执行transactionCache的flushPendingEntries方法将entriesToAddOnCommit中的数据提交到mapper的cache当中
                    this.tcm.putObject(cache, key, list);
                }
                return list;
            }
        }
        // 委托模式,交给SimpleExecutor等实现类去实现方法
        return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}


// 同样在执行insert/update/delete也会执行该方法进行缓存的清除
private void flushCacheIfRequired(MappedStatement ms) {
    // 先从MapperStatement取出缓存,只有通过<cache/>、<cache-ref/>或@CacheNamespace、@CacheNamespaceRef标记使用缓存的Mapper.xml或Mapper接口(同一个namespace,不能同时使用)才会有二级缓存。
    // 如果缓存不为空,说明是存在缓存。如果cache存在,那么会根据sql配置insert/select/update/delete的flushCache属性来确定是否清空缓存。
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {      
      // tcm是TransactionalCacheManager的实例,持有Cache和用TransactionalCache包装后的Cache的映射关系Map集合
      //这里的clear并不会去去清空二级缓存区域,而是设置了一个提交标识和清空entriesToAddOnCommit,如果事务回滚或者不提交事务,则不对缓存产生影响
      tcm.clear(cache);
    }
}

同样在执行insert/update/delete时,CachingExecutor也会执行flushCacheIfRequired方法进行是否清除缓存暂存区的判断,即清除entriesToAddOnCommit中的值。

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

// 提交和回滚都是直接调用TransactionalCacheManager的commit和rollback方法
// TransactionalCacheManager会将对应TransactionalCache中的数据提交到二级缓存或清除
public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();
}

public void rollback(boolean required) throws SQLException {
    try {
      delegate.rollback(required);
    } finally {
      if (required) {
        tcm.rollback();
      }
    }
}

 

二级缓存总结

1、MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。但正是由于所有SqlSession实例共享,基于namespace隔离的特点,故如果不同namespace定义了同时操作一个表的SQL语句,则会造成不同namespace之间的缓存不一致的问题。

2、MyBatis在多表查询时,极大可能会出现脏数据。采用cache-ref来进行命名空间的依赖能够避免二级缓存,但是总不能每次写一个XML配置都会采用这种方式吧,最有效的方式还是避免多表操作使用二级缓存。

3、在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。

 


版权声明:本文为qq_39768738原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。