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

news2024/11/24 3:40:53

大家好,我是王有志,一个分享硬核 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 中,不会存在这种二级缓存与一级缓存的冲突


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

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

相关文章

14-40 剑和诗人14 - 为什么机器学习需要合成数据

​​​​​​ 数据是人工智能的命脉。如果没有高质量、有代表性的训练数据&#xff0c;我们的机器学习模型将毫无用处。但随着神经网络规模越来越大、人工智能项目越来越雄心勃勃&#xff0c;人们对数据的需求也越来越大&#xff0c;我们面临着一场危机——现实世界的数据收集和…

STM32-ADC+DMA

本内容基于江协科技STM32视频学习之后整理而得。 文章目录 1. ADC模拟-数字转换器1.1 ADC模拟-数字转换器1.2 逐次逼近型ADC1.3 ADC框图1.4 ADC基本结构1.5 输入通道1.6 规则组的转换模式1.6.1 单次转换&#xff0c;非扫描模式1.6.2 连续转换&#xff0c;非扫描模式1.6.3 单次…

C语言中32位浮点数的格式

以 GNU C为例&#xff0c;它遵循 IEEE 754-2008标准中制定的浮点表示规范。在该规范中定义了 5种不同大小的基础二进制浮点格式&#xff0c;包括&#xff1a;16位&#xff0c;32位&#xff0c;64位&#xff0c;128位&#xff0c;256位。其中&#xff0c;32位的格式被用作标准 C…

基于PSOGWO-CNN-BiLSTM数据回归预测(多输入单输出)-粒子群结合灰狼算法优化-CNN-BiLSTM

基于PSOGWO-CNN-BiLSTM数据回归预测(多输入单输出)-粒子群结合灰狼算法优化-CNN-BiLSTM 1.数据均为Excel数据&#xff0c;直接替换数据就可以运行程序。 2.所有程序都经过验证&#xff0c;保证程序可以运行。 3.具有良好的编程习惯&#xff0c;程序均包含简要注释。 获取方式…

恢复出厂设置后如何从 iPhone 恢复数据

在 iPhone 恢复出厂设置后&#xff0c;所有数据都会被删除&#xff0c;并且 iPhone 将恢复到原始出厂设置&#xff0c;这意味着您的所有 iPhone 数据&#xff0c;包括照片、视频、联系人和应用程序都将消失。 幸运的是&#xff0c;如果您有备份可以恢复&#xff0c;这并不一定…

JavaSe系列二十七: Java正则表达式

正则表达式 为什么要学习正则表达式再提几个问题解决之道-正则表达式正则表达式基本介绍介绍 正则表达式底层实现实例分析 正则表达式语法基本介绍元字符-转义号 \\\\元字符-字符匹配符元字符-选择匹配符元字符-限定符元字符-定位符分组非贪婪匹配 应用实例对字符串进行如下验证…

cloudflare tunnels tcp

这里是官网的说明Cloudflare Tunnel Cloudflare Zero Trust docs 根据实际情况安装环境 tunnels除了http,https协议是直接暴露公网&#xff0c;tcp是类似ssh端口转发。 在需要内网穿透的局域网找一条机子部署代理 我这边是window cloudflared tunnel login #生成一个身份校…

【论文阅读】-- Visual Analytics for Model Selection in Time Series Analysis

时间序列分析中模型选择的可视化分析 摘要1 引言2 相关工作3 问题表征3.1 Box-Jenkins 方法论3.2 ARIMA 和季节性 ARIMA 模型3.3 模型规范3.4 模型拟合3.5 模型诊断 4 需求分析5 VA 用于时间序列分析中的模型选择5.1 VA选型流程说明5.2 TiMoVA 原型5.2.1 实施选择5.2.2 图形用户…

(详细版)学生管理系统(姓名、成绩、学号)---顺序表

//1:创建顺序表 //2:判满 //3:判空 //4:插入学生信息 //5:输出学生信息 //6:按位置插入学生信息 //7:按位置删除学生信息 //8:按位置修改学生信息 //9:按学号查找学生信息 //10:顺序表去重 //11:销毁顺序表 main.c&#xff1a; int main(int argc, const char *argv[]) {seq_p…

