Mybatis的CachingExecutor与二级缓存

举报
战斧 发表于 2023/09/21 18:54:20 2023/09/21
【摘要】 前言上次我们讲Mybatis的缓存时,我们提到了CachingExecutor,知道了这个带缓存的执行器就是二级缓存的来源,这次我们系统的分析下其是如何产生作用的一、CachingExecutor的在逻辑定位1. 流程图中的位置我把CachingExecutor在逻辑链路中的位置标出来了,就是储存在会话对象中,通过会话可使用到CachingExecutor,而CachingExecutor又...

前言

上次我们讲Mybatis的缓存时,我们提到了CachingExecutor,知道了这个带缓存的执行器就是二级缓存的来源,这次我们系统的分析下其是如何产生作用的


一、CachingExecutor的在逻辑定位

1. 流程图中的位置

我把CachingExecutor在逻辑链路中的位置标出来了,就是储存在会话对象中,通过会话可使用到CachingExecutor,而CachingExecutor又内置一个SimpleExecutor,熟悉设计模式的同学应该知道这就是所谓的委派模式。当然,这里面会话内置的也可能直接就是SimpleExecutor了,那样的话,调用的就直接是SimpleExecutor执行器了。
在这里插入图片描述

二、CachingExecutor的生效

既然知道了CachingExecutor在会话对象中,那毫无疑问,就是在创建会话的时候,把一个CachingExecutor放入到会话对象中的,我们来看看,要实现这个目标要做什么

1.全局参数

首先,要想启用CachingExecutor,我们得开启一个全局的设置参数

mybatis.configuration.cache-enabled=true

为什么开了这个参数就有用呢?其实不难猜想,其作用的位置肯定还是在创建会话对象的时候,我们直接看源码吧

SqlSessionUtils.class

// SqlSessionUtils.class
// 通过工厂对象(单例,存在容器中),开启会话,获得会话对象,executorType是执行器枚举类(SIMPLE, REUSE, BATCH),一般是SIMPLE
session = sessionFactory.openSession(executorType);

DefaultSqlSessionFactory.class

// DefaultSqlSessionFactory.class
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    final Environment environment = configuration.getEnvironment();
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    // 通过configuration(Mybatis配置,单例)创建执行器
    final Executor executor = configuration.newExecutor(tx, execType);
    return new DefaultSqlSession(configuration, executor, autoCommit);
  } catch (Exception e) {
    closeTransaction(tx); // may have fetched a connection so lets call close()
    throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

Configuration.class

// Configuration.class
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);
  }
  // 如果开启了缓存,即 mybatis.configuration.cache-enabled=true
  if (cacheEnabled) {
    // 创建了缓存执行器,并且把简易执行器作为其 delegate,通过构造方法放入
    executor = new CachingExecutor(executor);
  }
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

如上,可以看出CachingExecutor就是由 mybatis.configuration.cache-enabled=true 开启的,并且和会话的情况无关,这是一个全局设置,一旦开启,所有会话都会首先调用CachingExecutor

2. MappedStatement启用Cache

是不是我们启用了CachingExecutor就可以使用二级缓存了呢?为什么这么说,我们还是直接看CachingExecutor的源码
CachingExecutor.class

// CachingExecutor.class
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  // mappedStatement 就是我们写的mapper接口里的某个方法的所有信息,注意其描述的是一个方法,而不是一整个mapper接口的所有方法
  // 当然,它同样也包含某些mapper层次的设置,如对应的xml文件位置等,此处的Cache同样是mapper层次的设置
  Cache cache = ms.getCache();
  // 有Cache才会真正的去找二级缓存,否则直接就让委托的执行器去查询数据了。
  // 注意此处的Cache并不是缓存本身,而是mapper里的缓存配置
  if (cache != null) {
    // ....
      List<E> list = (List<E>) tcm.getObject(cache, key);
    // ....
      return list;
    }
  }
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

可以看到,要想真正启用缓存,还得在Mapper层级进行一次缓存配置,也就是所谓的

声明下Cache时可以设置一下参数
<cache eviction="FIFO" flushinterval="60000" size="512" readOnly="true"/>
当然也可以不进行任何参数配置,就单独声明下Cache,如下:
<cache/>

我们看一下此时MappedStatement里Cache的构成,可以说是套娃巅峰,把委派模式玩到了极致在这里插入图片描述

我们通过源码看其实现
MapperBuilderAssistant.class

// MapperBuilderAssistant.class
public Cache useNewCache(Class<? extends Cache> typeClass,
    Class<? extends Cache> evictionClass,
    Long flushInterval,
    Integer size,
    boolean readWrite,
    boolean blocking,
    Properties props) {
  Cache cache = new CacheBuilder(currentNamespace)
      .implementation(valueOrDefault(typeClass, PerpetualCache.class))
      .addDecorator(valueOrDefault(evictionClass, LruCache.class))
      .clearInterval(flushInterval)
      .size(size)
      .readWrite(readWrite)
      .blocking(blocking)
      .properties(props)
      .build();
  configuration.addCache(cache);
  currentCache = cache;
  return cache;
}

