上一篇我们介绍了mybatis的一级缓存, 讲解了一级缓存与会话的关系, 一级缓存的生命周期, 一级缓存查询执行的过程等, 其中也有提到二级缓存的地方, 但是都暂且略过了, 而今天这次我们就要来嗑一嗑mybatis二级缓存与一级缓存的关系 ~ 友情提示: 搭配 https://www.jianshu.com/p/36a1d8cf830e食用更香。
NO.1 |思维发散
二级缓存是用来解决一级缓存不能跨会话共享的问题,范围是namespace级别,可以被多个sqlSession(会话)共享, 生命周期和应用同步。默认关闭。
通过对一级缓存的学习我们知道, 一级缓存是默认开启的, 如果我们同时开启了二级缓存, 那就势必存在一级缓存和二级缓存都要使用的情况。这样一来我们要思考的第一个问题产生了, 一级缓存和二级缓存的执行顺序是怎样的呢?
还是先推断一下, 二级缓存作为一个作用范围更广的缓存(可以跨会话), 从节省资源的角度来设计, 二级缓存肯定是要工作在一级缓存(不能跨会话)之前的。也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。如果你的MyBatis使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取数据,取不到才会去走一级缓存,一级缓存中也取不到, 就会与数据库进行交互了。即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库。
按照这种执行顺序设计来思考, 一级缓存已经利用BaseExecutor完成了自身功能的实现, 那么二级缓存要加在哪里进行维护,才合适呢? 实际上MyBatis这里用了一个设计模式——装饰器模式来维护二级缓存,实现这个功能的类就是CachingExecutor(缓存执行器)。
tips:装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装
具体是怎么做的呢?MyBatis让CachingExecutor对BaseExecutor进行了包装。CachingExecutor中不仅要实现了二级缓存的功能, 同时也要持有一个基础执行器(BaseExecutor)。当查询请求来临的时候,CachingExecutor会先判断二级缓存是否有缓存结果,如果有就直接返回,如果没有则委派交给自己持有的BaseExecutor实现类,比如SimpleExecutor(简单执行器)来执行查询, 这样就顺理成章的从二级缓存过渡到了一级缓存的执行流程了。最后会把得到结果缓存起来,并且返回给用户。
实际上是否是如此呢, 又到了喜闻乐见的放源码时间。
NO.2 |源码论证
(1)还是要从创建会话讲起。
创建会话的过程中, 会先创建执行器(具体创建执行器过程参见(2)), 这个执行器便是CachingExecutor了。然后把得到的执行器——CachingExecutor交给DefaultSqlSession(会话)来持有。
DefaultSqlSessionFactory:
@Overridepublic SqlSession openSession(ExecutorType execType) {
return openSessionFromDataSource(execType, null, false);
}
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);
// 注意:看这里!! 创建Executor执行器, 这里创建的是CachingExecutor
final Executor executor = configuration.newExecutor(tx, execType);
// 创建DefaultSqlSession
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();
}
}
(2)创建执行器部分源码。
在newExecutor()方法中,首先还是要创建基础执行器(BaseExecutor)的子类, 毕竟一级缓存的逻辑还要依靠它去完成。如果开启了缓存, 最后要创建一个缓存执行器(CachingExecutor), 并把之前创建好的基础执行器(BaseExecutor)的子类作为CachingExecutor的有参构造的参数传入, 让CachingExecutor持有BaseExecutor。并返回CachingExecutor让DefaultSqlSession(会话)来持有。
Configuration:
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;
}
(3)执行查询部分源码
Mybatis是以sqlSession.selectList()方法, 作为查询的入口。可以看见,在selectList() 方法中,调用了executor.query()方法来获取数据, 而sqlSession持有的Executor正是缓存执行器(CachingExecutor)。也就是说这里调用的是CachingExecutor的query()方法。
DefaultSqlSession:
private final Configuration configuration;
private final Executor executor;
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
// 注意:看这里 !! 调用执行器的查询方法,
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
在CachingExecutor的query()方法中, 如果使用了二级缓存并且二级缓存存在, 则先去二级缓存中查找数据, 如果数据存在则返回数据。如果数据不存在,会直接委派给一级缓存进行查询。(二级缓存本身的组件结构和实现逻辑没有写出来, 不要急, 下次会写哒!)
CachingExecutor:
private final Executor delegate;//注意: 看这里!! 这里持有的便是基础执行器的子类
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
@Override
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);
// 注意:看这里!! 从二级缓存中查询数据
List<E> list = (List<E>) tcm.getObject(cache, key);
// 注意:看这里!! 二级缓存中没有数据, 委托给BaseExecutor执行
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 委托给BaseExecutor执行
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
到这里就完成了我们整个一级缓存与二级缓存结构关系的介绍。
NO.3 |结构与总结
总结:sqlSession 持有 CachingExecutor, CachingExecutor来完成二级缓存的功能实现,并且持有BaseExecutor , 在二级缓存开启并且查不到数据时(或者二级缓存本身没有开启), 都会委派给BaseExecutor来执行查询。
整体结构图如下:
怎么样, 现在对mybatis二级缓存与一级缓存的关系是不是有了大概的了解~
这里除了编程知识分享,同时也是我成长脚步的印记, 期待与您一起学习和进步,我在公众号更新会更快些, 可以扫描下方二维码关注我的公众号: 程序媛swag。如果您觉得这篇文章帮助到了您, 请帮忙点赞!