首页 > 其他分享 >MyBatis中二级缓存的配置与实现原理

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

时间:2024-07-07 20:29:39浏览次数:9  
标签:缓存 Cache 二级缓存 实例 MyBatis 原理 public

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


标签:缓存,Cache,二级缓存,实例,MyBatis,原理,public
From: https://blog.csdn.net/wyz_1945/article/details/140251658

相关文章

  • 昇思25天学习打卡营第11天 | LLM原理和实践:基于MindSpore实现BERT对话情绪识别
    1.基于MindSpore实现BERT对话情绪识别1.1环境配置#实验环境已经预装了mindspore==2.2.14,如需更换mindspore版本,可更改下面mindspore的版本号!pipuninstallmindspore-y!pipinstall-ihttps://pypi.mirrors.ustc.edu.cn/simplemindspore==2.2.14#该案例在min......
  • 硬件开发笔记(二十三):贴片电阻的类别、封装介绍,AD21导入贴片电阻原理图封装库3D模型
    前言  电阻,电容,电感还有各种基础的电子元器件、连接器和IC构成了各种实现功能的电子电路。  本篇介绍贴片电阻,并将贴片电阻封装导入AD21,预览其三维模型。 贴片电阻    贴片电阻(SMDResistor)作为一种不可或缺的电子元件,广泛应用于各种电路和设备中。其体积......
  • MybatisPlus实现插入/修改数据自动设置时间
    引言插入数据时自动设置当前时间,更新数据时自动修改日期为修改时的日期。使用MybatisPlus的扩展接口MetaObjectHandler步骤实现接口实体类加注解实现接口packagecom.example.vueelementson.common;importcom.baomidou.mybatisplus.core.handlers.MetaObjectHa......
  • 【Mybatis】(接口式)
    前期准备:一张表(!已经连接数据库!我的数据库叫book,表名为user,有id、name、age三列)项目结构:(log4j可以没有)1,创建一个空的Maven项目然后在pom.xml中导入依赖(mybatis需要3个依赖,junit测试依赖包、mybatis依赖包、SQL连接依赖包)<dependencies><dependency><groupI......
  • mybatis-plus中last和orderBy的连用的使用顺序
    1. mybatis-plus中last和orderBy的连用的使用顺序在MyBatis-Plus中,last方法用于在构建查询时添加自定义的SQL片段,而orderBy方法用于指定排序规则。当你想要结合使用这两个方法时,可以先调用orderBy指定排序规则,然后调用last添加自定义的SQL片段。@Autowired......
  • 灰色预测GM(1,1)模型的理论原理
    灰色预测是对时间有关的灰色过程进行预测。通过建立相应的微分方程模型,从而预测事物未来发展趋势的状况。由于笔者的水平不足,本章只是概括性地介绍GM(1,1)模型的理论原理,便于对初学者的初步理解目录一、灰色系统二、GM(1,1)灰色预测模型1.生成累加数据与紧临均值生成序列2.建立预测......
  • Optimize-Volume 命令用于优化指定驱动器的性能。除了 -Defrag 参数以外,还有一些其他
    Optimize-Volume命令起源于Microsoft的PowerShell环境中的一个磁盘优化工具。它主要用于对磁盘驱动器执行优化操作,包括碎片整理、TRIM操作(针对固态硬盘)、分块整理等。这些操作有助于提高磁盘性能和延长硬件寿命,特别是对于使用频繁的系统和数据驱动器来说尤为重要。在Power......
  • MyBatisPlus
    目录引入MybatisPlus的起步依赖1、引入MybatisPlus依赖,代替Mybatis依赖2.定义Mapper常见注解@TableName:用来指定表名@Tableld:用来指定表中的主键字段信息@TableField:用来指定表中的普通字段信息 使用@TableField的常见场景:成员变量名与数据库字段名不一致​编......
  • JWT原理
    JWTJWT(jsonwebtoken)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算......
  • Java信号量semaphore的原理与使用方法
    Semaphore的基本概念在Java中,Semaphore是位于java.util.concurrent包下的一个类。它的核心就是维护了一个许可集。简单来说,就是有一定数量的许可,线程需要先获取到许可,才能执行,执行完毕后再释放许可。那么,这个许可是什么呢?其实,你可以把它想象成是对资源的访问权。比如,有5个......