MyBaits的二级缓存机制及其实现原理详解
保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/78/ )。
MyBatis具有两级缓存,那它的两级缓存是如何实现的呢?本文将详细讨论相关的实现细节。
本文内容参考《通用源码阅读指导书——MyBatis源码详解》一书。
在介绍MyBatis的缓存机制之前,我们先提前了解下MyBatis的Executor接口的概况。
Executor接口是执行器接口,它负责进行数据库查询等操作。它有两个直接子类,CachingExecute类和BaseExecute类:
- CachingExecute是一个装饰器类,它能够为执行器实现类增加缓存功能。
- BaseExecute类是所有实际执行器类的基类,它有SimpleExecutor、BatchExecutor、ReuseExecutor、ClosedExecutor四个子类。而其中的ClosedExecutor类本身没有实际功能,我们暂时忽略它。
因此,最终我们可以给出下图所示的Executor接口的简化类图。
1 一级缓存
MyBatis的一级缓存又叫本地缓存,它的结构和使用都比较简单,与它相关的配置项有两个。
一个是在配置文件的settings节点下,我们可以增加如下所示的配置语句来改变一级缓存的作用范围。配置值的可选项有SESSION与STATEMENT,分别对应了一次会话和一条语句。一级缓存的默认作用范围是SESSION。
<setting name="localCacheScope" value="SESSION"/>
二是可以在映射文件的数据库操作节点内增加flushCache属性项,如下所示,该属性可以配置true和false。当设置为true时,MyBatis会在该数据库操作执行前清空一二级缓存。该属性的默认值为false。
<select id="queryUserBySchoolName" resultType="User" flushCache="false">
SELECT * FROM `user` WHERE schoolName = #{schoolName}
</select>
了解了MyBatis一级缓存的配置后,我们查看一级缓存的源码。
一级缓存功能由BaseExecutor类实现。BaseExecutor类作为实际执行器的基类,为所有实际执行器提供一些通用的基本功能,在这里增加了缓存也就意味着每个实际执行器都具有这一级缓存。
在BaseExecutor内,我们可以看到与一级缓存相关的两个属性,分别是localCache和localOutputParameterCache,如下所示。这两个属性使用的都是没有经过任何装饰器装饰的PerpetualCache对象。
// 查询操作的结果缓存
protected PerpetualCache localCache;
// Callable查询的输出参数缓存
protected PerpetualCache localOutputParameterCache;
这两个变量中,localCache缓存的是数据库查询操作的结果。对于CALLABLE形式的语句,因为最终向上返回的是输出参数,便使用localOutputParameterCache直接缓存的输出参数。
因为localCache和localOutputParameterCache都是Executor的属性,不可能超出Executor的作用范围。而Executor归属SqlSession,因此第一级缓存的最大作用范围便是SqlSession,即一次会话。
下面代码给出了BaseExecutor中query操作的源码,通过它我们可以详细了解一级缓存的作用原理以及localCacheScope配置、flushCache配置如何生效。
/**
* 查询数据库中的数据
* @param ms 映射语句
* @param parameter 参数对象
* @param rowBounds 翻页限制条件
* @param resultHandler 结果处理器
* @param key 缓存的键
* @param boundSql 查询语句
* @param <E> 结果类型
* @return 结果列表
* @throws SQLException
*/
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
// 执行器已经关闭
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) { // 新的查询栈且要求清除缓存
// 清除一级缓存
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 尝试从本地缓存获取结果
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 本地缓存中有结果,则对于CALLABLE语句还需要绑定到IN/INOUT参数上
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();
// 如果本地缓存的作用域为STATEMENT,则立刻清除本地缓存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
}
return list;
}
数据库操作中的INSERT、UPDATE、DELETE操作都对应了BaseExecutor中的update方法。在update方法中,会引发一级缓存的更新。下面代码展示了BaseExecutor中update方法的源码。
/**
* 更新数据库数据,INSERT/UPDATE/DELETE三种操作都会调用该方法
* @param ms 映射语句
* @param parameter 参数对象
* @return 数据库操作结果
* @throws SQLException
*/
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
// 执行器已经关闭
throw new ExecutorException("Executor was closed.");
}
// 清理本地缓存
clearLocalCache();
// 返回调用子类进行操作
return doUpdate(ms, parameter);
}
可见一级缓存就是BaseExecutor中的两个PerpetualCache类型的属性,其作用范围很有限,不支持各种装饰器的修饰,因此不能进行容量配置、清理策略设置、阻塞设置等。
2 二级缓存
二级缓存的作用范围是一个命名空间(即一个映射文件),而且可以实现多个命名空间共享一个缓存。因此与一级缓存相比其作用范围更广,且选择更为灵活。
与二级缓存相关的配置项有四项。
第一个配置项在配置文件的settings节点下,我们可以增加如下所示的配置语句来启用和关闭二级缓存。该配置项默认值为true,即默认启用二级缓存。
<setting name="cacheEnabled" value="true"/>
第二个配置项在映射文件内。可以使用下面所示的cache标签来开启并配置本命名空间的缓存。
<cache type="PERPETUAL"
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"
blocking="true">
<!--可以加入property节点,将用来直接修改Cache实现类及装饰器类的属性-->
</cache>
也可以使用下面所示的标签来声明本命名空间使用其他命名空间的缓存,如果两项都不配则表示命名空间没有缓存。该项配置只有在第一项配置选择启用二级缓存时才有效。
<cache-ref namespace="com.github.yeecode.mybatisdemo.dao.UserMapper"/>
第三个配置项为数据库操作节点内的useCache属性,如下所示。通过它可以配置该数据库操作节点是否使用二级缓存。只有当第一、二项配置均启用了缓存时,该项配置才有效。对于SELECT类型的语句而言,useCache属性的默认值为true,对于其他类型的语句而言则没有意义。
<select id="queryUserBySchoolName" resultType="User" flushCache="false" useCache="true">
SELECT * FROM `user` WHERE schoolName = #{schoolName}
</select>
第四个配置项为数据库操作节点内的flushCache属性项,该配置属性与一级缓存共用。表示是否要在该语句执行前清除一二级缓存。
了解了二级缓存的配置项之后,我们通过源码来了解二级缓存的详细原理。
二级缓存功能由CachingExecutor类实现,它是一个装饰器类,能通过装饰实际执行器为它们增加二级缓存功能。如下所示,在Configuration的newExecutor方法中,MyBatis会根据配置文件中的二级缓存开关配置用CachingExecutor类装饰实际执行器。
/**
* 创建一个执行器
* @param transaction 事务
* @param executorType 数据库操作类型
* @return 执行器
*/
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);
}
// 根据配置文件中settings节点cacheEnabled配置项确定是否启用缓存
if (cacheEnabled) { // 如果配置启用缓存
// 使用CachingExecutor装饰实际执行器
executor = new CachingExecutor(executor);
}
// 为执行器增加拦截器(插件),以启用各个拦截器的功能
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
在阅读CachingExecutor类的源码之前,我们先讨论另外一个概念:事务。我们知道,在数据库操作中,可以将多条语句封装为一个事务;而在我们没有显式地声明事务时,数据库会为每条语句开启一个事务。于是,事务不仅可以代指封装在一起的多条语句,也可以用来代指普通的一条语句。
CachingExecutor类中由两个属性,如下面代码所示。其中delegate是被装饰的实际执行器,tcm是事务缓存管理器。既然一条语句也是一个事务,那事务缓存管理器可以应用在有事务的场景,也可以应用在无事务的场景。
// 被装饰的执行器
private final Executor delegate;
// 事务缓存管理器
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
了解了这些之后,我们查看CachingExecutor这一装饰器类的query核心方法,如下面代码所示。在下面代码中,我们详细注释了整个工作的过程。
/**
* 查询数据库中的数据
* @param ms 映射语句
* @param parameterObject 参数对象
* @param rowBounds 翻页限制条件
* @param resultHandler 结果处理器
* @param key 缓存的键
* @param boundSql 查询语句
* @param <E> 结果类型
* @return 结果列表
* @throws SQLException
*/
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 获取MappedStatement对应的缓存,可能的结果有:该命名空间的缓存、共享的其他命名空间的缓存、无缓存
Cache cache = ms.getCache();
// 如果映射文件未设置<cache>或<cache-ref>则,此处cache变量为null
if (cache != null) { // 存在缓存
// 根据要求判断语句执行前是否要清除二级缓存,如果需要,清除二级缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) { // 该语句使用缓存且没有输出结果处理器
// 二级缓存不支持含有输出参数的CALLABLE语句,故在这里进行判断
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);
}
其中的flushCacheIfRequired子方法是用来判断并清除二级缓存的方法。源码如下所示。
/**
* 根据要求判断语句执行前是否要清除二级缓存,如果需要,清除二级缓存
* 注意:默认情况下,非SELECT语句的isFlushCacheRequired方法会返回true
* @param ms MappedStatement
*/
private void flushCacheIfRequired(MappedStatement ms) {
// 获取MappedStatement对应的缓存
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) { // 存在缓存且该操作语句要求执行前清除缓存
// 清除事务中的缓存
tcm.clear(cache);
}
}
而CachingExecutor的update方法(对应INSERT、UPDATE、DELETE三种数据库操作)也会调用flushCacheIfRequired方法,而对于这些语句isFlushCacheRequired子方法恒返回true。因此,总会导致二级缓存的清除。
3 两级缓存机制
现在我们已经清楚MyBatis存在两级缓存,其中一级缓存是BaseExecutor通过两个PerpetualCache类型的属性提供,而二级缓存由CachingExecutor包装类提供。
那在数据库查询操作中,是先访问一级缓存还是先访问二级缓存呢?为了便于讨论,我们再次附上Executor接口的简化的类图,如下图所示。
答案并不复杂,CachingExecutor作为装饰器会先运行,然后才会调用到实际执行器,这时BaseExecutor中的方法才会执行。因此,在数据库查询操作中,MyBatis先访问二级缓存再访问一级缓存。
这样,我们便可以得到下图所示的MyBatis两级缓存示意图。
以上内容均参考《通用源码阅读指导书——MyBatis源码详解》一书。
这是一本以MyBatis的源码为实例讲述源码阅读方法的书籍,并且附带有示例项目源码,MyBatis的全中文注释。书籍还总结了大量的编程知识和架构经验,对提升编程和架构能力十分有用。推荐给大家。
可以访问个人知乎阅读更多文章:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。
