大家好,我是王有志,一个分享硬核 Java 技术的金融摸鱼侠,欢迎大家加入 Java 人自己的交流群“共同富裕的 Java 人”。
上一篇文章《MyBatis中一级缓存的配置与实现原理》中,我们已经掌握了 MyBatis 一级缓存的配置(虽然根本不需要做什么配置)与原理,那么今天我们就来学习 MyBatis 的二级缓存。
首先我们来问一个问题:为什么要使用 MyBatis 的二级缓存呢?
因为 MyBatis 的一级缓存是基于 SqlSession 实例的,在不同的 SqlSession 实例之间是相互隔离的,但是通常来说,我们的应用程序中不会只有一个 SqlSession 实例,如果想要在所有 SqlSession 实例中共享缓存,MyBatis 的一级缓存是无法实现的,这时候就要使用 MyBatis 的二级缓存。
Tips:本文不讨论缓存的设计与实现,因此在涉及到 FIFO,LRU 等常见的缓存淘汰策略时,不会进行深入的讨论。
配置与使用
MyBatis 的二级缓存是基于映射器文件的 namespace 的,在《MyBatis 的应用组成》中我们提到过:
Configuration 是核心配置文件 mybatis-config.xml 在 Java 应用程序中的体现,是 MyBatis 在整个运行周期中的配置信息管理器,包含了 MyBatis 运行期间所需要的全部配置信息和映射器。
由于每个 MyBatis 应用程序都是以一个 SqlSessionFactory 实例为核心的,并且每个 SqlSessionFactory 实例中都只会持有一个 Configuration 实例,而 Configuration 实例中会保存 MyBatiis 应用程序中所有的配置信息和映射器,因此在这种意义上我们也可以说,MyBatis 二级缓存的作用域是整个 SqlSessionFactory 实例(通常一个 MyBatis 应用程序只需要一个 SqlSessionFactory 实例),这使得通过 SqlSessionFactory 实例获取到的 SqlSession 实例能够共享 MyBatis 的二级缓存。
MyBatis 二级缓存默认是关闭的,需要进行相应的配置才能开启,在不进行任何自定义 MyBatis 二级缓存配置的场景中,使用 MyBatis 的二级缓存需要满足 4 个条件:
- mybatis-config.xml 中的 cacheEnabled 配置处于默认状态(默认状态即开启状态)或开启状态;
- 映射器中需要添加配置;
- 映射器中返回的 Java 对象必须是可序列化的,即需要实现 Serializable 接口;
- 使用时,只有提交(执行
SqlSession#commit
方法)或关闭(执行SqlSession#close
方法)时数据才会被提交到缓存中。
这里我们使用支付订单的的接口与 Java 对象来举个例子,首先是为 PayOrderDO 实现 Serializable 接口,代码如下:
public class PayOrderDO implements Serializable {
@Serial
private static final long serialVersionUID = -6344099430949558150L;
// 省略其它字段
}
接着我们为 PayOrderMapper.xml 中添加配置,配置如下:
<mapper namespace="com.wyz.mapper.PayOrderMapper">
<cache/>
<!-- 省略其它配置 -->
</mapper>
最后我们写一个单元测试,来测试 MyBatis 二级缓存的执行情况,代码如下:
public class SecondLevelCacheTest {
public void testSecondLevelCache() {
Reader mysqlReader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mysqlReader);
System.out.println("第一次查询,使用通过【sqlSession】获取到的【payOrderMapper】查询【payOrder】");
SqlSession sqlSession = sqlSessionFactory.openSession();
PayOrderMapper payOrderMapper = sqlSession.getMapper(PayOrderMapper.class);
PayOrderDO payOrder = payOrderMapper.selectPayOrderByOrderId(1);
System.out.println(JSON.toJSONString(payOrder));
sqlSession.commit();
// 第二次查询,验证二级缓存
System.out.println("第二次查询,使用通过【newSqlSession】获取到的【newPayOrderMapper】查询【newPayOrder】");
SqlSession newSqlSession = sqlSessionFactory.openSession();
PayOrderMapper newPayOrderMapper = newSqlSession.getMapper(PayOrderMapper.class);
PayOrderDO newPayOrder = newPayOrderMapper.selectPayOrderByOrderId(1);
System.out.println(JSON.toJSONString(newPayOrder));
newSqlSession.commit();
}
}
执行单元测试,我们来看控制台的输出:
首先我们看到控制台中只有第一次查询时输出了执行的 SQL 语句,这表明只有第一次查询时 MyBaits 是通过与数据库交互获取到的数据。
接着我们再来看输出的“Cache Hit Ratio [com.wyz.mapper.PayOrderMapper]”,这行输出的是 MyBatis 二级缓存的命中率,第一次输出的是 0.0,这是因为首次查询 MyBatis 的二级缓存中还没有任何数据,因此命中率是 0.0,那么第二次查询输出的 0.5 就很容易理解了。
MyBatis 二级缓存的默认配置
前面的例子中,我们使用的是 MyBatis 二级缓存的默认配置,这个配置有以下 6 个特点:
- 所有 select 查询语句的结果都会被缓存;
- 执行 insert 语句,update 语句和 delete 语句后会刷新(清空)缓存;
- 缓存默认使用 LRU 淘汰策略来实现缓存淘汰;
- 缓存不会进行定时刷新;
- 缓存的默认大小是 1024 个;
- 缓存被设置为读/写缓存,这表示通过缓存获取到的对象并不是共享的,并且允许修改缓存。
其中第 1 点和第 2 点的特性是 MyBatis 中二级缓存通用的特性,即无论如何配置 MyBatis 的二级缓存,总是会在执行 select 语句后将查询到的数据进行缓存,而在执行 insert 语句,update 语句和 delete 语句后刷新(清空)缓存。而第 3 点到第 6 点的特性,则是根据 MyBatis 二级缓存的具体配置来决定的。
关于 MyBatis 二级缓存的默认配置,我还会在后面的实现原理的部分和大家一起通过源码进行分析,现在我们先来看自定义 MyBatis 二级缓存的配置。
自定义 MyBaits 二级缓存的配置
除了使用默认配置外,我们还可以通过对 MyBatis 的二级缓存进行定制化配置,MyBatis 为二级缓存提供了 6 项配置属性:
- type 属性,用于指定缓存的实现类,通常在没有使用自定义缓存的场景下,我们不需要配置,如果使用自定义缓存,要实现
org.apache.ibatis.cache.Cache
接口; - eviction 属性,用于设置缓存的淘汰策略,MyBatis 中提供了 4 中缓存的淘汰策略:
- LRU 策略,最近最少使用策略,也是常见的缓存淘汰策略,优先移出最近最少访问的数据;
- FIFO 策略,先进先出策略,根据数据缓存的顺序,淘汰最早被缓存的数据;
- SOFT 策略,软引用策略,使用 Java 中的软引用,当内存不足时,Java 的垃圾回收器会回收这些数据;
- WEAK 策略,弱引用策略,使用 Java 中的弱引用,相比于 SOFT 策略更为激进,即便内存充足,也会回收被设置为弱引用的数据。
- flushInterval 属性,用于设置缓存的刷新(清空)时间间隔,单位为毫秒,当到达 flushInterval 设置的时间间隔后,即便内存充足,MyBatis 的二级缓存也会被刷新(清空);
- size 属性,用于设置缓存的最大容量,限制缓存出处数据的数量,当缓存的数据到大这个数量时会根据相应的淘汰策略淘汰数据;
- readOnly 属性,用于设置缓存中数据是否为只读数据:
- 只读(true):只读状态下,数据不允许被修改,缓存会返回数据的相同实例;
- 读写(false):读写状态下,数据允许被修改,缓存会通过序列化的方式返回数据的拷贝。
- blocking 属性,用于控制 MyBatis 二级缓存的并发行为,允许填入 true 或 false,开启 blocking 配置后在并发环境中,只有一个线程能够访问和更新数据,其余线程会被阻塞。
配置都是缓存中常见的配置,接下来我们就一起来修改一下 PayOrderMapper.xml 的缓存配置,代码如下:
<mapper namespace="com.wyz.mapper.PayOrderMapper">
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true" />
<!-- 省略其它配置 -->
</mapper>
这个配置非常简单,相信大家可以很轻松的看懂配置的内容,这里我就不多做解释了。
我们直接来执行单元测试SecondLevelCacheTest#testSecondLevelCache
方法,并将断点打在第 18 行,来观察通过缓存查询出来的数据是否与通过数据库查询出来的数据是相同的实例:
可以看到,通过缓存获取到的数据 newPayOrder 与通过数据库获取到的数据 payOrder 是相同的实例,你可以删除掉 readOnly 的配置,再来测试看下 newPayOrder 与 payOrder 是否使用同一个实例。
关于其它属性的配置效果,就留给大家自行测试了,下面我们就一起通过源码来分析 MyBatis 二级缓存的实现原理。
实现原理
本文的开始我们就说过“MyBatis 的二级缓存是基于映射器文件的 namespace 的”,并且在配置 MyBatis 的二级缓存时,我们也是通过映射器中的配置实现的,因此我们很容易就能够联想到,MyBatis 会在解析映射器文件时创建 MyBatis 的二级缓存。
创建逻辑分析
我们直接找到 MyBatis 中解析映射器文件的XMLMapperBuilder#configurationElement
方法,该方法用于解析每个映射器中的配置,部分源码如下:
private void configurationElement(XNode context) {
String namespace = context.getStringAttribute("namespace");
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
}
我们忽略解析其它配置的部分,直接来看第 5 行中解析 cache 元素调用的XMLMapperBuilder#cacheElement
方法,部分源码如下:
private void cacheElement(XNode context) {
// 获取配置中缓存的类型
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
// 获取配置中缓存的淘汰策略
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
// 获取配置中缓存刷新(清空)的时间间隔
Long flushInterval = context.getLongAttribute("flushInterval");
// 获取配置中缓存的大小
Integer size = context.getIntAttribute("size");
// 获取配置中缓存读写配置
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
// 获取配置中缓存并发控制配置
boolean blocking = context.getBooleanAttribute("blocking", false);
// 调用 MapperBuilderAssistant#useNewCache 创建缓存
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
每行代码的作用我已经添加了注释,这里主要是解析映射器中 cache 元素的配置,如果没有进行相关配置则使用默认配置(除了缓存大小的配置),我们可以使用前面提到的默认配置与源码做一个对比,看看是否符合源码中的实际情况。
我们来看第 23 行中调用的MapperBuilderAssistant#useNewCache
方法,该方法用于根据解析到的配置创建 MyBatis 的二级缓存,源码如下:
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;
}
见名知意, MapperBuilderAssistant 类是用于构建 MyBatis 映射器的助手类,MapperBuilderAssistant#useNewCache
方法中,根据当前解析的映射器中的 cache 元素的配置创建了 Cache 实例,并将 Cache 实例添加到 Configuration 实例的 caches 字段中,以及将自身(MapperBuilderAssistant 实例)的 currentCache 字段指向了刚刚创建的 Cache 实例,也就是说,Configuration 实例会保存所有映射器的缓存。
除此之外,我们还需要关注下Cache#build
方法(具体源码我们就不展示了),MyBatis 中的 Cache 设计使用了委派模式,每种 Cache 的实现类提供了一种功能,通过组合不同的 Cache 实现类,实现了 MyBatis 中 Cache 的不同能力,如图是在我们这个例子中 Cache 的委派链。
到这里为我们已经能够看到,作为构建 SqlSessionFactory 的核心 Configuration 实例中已经持有了 Cache 实例。但是还没结束,我们回到XMLMapperBuilder#configurationElement
方法中,来看第 9 行调用的XMLMapperBuilder#buildStatementFromContext
方法,该方法用于解析映射器中的 SQL 语句,并创建 SQL 语句在 MyBatis 应用中的 MappedStatement 实例,部分源码如下:
private void buildStatementFromContext(List<XNode> list) {
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
statementParser.parseStatementNode();
}
}
XMLMapperBuilder#buildStatementFromContext
方法是个重载方法,我们来看第 7 行中创建 XMLStatementBuilder 实例时传入的参数,包含 Configuration 实例,以及已经持有了当前解析的映射器 Cache 实例的 MapperBuilderAssistant 实例,XMLStatementBuilder 的构造方法我们就不看了,只有简单的字段赋值,没有额外的内容。
接着来看第 8 行中调用的XMLStatementBuilder#parseStatementNode
方法,部分源码如下:
public void parseStatementNode() {
// 省略前面解析参数的代码
builderAssistant.addMappedStatement(
id,
sqlSource,
statementType,
sqlCommandType,
fetchSize,
timeout,
parameterMap,
parameterTypeClass,
resultMap,
resultTypeClass,
resultSetTypeEnum,
flushCache,
useCache,
resultOrdered,
keyGenerator,
keyProperty,
keyColumn,
databaseId,
langDriver,
resultSets,
dirtySelect);
}
XMLStatementBuilder#parseStatementNode
方法会做一些其它配置的解析,这与我们的主题没有关系,我们往下追第 3 行调用的MapperBuilderAssistant#addMappedStatement
方法,部分源码如下:
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets,
boolean dirtySelect) {
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(flushCache)
.useCache(useCache)
.cache(currentCache)
.dirtySelect(dirtySelect);
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
参数非常多,不过不重要,我们只看与缓存相关的内容,我们来看第 24 行中创建 MappedStatement.Builder 实例的方法,这是一个建造者模式,接着看 MappedStatement.Builder 的构造方法,部分源码如下:
public static class Builder {
private final MappedStatement mappedStatement = new MappedStatement();
public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
mappedStatement.configuration = configuration;
mappedStatement.id = id;
mappedStatement.sqlSource = sqlSource;
mappedStatement.sqlCommandType = sqlCommandType;
}
}
可以看到 MappedStatement.Builder 实例本身就持有了 MappedStatement 实例,我们再来看MapperBuilderAssistant#addMappedStatement
方法第 42 行调用的MappedStatement.Builder#build
方法,部分源码如下:
public MappedStatement build() {
return mappedStatement;
}
这里是直接将 MappedStatement.Builder 实例持有的 MappedStatement 实例返回。
那么我们回到MapperBuilderAssistant#addMappedStatement
方法的第 40 行调用的MappedStatement.Builder#cache
方法:
public Builder cache(Cache cache) {
mappedStatement.cache = cache;
return this;
}
这里是直接将 MappedStatement 实例中 cache 指向了 MapperBuilderAssistant 中持有的 currentCache 实例,也就是说每个 MappedStatement 实例中都有执行当前映射器中二级缓存的引用。
至此,MyBatis 已经完成了二级缓存的创建,我们再来看看 MyBatis 中各个组件与二级缓存的关系:
- 每个 MappedStatement 实例中都保存着指向自己映射器中二级缓存(Cache 实例)的引用;
- Configuration 实例中保存着所有映射器的二级缓存(Cache 实例);
- SqlSessionFactory 实例中都保存着 Configuration 实例。
缓存类型
前面我们已经看到 MyBatis 二级缓存的创建过程,以及层层委派的结构,接下来我们就来看一看 MyBatis 的 Cache 体系:
MyBatis 中 Cache 接口有 11 个实现类,它们会根据配置信息进行组合,形成具有不同能力的 MyBatis 二级缓存。注意我上图的层级关系,也就是不同 Cache 的实现类在委派模式中处于的层级。我们由下至上,依次来看看每一层级中 Cache 实现类的功能:
- PerpetualCache 是 Cache 接口中最简单的实现,它提供了存储数据的能力,底层的容器是 HashMap,MyBaits 的一级缓存使用的也是 PerpetualCache;
- LruCache,FifoCache,SoftCache 和 WeakCache,它们 4 个本身并不存储数据,只是提供了缓存的淘汰策略;
- LruCache,使用 LinkedHashMap 存储缓存的 Key,LinkedHashMap 本身并不直接提供 LRU 算法,但是提供了 LRU 算法的基础,可以很容易的通过 LinkedHashMap 实现支持 LRU 算法的缓存,LruCache 借助 LinkedHashMap 获取最近最少使用的缓存的 Key,再使用 Key 删除 PerpetualCache 中的数据;
- FifoCache,使用 Deque 存储缓存的 Key,与 LruCache 一样,FifoCache 借助 Deque 的能力,获取最先入队的缓存的 Key,再使用 Key 删除 PerpetualCache 中的数据;
- SoftCache 和 WeakCache,两者的实现逻辑几乎一致,分别使用 SoftReference 和 WeakReference 包装缓存数据,再借助 Java 虚拟机对 SoftReference 和 WeakReference 的清除策略,实现 PerpetualCache 中数据的删除;
- ScheduledCache,不提供数据存储的能力,提供了“定期”刷新(清除)缓存的能力,实际上 ScheduledCache 并非真正的“定期”刷新,而是在每次存取数据时,计算当前时间与上次刷新(清除)缓存时间的差值是否超过了设置的间隔时间,从而决定是否刷新(清除)缓存;
- SerializedCache,不提供数据存储的能力,只是对存入 PerpetualCache 的数据进行序列化,从 PerpetualCache 取出数据时进行反序列化;
- LoggingCache,不提供数据存储的能力,用于计算缓存的命中率;
- SynchronizedCache,不提供数据存储的能力,使用 synchronized 关键字修饰存储数据与获取数据的方法,提供了并发环境下安全访问缓存的能力;
- BlockingCache,不提供数据存储的能力,它添加了锁机制,实现了阻塞版本的缓存,主要用于处理并发访问时缓存数据的同步问题;
- TransactionalCache,最特殊的缓存,它提为缓存提供了事务的能力,会将查询结果暂存到自身的容器中,只有当数据库事务成功提交时才会将数据提交到 PerpetualCache 中存储,而当事务失败时,会将暂存的数据删除。
上面的 Cache 实现中,除了 PerpetualCache 外每个实现类都会包含 delegate 字段,用于指向自己委派的 Cache 实现,如:
public class TransactionalCache implements Cache {
private final Cache delegate;
}
public class BlockingCache implements Cache {
private final Cache delegate;
}
public class SynchronizedCache implements Cache {
private final Cache delegate;
}
public class LoggingCache implements Cache {
private final Cache delegate;
}
public class SerializedCache implements Cache {
private final Cache delegate;
}
public class ScheduledCache implements Cache {
private final Cache delegate;
}
public class LruCache implements Cache {
private final Cache delegate;
}
public class FifoCache implements Cache {
private final Cache delegate;
}
public class SoftCache implements Cache {
private final Cache delegate;
}
public class WeakCache implements Cache {
private final Cache delegate;
}
接下来我们重点来看一下 TransactionalCache 的实现,这有助于我们理解后面的 MyBatis 二级缓存的存取过程,至于其它的 Cache 实现类,由于源码并不复杂,这里我就不和大家一起分析了。
TransactionalCache
首先来看 TransactionalCache 中提供了哪些字段,源码如下:
private final Cache delegate;
private boolean clearOnCommit;
private final Map<Object, Object> entriesToAddOnCommit;
private final Set<Object> entriesMissedInCache;
我们来逐个解释下这些字段你的作用
- delegate,指向被委派的缓存;
- clearOnCommit, 用于标记是否在事务提交前清空缓存;
- entriesToAddOnCommit,当事务未提交时,暂存数据;
- entriesMissedInCache,用于存储未能从缓存中获取到数据的 Key。
了解完上面 4 个字段之后,我们来看看它们在 TransactionalCache 中发挥的作用,我们先来看获取数据的逻辑,TransactionalCache#getObject方法的源码如下:
public Object getObject(Object key) {
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
if (clearOnCommit) {
return null;
}
return object;
}
逻辑非常简单,通过委派对象(delegate)获取数据,如果获取不到数据就将 Key 存储到 entriesMissedInCache 中,第 6 行的条件语句中会根据 clearOnCommit 来决定是否返回 null。
下面我们来看存储数据的逻辑,TransactionalCache#putObject
方法的源码如下:
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
比获取数据的逻辑还要简单,将数据暂存到 entriesToAddOnCommit 中,等待事务提交时再将数据存储到缓存中。
接下来是事务提交时调用的TransactionalCache#commit
方法,源码如下:
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
首先是TransactionalCache#commit
方法,第 2 行的条件语句根据 clearOnCommit 来决定是否在提交前清空缓存。
接着是第 5 行调用的TransactionalCache#flushPendingEntries
方法,源码如下:
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
分别遍历 entriesToAddOnCommit 中的数据和 entriesMissedInCache 中的数据,将它们提交到缓存中。
最后来看第 6 行调用的TransactionalCache#reset
方法,源码如下:
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
这里的清除逻辑非常简单,TransactionalCache#reset
方法用于重置 TransactionalCache。
剩下的就是异常回滚时调用的TransactionalCache#rollback
方法,源码如下:
public void rollback() {
unlockMissedEntries();
reset();
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
try {
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifying a rollback to the cache adapter. Consider upgrading your cache adapter to the latest version. Cause: " + e);
}
}
}
遍历 entriesMissedInCache 中的数据,并在委派的缓存中删除它们,以及调用TransactionalCache#reset
方法,重置 TransactionalCache。
TransactionalCacheManager
聊完了 TransactionalCache 后,我们再来看 TransactionalCacheManager,顾名思义,TransactionalCacheManager 是 TransactionalCache 的管理类。
由于 TransactionalCacheManager 的源码非常简单,我们这里一口气看完,源码如下:
public class TransactionalCacheManager {
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
private TransactionalCache getTransactionalCache(Cache cache) {
return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
}
}
我们重点来看第 29 行的TransactionalCacheManager#getTransactionalCache
方法的逻辑,它只调用了MapUtil#computeIfAbsent
方法,源码如下:
public static <K, V> V computeIfAbsent(Map<K, V> map, K key, Function<K, V> mappingFunction) {
V value = map.get(key);
if (value != null) {
return value;
}
return map.computeIfAbsent(key, mappingFunction);
}
结合两者,TransactionalCacheManager#getTransactionalCache
方法的逻辑就非常简单了,从 TransactionalCacheManager 中的 transactionalCaches 中使用传入的 Cache 实例作为 Key 查找 TransactionalCache 实例。
需要我们注意的是,在构建 TransactionalCache 时,会将传入的 Cache 实例作为 TransactionalCache 实例的委派对象。
那我们再回过头来看 TransactionalCacheManager 中其它的方法,所有的方法都是基于通过传入的 Cache 实例,查找 transactionalCaches 中的 TransactionalCache 实例后进行的操作,无论是存储数据,获取数据还是提交数据,都需要依赖TransactionalCacheManager#getTransactionalCache
方法获取 TransactionalCache 实例或者直接遍历 TransactionalCacheManager 中的 transactionalCaches 字段。
那么我们不禁要问,传入的 Cache 实例是什么呢?别急,我们先往下看。
数据存取逻辑分析
上面我们做了那么多铺垫,现在我们来看 MyBatis 二级缓存的存储逻辑。
由于在使用 MyBatis 二级缓存时,必须保证 mybatis-config.xml 中 cacheEnabled 的配置为 true,因此这里的 Executor 一定是 CachingExecutor 实例,所以我们直接从CachingExecutor#query
方法入手,部分源码如下:
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) {
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);
}
第 2 行中调用了MappedStatement#getCache
方法,从 MappedStatement 实例中获取二级缓存,这与我们前面在分析 MyBatis 创建二级缓存时的结论一致。
第 4 行调用了CachingExecutor#flushCacheIfRequired
方法,这与我们在《MyBatis中一级缓存的配置与实现原理》中讲解 flushCache 配置时提到的清除一级缓存的作用相似,只不过这里是根据 flushCache 的配置清除二级缓存的。
第 6 行中是通过 tcm 来获取缓存中的数据,前面我们已经聊过了 TransactionalCacheManager,这里我们看传入的 Cache 实例,就是我们最开始创建的 MyBatis 二级缓存的实例。这样一来整体的逻辑就很清晰了,首次通过 TransactionalCacheManager 的实例 tcm 获取缓存时,tcm 中并没有存储任何数据,会调用TransactionalCacheManager#getTransactionalCache
方法,创建 TransactionalCache 实例,并将通过 MappedStatement 实例获取的 Cache 实例作为 TransactionalCache 实例的被委派对象,并将 TransactionalCache 实例作为 Value,Cache 实例作为 Key 存储到 TransactionalCacheManager 的 transactionalCaches 字段中。
这是 TransactionalCacheManager 的结构如下:
最后是第 9 行调用的TransactionalCacheManager#putObject
方法,这个方法用于将查询出来数据进行缓存,这里我们就不多做解释了。
数据提交逻辑分析
最后我们来看数据从 TransactionalCache 暂存区提交到缓存中的逻辑,这依赖于我们主动调用SqlSession#commit
方法或者SqlSession#close
方法,我们先来看SqlSession#commit
方法的调用逻辑。
通过SqlSession#commit
方法提交暂存数据
在我们主动调用SqlSession#commit
方法时,实际上调用的是DefaultSqlSession#commit
方法,部分源码如下:
public void commit() {
commit(false);
}
public void commit(boolean force) {
executor.commit(isCommitOrRollbackRequired(force));
dirty = false;
}
可以看到这里实际上调用的是Executor#commit
方法,因为我们使用了二级缓存,所以这里的 Executor 实现类是 CachingExecutor,CachingExecutor#commit
方法的源码如下:
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
很明显CachingExecutor#commit
方法中调用了TransactionalCacheManager#commit
方法提交数据,结合前面的内容,我们很容易就能知道,TransactionalCacheManager#commit
会将 TransactionalCache 中暂存的数据提交到 MyBatis 的二级缓存中。
通过SqlSession#close
方法提交暂存数据
下面我们来看DefaultSqlSession#close
方法,部分源码如下:
public void close() {
executor.close(isCommitOrRollbackRequired(false));
closeCursors();
dirty = false;
}
同样是调用Executor#close
方法,有了前面的经验,我们直接来看CachingExecutor#close
方法,部分源码如下:
public void close(boolean forceRollback) {
try {
if (forceRollback) {
tcm.rollback();
} else {
tcm.commit();
}
} finally {
delegate.close(forceRollback);
}
}
这里的逻辑就很简单了,在没有发生异常的场景下在执行CachingExecutor#close
方法时,会调用TransactionalCacheManager#commit
方法将暂存在 TransactionalCache 中的数据提交到 MyBatis 的二级缓存中。
二级缓存与一级缓存的冲突
我们来做一个测试,前提条件是 PayOrderMapper 中使用二级缓存,而 UserOrderMapper 中不使用二级缓存,接着我们来写单元测试,代码如下:
public void testSecondLevelCacheConflict() {
Reader mysqlReader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mysqlReader);
SqlSession sqlSession = sqlSessionFactory.openSession();
System.out.println("第一次查询,使用通过【sqlSession】获取到的【userOrderMapper】查询【userOrder】");
UserOrderMapper userOrderMapper = sqlSession.getMapper(UserOrderMapper.class);
UserOrderDO userOrder = userOrderMapper.selectByOrderId(1);
System.out.println(JSON.toJSONString(userOrder));
System.out.println("第二次查询,使用通过【sqlSession】获取到的【payOrderMapper】查询【payOrder】");
PayOrderMapper payOrderMapper = sqlSession.getMapper(PayOrderMapper.class);
PayOrderDO payOrder = payOrderMapper.selectPayOrderByOrderId(1);
System.out.println(JSON.toJSONString(payOrder));
sqlSession.commit();
System.out.println("第三次查询,使用通过【sqlSession】获取到的【userOrderMapper】查询【newUserOrder】");
UserOrderDO newUserOrder = userOrderMapper.selectByOrderId(1);
System.out.println(JSON.toJSONString(newUserOrder));
}
我先来解释下这段代:
- 第 2 行到第 4 行,解析 mybatis-config.xml 配置,并获取 SqlSession 实例;
- 第 6 行到第 9 行,获取 UserOrderMapper 实例,并调用
UserOrderMapper#selectByOrderId
方法,此时我们预期,数据应该被存储到 MyBatis 的一级缓存中; - 第 11 行到第 15 行,获取 payOrderMapper 实例,并调用
PayOrderMapper#selectPayOrderByOrderId
方法,最后提交数据,此时我们的预期是,数据应该被存储到 MyBatis 的二级缓存中; - 第 17 行到第 19 行,使用前面获取的 UserOrderMapper 实例,并使用完全相同的参数再次调用
UserOrderMapper#selectByOrderId
方法,此时我们的预期是,数据应该是从 MyBatis 的一级缓存中取出,而不会再次查询数据库。
接下来我们就执行单元测试,控制台输出如下:
可以看到,3 次查询每次都执行了查询数据库的操作,与我们预期的第 3 次查询使用 MyBatis 一级缓存的情况不符。
我们来分析下这种情况,其实很简单,我们回到CachingExecutor#commit
方法中:
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
这里的操作除了调用TransactionalCacheManage#commit
方法提交二级缓存还,第 2 行还调用了被委派对象的 commit 方法。
又见委派模式,根据之前文章中聊过的 Executor 的创建逻辑,我们可以得知,这里被委派的是 SimpleExecutor 的实例,但是由于 SimpleExecutor 并没有实现 commit 方法,因此这里会执行BaseExecutor#commit
方法,部分源码如下:
public void commit(boolean required) throws SQLException {
clearLocalCache();
flushStatements();
}
是不是真相大白了?BaseExecutor#commit
方法中调用了BaseExecutor#clearLocalCache
方法来清除 MyBatis 的一级缓存,这个方法我在《MyBatis中一级缓存的配置与实现原理》里聊过,不熟悉的可以翻翻之前的文章。
我们来总结下,因为在使用二级缓存时,提交数据后会清除当前 SqlSession 中的一级缓存,因此造成了同一个 SqlSession 下,当使用二级缓存的数据提交后,SqlSession 中的一级缓存不可用,但是在不同的 SqlSession 中,不会存在这种二级缓存与一级缓存的冲突。