MyBatis中二级缓存的配置与实现原理

大家好,我是王有志,一个分享硬核 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 个条件:

  1. mybatis-config.xml 中的 cacheEnabled 配置处于默认状态(默认状态即开启状态)或开启状态;
  2. 映射器中需要添加配置;
  3. 映射器中返回的 Java 对象必须是可序列化的,即需要实现 Serializable 接口;
  4. 使用时,只有提交(执行SqlSession#commit方法)或关闭(执行SqlSession#close方法)时数据才会被提交到缓存中。

这里我们使用支付订单的的接口与 Java 对象来举个例子,首先是为 PayOrderDO 实现 Serializable 接口,代码如下:

public class PayOrderDO implements Serializable {@Serialprivate 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 个特点:

  1. 所有 select 查询语句的结果都会被缓存;
  2. 执行 insert 语句,update 语句和 delete 语句后会刷新(清空)缓存;
  3. 缓存默认使用 LRU 淘汰策略来实现缓存淘汰;
  4. 缓存不会进行定时刷新;
  5. 缓存的默认大小是 1024 个;
  6. 缓存被设置为读/写缓存,这表示通过缓存获取到的对象并不是共享的,并且允许修改缓存。

其中第 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 中,不会存在这种二级缓存与一级缓存的冲突


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/42575.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue3实现echarts——小demo

版本&#xff1a; 效果&#xff1a; 代码&#xff1a; <template><div class"middle-box"><div class"box-title">检验排名TOP10</div><div class"box-echart" id"chart1" :loading"loading1"&…

Linux操作系统中逻辑卷的缩减

流程&#xff1a;第一步先是要缩减逻辑卷的文件系统。 第二步就是要去缩减逻辑卷的物理边界。 注意事项&#xff1a; 1.逻辑卷要处于卸载状态&#xff0c; 2.建议先备份数据 3.在缩减逻辑卷的时候&#xff0c;要注意xfs文件系统的逻辑卷是不支持直接进行缩减的。 4.在缩减…

Linux:DHCP服务配置

目录 一、DHCP概述以及DHCP的好处 1.1、概述 1.2、DHCP的好处 二、DHCP的模式与分配方式 2.1、模式 2.2、DHCP的分配方式 三、DHCP工作原理 四、安装DHCP服务 五、DHCP局部配置并且测试 5.1、subnet 网段声明 5.2、客户机预留指定的固定ip地址 一、DHCP概述以及DHCP…

下载nvm 管理多个node版本并切换

nvm管理多个node版本并切换 安装nvm时不能安装任何node版本&#xff08;先删除node再安装nvm&#xff09;&#xff0c;保证系统无任何node.js残留 1. 卸载node 控制面板中卸载nodejs 删除以下文件夹&#xff1a; C:\Program Files (x86)\Nodejs C:\Program Files\Nodejs C…

MybatisX插件的简单使用教程

搜索mybatis 开始生成 module path&#xff1a;当前项目 base package:生成的包名&#xff0c;建议先独立生成一个&#xff0c;和你原本的项目分开 encoding&#xff1a;编码&#xff0c;建议UTF-8 class name strategy&#xff1a;命名选择 推荐选择camel&#xff1a;驼峰命…

Go:hello world

开启转职->Go开发工程师 下面是我的第一个go的程序 在上面的程序介绍&#xff1a; 1、package main 第一行代码package main定义了包名。必须在源文件中非注释的第一行指明这个文件属于哪个包&#xff0c;如&#xff1a;package main。package main表示一个可独立执行的程…

spRAG框架学习小结

spRAG是什么 spRAG是一个针对非结构化数据的检索引擎。它特别擅长处理对密集文本的复杂查询&#xff0c;比如财务报告、法律文件和学术论文。有两种关键方法用于提高性能&#xff0c;超越了普通的RAG系统&#xff1a; 自动上下文&#xff08;AutoContext&#xff09;&#xff…

µCOS-III 任务同步机制-任务信号量

1. 什么是任务信号量 任务信号量是一种用于任务间同步和通信的计数器&#xff0c;通常用于解决任务间的竞争条件和资源共享问题。在C/OS-III中&#xff0c;任务信号量提供了二进制信号量和计数信号量两种类型&#xff1a; 二进制信号量&#xff1a;只能取值0或1&#xff0c;适…

Debezium报错处理系列之第109篇:解决升级日志解析jar包重启集群出现的字段类型和值不匹配的错误

Debezium报错处理系列之第109篇:解决升级日志解析jar包重启集群出现的字段类型和值不匹配的错误 一、完整报错二、错误原因三、解决方法Debezium从入门到精通系列之:研究Debezium技术遇到的各种错误解决方法汇总: Debezium从入门到精通系列之:百篇系列文章汇总之研究Debezi…

传统IO和NIO文件拷贝过程

参考&#xff1a;https://blog.csdn.net/weixin_57323780/article/details/130250582

昇思25天学习打卡营第9天|ResNet50图像分类

一、Resnet残差网络模型 构建残差网络结构;Building BlockBottleneck 残差结构由两个分支构成&#xff1a;一个主分支 &#x1d439;(&#x1d465;)&#xff0c;一个shortcuts&#xff08;图中弧线表示,&#x1d465;&#xff09;。 得到残差网络结构:&#x1d439;(&#x…

SpringMVC常见的注解

一、Spring MVC Spring Web MVC是基于ServletAPI构建的原始web 框架&#xff0c;一开始就包含在Spring 框架中&#xff0c;通常被称为“Spring MVC”。 1.MVC 是什么&#xff1f; MVC(Model、View、Controller&#xff09;是软件工程中的一种软件架构设计模型。它把软件系统分…

STM32-输入捕获IC和编码器接口

本内容基于江协科技STM32视频学习之后整理而得。 文章目录 1. 输入捕获IC1.1 输入捕获IC简介1.2 频率测量1.3 输入捕获通道1.4 主从触发模式1.5 输入捕获基本结构1.6 PWMI基本结构 2. 输入捕获库函数及代码2.1 输入捕获库函数2.2 6-6 输入捕获模式测频率2.2.1 硬件连接2.2.2 硬…

2024暑假集训

Day1——枚举 Day2——测试 Day3——贪心 Day4、5——测试 ——————————————————————————————————————————— Day3T7&Day5T7:没思路 Day3T8:不知道怎么排序筛选 Day5T5:没有算法难度&#xff0c;但是不知道怎么处理2队奶牛的情…

什么牌子的头戴式蓝牙耳机好性价比高?

说起性价比高的头戴式蓝牙耳机,就不得不提倍思H1s,作为倍思最新推出的新款,在各项功能上都实现了不错的升级,二字开头的价格,配置却毫不含糊, 倍思H1s的音质表现堪称一流。它采用了40mm天然生物纤维振膜,这种振膜柔韧而有弹性,能够显著提升低音的量感。无论是深沉的低音还是清…

数据跨境法案:美国篇上

近年来随着全球数字化的加速发展&#xff0c;数据已成为国家竞争力的重要基石。在这样的背景下&#xff0c;中国软件和技术出海的场景日益丰富。本系列邀请到在跨境数据方面的研究人员针对海外的数据跨境政策进行解读。 本期将针对美国对数据跨境流动的态度和政策进行阐释。过…

非比较排序 计数排序

1.核心思路 首先要找出max 和 min&#xff0c;最大值 - 最小值 1&#xff0c;就可以计算出数据在什么范围然后创建计数数组大小&#xff0c;a[i] - min 在数组的相对位置计数 通过自然序列排序然后把计数好的值&#xff0c;按照顺序依次放回原数组即可 动图解释&#xff0c;其…

Linux—网络设置

目录 一、ifconfig——查看网络配置 1、查看网络接口信息 1.1、查看所有网络接口 1.2、查看具体的网络接口 2、修改网络配置 3、添加网络接口 4、禁用/激活网卡 二、hostname——查看主机名称 1、查看主机名称 2、临时修改主机名称 3、永久修改主机名称 4、查看本…

Java引用的4种类型:强、软、弱、虚

在Java中&#xff0c;引用的概念不仅限于强引用&#xff0c;还包括软引用、弱引用和虚引用&#xff08;也称为幻影引用&#xff09;。这些引用类型主要用于不同的内存管理策略&#xff0c;尤其是在垃圾收集过程中。以下是对这四种引用类型的详细解释&#xff1a; 1. 强引用&am…

algorithm算法库学习之——不修改序列的操作

algorithm此头文件是算法库的一部分。本篇介绍不修改序列的操作函数。 不修改序列的操作 all_ofany_ofnone_of (C11)(C11)(C11) 检查谓词是否对范围中所有、任一或无元素为 true (函数模板) for_each 应用函数到范围中的元素 (函数模板) for_each_n (C17) 应用一个函数对象到序…