前言
上次我们讲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入参
接下来,我们不难发现已经出现了 存储、获取、清除这三种方法,且都围绕着
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里在一二级都开启的情况下,优先使用的是二级缓存的数据,因此这里需要特别注意。