三、二级缓存的存取

上面我们已经看了如何使二级缓存生效,但真正的查询和存入还没有细看,现在来看看其存储的位置,及如何读取,我们直接看下源码

1. 缓存源码分析

// CachingExecutor.class query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
if (cache != null) {
  // 是否清缓存,即如果Cache配置里含有flushCache=“true” 则进行 tcm.clear();
  flushCacheIfRequired(ms);
  if (ms.isUseCache() && resultHandler == null) {
    ensureNoOutParams(ms, boundSql);
    @SuppressWarnings("unchecked")
    // tcm 是一个成员变量,是通过new TransactionalCacheManager()赋值的,此处为查询
    List<E> list = (List<E>) tcm.getObject(cache, key);
    if (list == null) {
      list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
      // 通过 tcm 进行结果的存储
      tcm.putObject(cache, key, list); // issue #578 and #116
    }
    return list;
  }
}

在进行下一步前,我们有必要看一下获取缓存,为什么要传两个入参,即cache和key?
在这里插入图片描述

  • cache:mapper级别的缓存配置及二级缓存
  • key:方法的全限定名及完整的sql即sql入参

接下来,我们不难发现已经出现了 存储、获取、清除这三种方法,且都围绕着tcm(TransactionalCacheManager),因此接下来,我们还得关注一下TransactionalCacheManager。

public class TransactionalCacheManager {
	private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
	public Object getObject(Cache cache, CacheKey key) {
	    // 先把缓存设置作为key,查询出一个TransactionalCache
    	return getTransactionalCache(cache).getObject(key);
  	}
  	private TransactionalCache getTransactionalCache(Cache cache) {
  	    // 作为第一次查询,不难发现,所谓的value居然也是依托缓存设置来构造的
    	return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
    }
  	// ....
}

public class TransactionalCache implements Cache {
    private final Cache delegate;
    private boolean clearOnCommit;
    // 临时待加缓存,查询数据库返回的结果,首先会放在这里,等本次事务提交后,才会加入到真正的二级缓存中,即调用套娃对象,层层深入,最终存入HashMap
    // 在事务提交前,其他会话甚至本会话自己都无法看见该缓存,更无法使用该缓存
    private final Map<Object, Object> entriesToAddOnCommit;
    // 查二级缓存没查到时,会把key值存在这个miss的集合中
    private final Set<Object> entriesMissedInCache;
    public TransactionalCache(Cache delegate) {
    	this.delegate = delegate;
    	this.clearOnCommit = false;
    	this.entriesToAddOnCommit = new HashMap<>();
    	this.entriesMissedInCache = new HashSet<>();
    }
	public Object getObject(Object key) {
		// issue #116
		// 观察后,可以知道,这里的delegate其实就是我们说的mapper缓存设置
		// 由此可见,缓存设置中也能包含缓存的值,并且以完整sql为键,sql结果为值以map存储
		Object object = delegate.getObject(key);
		if (object == null) {
			entriesMissedInCache.add(key);
		}
		// issue #146
		if (clearOnCommit) {
			return null;
		} else {
			return object;
		}
	}
}

由于Cache的套娃十分严重,实际形成了链状引用,而且受配置的影响很大,所以没有办法把每一种配置缓存间的相互调用阐述详尽。但是我们仍然可以讲出其核心思想。忽略掉中间的套娃,最终实现存储的缓存类为PerpetualCache.class,其包含了一个HashMap,键就是我们上面提及的key(混合了方法信息,完整sql等),值为sql返回值的字节数组
在这里插入图片描述

2. 二级缓存可见性

二级缓存并不是即时生效的,我们可以关注下TransactionalCache 类,这个类看名字也知道是事务有关,它有一个成员变量

private final Map<Object, Object> entriesToAddOnCommit;

这个变量我们上面源码分析里其实说了,就算查数据库,返回了结果,也不是立即就到我们说的终极位置——PerpetualCache的HashMap里。而是在这个变量里暂存,等待事务提交了,再把这里的数据存入真正的二级缓存处

而在此之前,即使是本会话,也没法从二级缓存中捞到东西,也就是说在一个事务里,你连续执行两次同样的sql,尽管二级缓存已经暂存了数据,但第二次sql经过CachingExecutor时,它并不会把这个数据给你,那么自然,其他的会话也是无法看见这个数据的。

因此,我们说二级缓存的数据,只在本事务提交后才正式可见,在此之前,其他会话甚至本会话自己,都无法使用该二级缓存

3. 一二级缓存优先级

看上图,不难明白,如果开启了二级缓存,则先查的是二级缓存(mapper级别),这和我们一般的认知相悖,因为大多数缓存层级,都是优先查一级缓存,未命中再去查的二级缓存。除了上面因为可见性问题导致的,先查一级缓存,Mybatis里在一二级都开启的情况下,优先使用的是二级缓存的数据,因此这里需要特别注意。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。