ReuseExecutor#
这个ReuseExecutor相对于SimpleExecutor来说,不同点就是它先来的对Statement的复用,换句话说,某条Sql对应的Statement创建出来后被放在容器中保存起来,再有使用这个statement的地方就是容器中拿就行了
他是怎么实现的呢? 看看下面的代码就知道了
public class ReuseExecutor extends BaseExecutor { private final Map<String, Statement> statementMap = new HashMap(); public ReuseExecutor(Configuration configuration, Transaction transaction) { super(configuration, transaction); }
嗯! 所谓的容器,不过是一个叫statementMap的HashMap而已
下一个问题: 这个容器什么时候派上用场呢? 看看下面的代码也就知道了--this.hasStatementFor(sql)
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { BoundSql boundSql = handler.getBoundSql(); String sql = boundSql.getSql(); Statement stmt; if (this.hasStatementFor(sql)) { stmt = this.getStatement(sql); this.applyTransactionTimeout(stmt); } else { Connection connection = this.getConnection(statementLog); stmt = handler.prepare(connection, this.transaction.getTimeout()); this.putStatement(sql, stmt); } handler.parameterize(stmt); return stmt; }
最后一点: 当MyBatis知道发生了事务的提交,回滚等操作时,ReuseExecutor
会批量关闭容器中的Statement
BatchExecutor#
这个执行器相对于SimpleExecutor的特点是,它的update()
方法是批量执行的
执行器提交或回滚事务时会调用 doFlushStatements,从而批量执行提交的 sql 语句并最终批量关闭 statement 对象。
CachingExecutor与二级缓存#
首先来说,这个CachingExecutor
是什么? 那就得看一下的属性,如下:
public class CachingExecutor implements Executor { private final Executor delegate; private final TransactionalCacheManager tcm = new TransactionalCacheManager();
让我们回想一下他的创建时机,没错就是在每次创建一个新的SqlSession时创建出来的,源码如下,这就出现了一个惊天的大问号!!!,一级缓存和二级缓存为啥就一个属于SqlSession级别,另一个却被所有的SqlSession共享了? 这不是开玩笑呢? 我当时确实也是真的蒙,为啥他俩都是随时用随时new,包括上面代码中的TransactionalCacheManager
也是随时用随时new,凭什么它维护的二级缓存就这么牛? SqlSession挂掉后一级缓存也跟着挂掉,凭什么二级缓存还在呢?
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; }
先说一下,我是看到哪行代码后意识到二级缓存是这么特殊的,如下:大家也看到了,下面代码中的tcm.getObject(cache, key);
,是我们上面新创建出来的TransactionalCacheManager
,然后通过这个空白的对象的getObject()
竟然就将缓存中的对象给获取出来了,(我当时忽略了入参位置的cache,当然现在看,满眼都是这个cache)
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) { flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
我当时出现这个问题完全是我忽略了一部分前面解析配置文件部分的源码,下面我带大家看看这部分源码是怎么执行的
一开始MyBatis会创建一个XMLConfigBuilder
用这个builder去解析配置文件(因为我们环境是单一的MyBatis,并没有和其他框架整,这个builder就是用来解析配置文件的)
我们关注什么呢? 我们关注的是这个builder解析<mapper>
标签的,源码入下:
private void parseConfiguration(XNode root) { try { //issue #117 read properties first propertiesElement(root.evalNode("properties")); ... databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); mapperElement(root.evalNode("mappers"));
关注这个方法中的configuration.addMapper(mapperInterface);
方法,如下: 这里面存在一个对象叫做,MapperRegistry,这个对象叫做mapper的注册器,其实我觉得这是个需要记住的对象,因为它出现的频率还是挺多的,它干什么工作呢? 顾名思义,解析mapper呗 我的当前是基于注解搭建的环境,于是它这个MapperRegistry为我的mapper生成的对象就叫MapperAnnotationBuilder
见名知意,这是个基于注解的构建器
public <T> void addMapper(Class<T> type) { mapperRegistry.addMapper(type); }
所以说我们就得去看看这个解析注解版本mapper的builder,到底是如何解析我提供的mapper的,源码如下:
public void parse() { String resource = type.toString(); if (!configuration.isResourceLoaded(resource)) { loadXmlResource(); configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); parseCache(); parseCacheRef(); Method[] methods = type.getMethods(); for (Method method : methods) { try { // issue #237 if (!method.isBridge()) { parseStatement(method); } } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } }
方法千千万,但是我关注的是它的parseCache();
方法,为什么我知道来这里呢? (我靠!,我找了老半天...)
接下来就进入了一个高潮,相信你看到下面的代码也会激动, 为什么激动呢? 因为我们发现了Mybatis处理@CacheNamespace
注解的细节信息
private void parseCache() { CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class); if (cacheDomain != null) { Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size(); Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval(); Properties props = convertToProperties(cacheDomain.properties()); assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props); } }
再往下跟进这个assistant.useNewCache()
方法,就会发现,MyBatis将创建出来的一个Cache对象,这个Cache的实现类叫BlockingCache
创建出来的对象给谁了?
- Configuration对象自己留了一份 (放在了 caches = new StrictMap<>("Caches collection");中)
- 当前类
MapperBuilderAssistant
也保留一了一份 - 最主要的是
MappedStatement
对象中也保留了一份mappedStatement.cache
说了这么多了,附上一张图,用来纪念创建这个Cache的成员
小结#
其实上面创建这个Cache对象才是二级缓存者, 前面说的那个CachingExecutor
中的TransactionalCacheManager
不过是拥有从这个Cache中获取数据的能力而已
我有调试他是如何从Cache中获取出缓存,事实证明,二级缓存中存放的不是对象,而是被序列化后存储的数据,需要反序列化出来
下图是Mybatis反序列化数据到新创建的对象中的截图
下图是TransactionalCacheManager
是如何从Cache中获取数据的调用栈的截图
二级缓存与一级缓存的互斥性#
第一点: 通过以上代码的调用顺序也能看出,二级缓存在一级缓存之前优先被执行, 也就是说二级缓存不存在,则查询一级缓存,一级缓存再不存在,就查询DB
第二点: 就是说,对于二级缓存来说,无论我们有没有开启事务的自动提交功能,都必须手动commit()
二级缓存才能生效,否则二级缓存是没有任何效果的
第三点: CachingExecutor
提交事务时的源码如下:
@Override public void commit(boolean required) throws SQLException { // 代理执行器提交 delegate.commit(required); // 事务缓存管理器提交 tcm.commit(); }
这就意味着,TransactionalCacheManager和BaseExecutor的实现类的事务都会被提交
为什么说二级缓存和以及缓存互斥呢?可以看看BaseExecutor的源码中commit()
如下: 怎么样? 够互斥吧,一个不commit()
就不生效,commit()
完事把一级缓存干掉了
@Override public void commit(boolean required) throws SQLException { if (closed) { throw new ExecutorException("Cannot commit, transaction is already closed"); } clearLocalCache(); flushStatements(); if (required) { transaction.commit(); } }