Mybatis的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里在一二级都开启的情况下,优先使用的是二级缓存的数据,因此这里需要特别注意。