第一周周日总结

题目总结 1.给你一个整数数组 hours&#xff0c;表示以 小时 为单位的时间&#xff0c;返回一个整数&#xff0c;表示满足 i < j 且 hours[i] hours[j] 构成 整天 的下标对 i, j 的数目。 整天 定义为时间持续时间是 24 小时的 整数倍 。 例如&#xff0c;1 天是 24 小时…

DHCP的原理及配置

目录 一、了解DHCP服务 1.什么是DHCP 1.1DHCP广播 2.使用DHCP的好处 2.1为什么使用DHCP 3.DHCP的模式与分配方式 3.1分配方式 3.2模式 二、DHCP工作原理 1.四次回话 2.重新登录 3.更新租约 4.扩展 三、安装DHCP服务 四、DHCP局部配置并且测试 五、使用…

PyQt5中如何实现指示灯点亮和指示灯熄灭功能

一般上位机界面都会涉及指示灯点亮和指示灯熄灭功能&#xff0c;从网上下载该功能的上位机界面&#xff0c;学习如何使用PyQt5搭建具备指示灯点亮和指示灯熄灭效果的界面。 1. 上位机界面的效果展示 使用PyQt5实现以下界面&#xff0c;界面效果如下&#xff0c;界面图片是从网…

【CUDA】

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

相见恨晚的《新程序员》 AI 专辑

声明&#xff1a;本文并不什么“软文”&#xff0c;是我的真实感受分享。本人和《新程序员》无任何利益关系&#xff0c;读者购买专栏我不会获得任何分成。 一、前言 前不久有位朋友送我一本 CSDN 出品的 《新程序员 006&#xff1a;人工智能新十年》 的杂志。 说实话&#x…

昇思25天学习打卡营第12天|FCN图像语义分割

文章目录 昇思MindSpore应用实践基于MindSpore的FCN图像语义分割1、FCN 图像分割简介2、构建 FCN 模型3、数据预处理4、模型训练自定义评价指标 Metrics 5、模型推理结果 Reference 昇思MindSpore应用实践 本系列文章主要用于记录昇思25天学习打卡营的学习心得。 基于MindSpo…

谷粒商城学习笔记-16-人人开源搭建后台管理系统

文章目录 一&#xff0c;克隆前/后端代码1&#xff0c;克隆前端工程renren-fast-value2&#xff0c;克隆后端工程renren-fast 二&#xff0c;集成后台管理系统的后端代码三&#xff0c;启动后台管理系统四&#xff0c;前端系统的安装和运行1&#xff0c;下载安装VSCode2&#x…

【TB作品】51单片机 Proteus仿真 超声波LCD1602ADC0832 身高体重测量仪

00024 超声波LCD1602ADC0832 实验报告&#xff1a;基于51单片机的身高体重测量仪设计 背景介绍 本实验设计并实现了一个基于51单片机的身高体重测量仪。该系统利用超声波传感器测量高度&#xff0c;通过ADC0832模数转换芯片获取重量数据&#xff0c;并使用LCD1602显示屏显示…

26_嵌入式系统网络接口

以太网接口基本原理 IEEE802标准 局域网标准协议工作在物理层和数据链路层&#xff0c;其将数据链路层又划分为两层&#xff0c;从下到上分别为介质访问控制子层(不同的MAC子层&#xff0c;与具体接入的传输介质相关),逻辑链路控制子层(统一的LLC子层&#xff0c;为上层提供统…

【JavaSE复习】数据结构、集合

JavaSE 复习 1.数据结构1.1 查找1.1.1 基本查找1.1.2 二分查找1.1.3 插值查找1.1.4 斐波那契查找1.1.5 分块查找1.1.6 分块查找的扩展&#xff08;无规律数据&#xff09; 1.2 排序1.2.1 冒泡排序1.2.2 选择排序1.2.3 插入排序1.2.4 快速排序 2. 集合2.1 基础集合2.1.1 集合和数…

MATLAB制作一个简单的函数绘制APP

制作一个函数绘制APP&#xff0c;输入函数以及左右端点&#xff0c;绘制出函数图像。 编写回调函数&#xff1a; 结果&#xff1a;