MyBatis源码解析(四)二级缓存

1,290 阅读4分钟

简介

​ 二级缓存是应用级缓存,与一级缓存不同的是它的作用范围是整个应用,可以跨线程使用。

​ 二级缓存的原理和一级缓存的原理相同,第一个查询会将数据放入到缓存中,然后第二个查询直接查缓存,多个SqlSession可以在映射器中共享二级缓存。如果两个映射器的名称空间相同,那么这两个映射器共用一个缓存空间。

使用

二级缓存是默认关闭的,具体开启用法参照《MyBatis源码解析三 缓存执行链》。 juejin.cn/post/684490…

缓存命中场景

缓存命中场景大致同一直缓存,要求CacheKey相同,即MappedStatementIdsql, sql的偏移量,sql的限制以及参数。可参考《MyBatis源码解析二 一级缓存》 juejin.cn/post/684490…

执行流程及源码解析

@Test
public void cacheTest() {
    SqlSession sqlSession = factory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user = mapper.selectById(1);
}

以查询为例,去深入了解MyBatis 二级缓存的执行流程。

  • 首先创建 SqlSession 对象

    Configuration

    默认创建的执行器为SimpleExecutor

    // 创建执行器
    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;
      }
    
  • 获取 mapper对象

  • 执行查询操作

    调用SqlSession 接口的 SelectList方法,然后调用器实现类DefaultSqlSessionselectList方法

    DefaultSqlSession

    SqlSession 将特定的查询职责交给了 Executor

CachingExecutor

MyBatis中的二级缓存是通过CachingExecutor实现的。如果仅开启一级缓存,走BaseExecutor的查询方法。若开启了二级缓存,先走CachingExecutor的查询方法

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 获取BoundSql对象,包含sql,参数等
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 创建缓存的key,以后根据这个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) {
        // 根据flushCache判断是否需要清除缓存
        flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
            // 处理存储过程
            ensureNoOutParams(ms, boundSql);
            // 从缓存中获取数据,实际上是走责任链,获取数据
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) {
                // 缓存数据为空,从数据库中获取,并填充到缓存中.delegate是SynchronizedCache
                list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                // tcm 事务缓存管理器
                tcm.putObject(cache, key, list);
            }
            return list;
        }
    }
    // 没有开启二级缓存,执行BaseExecutor,走一级缓存。delegate默认是SimpleExecutor
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

调用TransactionalCache 将结果放入到 entriesToAddOnCommitmap集合中

public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
}
public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
}

TransactionalCacheManager

TransactionalCacheManager 中 保存着缓存和事务缓存的映射关系。TransactionalCache 也实现了 Cache 接口。每个会话都有对应的事务缓存管理器,在缓存管理器中放着不同查询的缓存结果,当执行提交时或者SqlSession关闭时会将查询结果刷到缓存中

private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

TransactionalCacheManager 用来管理 Cache,每个Cache 都会使用 TransactionalCacheManager 进行装饰,必须手动提交才能将 结果放到缓存中,这个是事务性的。

CachingExecutor中执行提交方法,才会调用TransactionalCacheManager 的提交方法,最终到了TrancationalCache 中的提交方法。

// CachingExecutor
public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();
  }

// TrancationalCache 
 public void commit() {
     if (clearOnCommit) {
         delegate.clear();
     }
     flushPendingEntries();
     reset();
 }

// 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);
      }
    }
  }

CacheKey

当我们根据CacheKey从缓存中取出数据,是如何判断CacheKey是相等的呢。在CacheKey 中定义了一个equals方法。

 public boolean equals(Object object) {
    if (this == object) {
      return true;
    }
    if (!(object instanceof CacheKey)) {
      return false;
    }

    final CacheKey cacheKey = (CacheKey) object;

    if (hashcode != cacheKey.hashcode) {
      return false;
    }
    if (checksum != cacheKey.checksum) {
      return false;
    }
    if (count != cacheKey.count) {
      return false;
    }

     // 遍历,只要updatelist中的元素相同,则CacheKey相同
    for (int i = 0; i < updateList.size(); i++) {
      Object thisObject = updateList.get(i);
      Object thatObject = cacheKey.updateList.get(i);
      if (!ArrayUtil.equals(thisObject, thatObject)) {
        return false;
      }
    }
    return true;
  }

总结

  • MyBatis 的二级缓存实现了SqlSession 之间的缓存数据共享
  • MyBatis 的二级缓存查询时必须提交,才能真正放入到缓存中。插入,修改,删除操作会清空缓存
  • MyBatis 的二级缓存是通过CachingExecutor实现的