大家好,我是王有志。今天给大家带来的是一道来自光大科技的 MyBatis 面试题:详细描述MyBatis缓存的实现原理。
在通过源码分析 MyBatis 一二级缓存的实现原理前,我先给出我的回答。
首先是 MyBatis 一级缓存的实现原理:
MyBaits 的一级缓存是默认开启的,作用域是 SqlSession 实例,所以 MyBatis 一级缓存在 SqlSession 之间是隔离的。MyBatis 应用程序中,每个 SqlSession 实例都会持有 Execuutor 实例,而 Executor 实例中又会持有 Cache 实例,这个 Cache 实例就是 MyBatis 的一级缓存。因为每个 SqlSession 实例是相互独立且隔离的,因此 Cache 实例在 SqlSession 之间也是相互独立且隔离的。
接着来看 MyBatis 二级缓存的实现原理:
MyBatis 的二级缓存是默认关闭的,作用域是 SqlSessionFactory 实例,所以 MyBatis 二级缓存在 SqlSesiion 之间是共享的。MyBatis 应用程序在启动时,解析 MyBatis 的核心配置文件 mybatis-config.xmk 生成 Configuration 实例,当解析到 XML 映射器时,会先根据 XML 映射器的配置创建 MyBatis 二级缓存的 Cache 实例,在之后解析 XML 映射器的每个 SQL 语句并生成相应的 MappedStatement 实例时,会将 MappedStatement 实例中的 cache 指向创建的 Cache 实例,并将所有根据 XML 映射器配置生成的 Cache 实例存储到 Configuration 实例的 caches 中。因为 MappedStatement 实例是按照每个 XML 映射器的 namesapce 划分的,因此我们会说 MyBatis 二级缓存的作用域是 XML 映射器的 namesapce,有因为 MappedStatement 实例被 Configuration 实例持有,且 Configuration 实例被 SqlSessionFactory 实例持有,我们也可以说 MyBatis 二级缓存的作用域是 SqlSessionFactory。
下面我们来通过源码分析 MyBatis 一二级缓存的实现原理。
原理分析
我们先通过一张图来构建出 MyBatis 中一二级缓存与 MyBatis 各个组件之间的关系,如下所示:
上图中,我只画出了 MyBatis 应用中与缓存相关的组件,这样能够帮助我们排除其它组件的干扰,清晰的了解到 MyBatis 的缓存与各组件之间的关系。接下来我们通过源码的角度来简单剖析 MyBatis 缓存的实现原理。
Tips:我在掘金专栏中的两篇文章《MyBatis中一级缓存的配置与实现原理》和《MyBatis中二级缓存的配置与实现原理》有更加详细的源码分析,感兴趣的可以去看看。
MyBatis 的一级缓存
从上图中,我们已经可以看到 MyBatis 的一级缓存位于 SqlSession 实例持有的 Executor 实例中,那么我们就从 MyBatis 中获取 SqlSession 实例的源码入手:
我们从DefaultSqlSessionFactory#openSession
方法入手,通过追踪源码的调用逻辑可以得知,MyBatis 的一级缓存实际上是由 Executor 的抽象父类 BaseExecutor 创建的,由于每个 SqlSession 都会持有 Executor 实例,因此每个 SqlSession 都会创建 MyBatis 一级缓存,所以在创建时 MyBatis 的一级缓存就已经想相互隔离了。
接着我们来看 MyBatis 一级缓存的使用,我们直接来看BaseExecutor#query
方法,部分源码如下:
public abstract class BaseExecutor implements Executor {
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
}
return list;
}
}
第 6 行源码中,通过 BaseExecutor 的 localCache 获取数据,如果获取成功执行第 8 行的BaseExecutor#handleLocallyCachedOutputParameters
方法处理数据,否则执行第 10 行的BaseExecutor#queryFromDatabase
方法,通过数据库获取数据,该方法的部分源码如下:
public abstract class BaseExecutor implements Executor {
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
}
第 6 行代码执行的BaseExecutor#doQuery
方法是抽象方法,由 BaseExecutor 子类实现,主要的功能是实现通过数据库查询数据,第 10 行代码中将查询出的数据存入到 localCache 中。
MyBatis 的二级缓存
相较于 MyBatis 的一级缓存,MyBatis 二级缓存的逻辑就复杂了很多,我们先来看 MyBatis 二级缓存的创建逻辑。
从 MyBatis 缓存与 MyBatis 组件之间的关系图中可以看到,MyBatis 的二级缓存存储在 Configuration 实例中,同时每个 MappedStatement 实例中也持有指向 MyBatis 二级缓存的引用,那我们就从创建 Configuration 实例和创建 MappedStatement 实例的源码入手,如下所示:
MyBatis 创建二级缓存的逻辑会复杂很多,简单总结一下就是在创建 MyBatiis 运行环境 SqlSessionFactory 时,通过解析 XML 映射器文件配置创建缓存,将缓存添加到 Configuration 实例中,并且在每个 XML 映射器中的 MappedStatement 实例中保存指向缓存的引用。
因为 MyBatis 中 SqlSession 实例是通过 SqlSessionFactory 获取的,使用 SqlSession 实例执行 SQL 语句时,会通过 SqlSessionFactory 实例获取对应的 MappedStatement 实例,而 MappedStatement 实例在不同的 SqlSession 之间共享,因此 MappedStatement 中持有的 MyBatis 二级缓存在不同的 SqlSession 实例之间也是共享的。
Tiips:将缓存添加到 Configuration 实例中源码在MapperBuilderAssistant#useNewCache
方法中的第 7 行。
接着我们来看 MyBatis 二级缓存的使用,与 MyBatis 一级缓存不同的是,在使用 MyBatis 二级缓存时,创建的 Executor 类型是 CachingExecutor,我们直接来看CachingExecutor#query
方法,源码如下:
public class CachingExecutor implements Executor {
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);
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
}
第 3 行源码中,通过 MappedStatement 实例获取 Cache 实例,第 9 行源码中,通过 Cache 实例获取数据,如果获取到数据则直接返回,否则执行第 11 行代码,通过数据库查询数据。
MyBatis 中使用一级缓存和二级缓存的逻辑都非常简单,重点在于 MyBatis 一级缓存和二级缓存的创建,MyBatis 一级缓存的创建逻辑比较简单,但是创建 MyBatis 二级缓存的逻辑会比较复杂,大家可以通过源码并结合最开始给出的结构图来理解 MyBatis 二级缓存的创建过程。