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 {

  @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 个特点:

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


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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用AOP思想实现开闭原则下的流水日志输出

主要实现思想&#xff1a; 通过实现Convert接口来抽取公共组件&#xff0c;获取想要的标准模型。 现在有两个订单场景&#xff0c;一个保存订单&#xff0c;一个为更新订单。构造如下的服务类&#xff1a; import org.springframework.stereotype.Service;Service public clas…

pwm 呼吸灯(如果灯一直亮或者一直灭)

&#xff08;这个文章收藏在我的csdn keil文件夹下面&#xff09; 如果这样设置预分频和计数周期&#xff0c;那么算出来的pwm频率如下 人眼看起来就只能是一直亮或者灭&#xff0c;因为pwm的频率太高了&#xff0c;但是必须是频率够高&#xff0c;才能实现呼吸灯的缓慢亮缓慢…

Django之项目开发(一)

一、项目的生命周期介绍 传统Web 项目的生命周期指的是从开始构建一个网站到该网站完成并维护的整个过程。通常情况下,Web 项目的生命周期包括以下几个阶段 需求分析阶段:在这个阶段,项目组会与客户进行沟通,确定网站的功能、内容和设计。 主要由产品经理参与产出思路与方案…

ChatGPT-4o大语言模型优化、本地私有化部署、从0-1搭建、智能体构建等高级进阶

目录 第一章 ChatGPT-4o使用进阶 第二章 大语言模型原理详解 第三章 大语言模型优化 第四章 开源大语言模型及本地部署 第五章 从0到1搭建第一个大语言模型 第六章 智能体&#xff08;Agent&#xff09;构建 第七章 大语言模型发展趋势 第八章 总结与答疑讨论 更多应用…

Nginx auth 的权限验证

基本流程 整个流程为&#xff1b;以用户视角访问API开始&#xff0c;进入 Nginx 的 auth 认证模块&#xff0c;调用 SpringBoot 提供的认证服务。根据认证结果调用重定向到对应的 API 接口或者 404 页面。 查看版本保证有 Nginx auth 模块 由于 OpenAI 或者本身自己训练的一套…

数据结构(其一)--基础知识篇

1. 数据结构三要素 1.1 数据结构的运算 即&#xff0c;增删改查 1.2 数据结构的存储结构 2. 数据类型&#xff0c;抽象数据类型 数据类型&#xff1a; &#xff08;1&#xff09;. 原子类型&#xff1a;bool、int... &#xff08;2&#xff09;. 结构类型&#xff1a;类、…

Linux多线程(中)

Linux多线程&#xff08;中&#xff09; 1.Linux线程互斥1.1互斥量的接口1.1.1初始化互斥量1.1.2销毁互斥量1.1.3互斥量加锁和解锁 1.2修改代码1.3互斥量实现原理 2.可重入VS线程安全3.死锁4.Linux线程同步5.生产者消费者模型 &#x1f31f;&#x1f31f;hello&#xff0c;各位…

Java 自定义集合常量

文章目录 Java 自定义集合常量一、普通方法自定义集合常量信息1、定义 Map 集合信息&#xff08;1&#xff09;方法一&#xff1a;使用静态代码块&#xff08;2&#xff09;方法二&#xff1a;简单定义 Map 常量 2、定义 List 集合信息3、定义 Set 集合信息 二、通过 Collectio…

用win的控制台去远程连接虚拟机linux的终端

以Ubuntu为例&#xff0c;首先确保Ubuntu已经安装了ssh服务 sudo apt-get install openssh-server输入密码 安装完毕后查看ssh状态是否开启 sudo systemctl status ssh 显示绿色激活状态&#xff0c;可以关闭或开启 对应start和stop winr打开win端控制台 输入 ssh -p 22 …

python-22-零基础自学python-数据分析基础 打开文件 读取文件信息

学习内容&#xff1a;《python编程&#xff1a;从入门到实践》第二版 知识点&#xff1a; 读取文件 、逐行读取文件信息等 练习内容&#xff1a; 练习10-1:Python学习笔记 在文本编辑器中新建一个文件&#xff0c;写几句话来总结一下你至此学到的Python知识&#xff0c;其中…

ASCII码对照表(Matplotlib颜色对照表)

文章目录 1、简介1.1 颜色代码 2、Matplotlib库简介2.1 简介2.2 安装2.3 后端2.4 入门例子 3、Matplotlib库颜色3.1 概述3.2 颜色图的分类3.3 颜色格式表示3.4 内置颜色映射3.5 xkcd 颜色映射3.6 颜色命名表 4、Colorcet库5、颜色对照表结语 1、简介 1.1 颜色代码 颜色代码是…

声明队列和交换机 + 消息转换器

目录 1、声明队列和交换机 方法一&#xff1a;基于Bean的方式声明 方法二&#xff1a;基于Spring注解的方式声明 2、消息转换器 1、声明队列和交换机 方法一&#xff1a;基于Bean的方式声明 注&#xff1a;队列和交换机的声明是放在消费者这边的&#xff0c;这位发送的人他…

OSS存储桶漏洞总结

简介 OSS&#xff0c;对象存储服务&#xff0c;对象存储可以简单理解为用来存储图片、音频、视频等非结构化数据的数据池。相对于主机服务器&#xff0c;具有读写速度快&#xff0c;利于分享的特点。 OSS工作原理&#xff1a; 数据以对象&#xff08;Object&#xff09;的形式…

Java高级重点知识点-21-IO、字节流、字符流、IO异常处理、Properties中的load()方法

文章目录 IOIO的分类 字节流字节输出流【OutputStream】字节输入流【InputStream】图片复制 字符流字符输入流【FileReader】字符输出流【FileWriter】 IO异常的处理&#xff08;扩展知识&#xff09;Properties属性集(java.util) IO Java中I/O操作主要是指使用 java.io 包下的…

iOS中多个tableView 嵌套滚动特性探索

嵌套滚动的机制 目前的结构是这样的&#xff0c;整个页面是一个大的tableView, Cell 是整个页面的大小&#xff0c;cell 中嵌套了一个tableView 通过测试我们发现滚动的时候&#xff0c;系统的机制是这样的&#xff0c; 我们滑动内部小的tableView, 开始滑动的时候&#xff0c…

想知道你的电脑能不能和如何升级RAM吗?这里有你想要的一些提示

考虑给你的电脑增加更多的RAM,但不确定从哪里开始?本指南涵盖了有关升级Windows PC或笔记本电脑中RAM的所有信息。 你需要升级RAM吗 在深入研究升级RAM的过程之前,评估是否需要升级是至关重要的。你是否经历过系统滞后、频繁的BSOD错误或应用程序和程序突然崩溃?这些症状…

Lock与ReentrantLock

在 Java 中&#xff0c;Lock 接口和 ReentrantLock 类提供了比使用 synchronized 方法和代码块更广泛的锁定机制。 简单示例&#xff1a; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample {pr…

聊一下Maven打包的问题(jar要发布)

文章目录 一、问题和现象二、解决方法&#xff08;1&#xff09;方法一、maven-jar-pluginmaven-dependency-plugin&#xff08;2&#xff09;方法二、maven-assembly-plugin 一、问题和现象 现在的开发一直都是用spring boot&#xff0c;突然有一天&#xff0c;要自己开发一个…

【CUDA】

笔者在学习Softmax实现时遇到了一个问题&#xff0c;很多文章直接将softmax的计算分成了五个过程&#xff0c;而没有解释每个过程的含义&#xff0c;尤其是在阅读这篇文章时&#xff0c;作者想计算最基本的softmax的效率&#xff0c;以展示可行的优化空间&#xff1a; 贴一个g…

MybatisX插件的简单使用教程

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