Mybatis的CachingExecutor与二级缓存

简介: Mybatis的CachingExecutor与二级缓存

前言

上次我们讲Mybatis的缓存时,我们提到了CachingExecutor,知道了这个带缓存的执行器就是二级缓存的来源,这次我们系统的分析下其是如何产生作用的


一、CachingExecutor的在逻辑定位

1. 流程图中的位置

我把CachingExecutor在逻辑链路中的位置标出来了,就是储存在会话对象中,通过会话可使用到CachingExecutor,而CachingExecutor又内置一个SimpleExecutor,熟悉设计模式的同学应该知道这就是所谓的委派模式。当然,这里面会话内置的也可能直接就是SimpleExecutor了,那样的话,调用的就直接是SimpleExecutor执行器了。

abf9fc1ef3234988aa7b03152ef2d0f2.png


二、CachingExecutor的生效

既然知道了CachingExecutor在会话对象中,那毫无疑问,就是在创建会话的时候,把一个CachingExecutor放入到会话对象中的,我们来看看,要实现这个目标要做什么


1.全局参数

首先,要想启用CachingExecutor,我们得开启一个全局的设置参数

mybatis.configuration.cache-enabled=true

为什么开了这个参数就有用呢?其实不难猜想,其作用的位置肯定还是在创建会话对象的时候,我们直接看源码吧


SqlSessionUtils.class


// SqlSessionUtils.class
// 通过工厂对象(单例,存在容器中),开启会话,获得会话对象,executorType是执行器枚举类(SIMPLE, REUSE, BATCH),一般是SIMPLE
session = sessionFactory.openSession(executorType);

DefaultSqlSessionFactory.class

// DefaultSqlSessionFactory.class
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    final Environment environment = configuration.getEnvironment();
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    // 通过configuration(Mybatis配置,单例)创建执行器
    final Executor executor = configuration.newExecutor(tx, execType);
    return new DefaultSqlSession(configuration, executor, autoCommit);
  } catch (Exception e) {
    closeTransaction(tx); // may have fetched a connection so lets call close()
    throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

Configuration.class

// Configuration.class
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  Executor executor;
  if (ExecutorType.BATCH == executorType) {
    executor = new BatchExecutor(this, transaction);
  } else if (ExecutorType.REUSE == executorType) {
    executor = new ReuseExecutor(this, transaction);
  } else {
    // 创建了一个简易执行器
    executor = new SimpleExecutor(this, transaction);
  }
  // 如果开启了缓存,即 mybatis.configuration.cache-enabled=true
  if (cacheEnabled) {
    // 创建了缓存执行器,并且把简易执行器作为其 delegate,通过构造方法放入
    executor = new CachingExecutor(executor);
  }
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

如上,可以看出CachingExecutor就是由 mybatis.configuration.cache-enabled=true 开启的,并且和会话的情况无关,这是一个全局设置,一旦开启,所有会话都会首先调用CachingExecutor


2. MappedStatement启用Cache

是不是我们启用了CachingExecutor就可以使用二级缓存了呢?为什么这么说,我们还是直接看CachingExecutor的源码

CachingExecutor.class

// CachingExecutor.class
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  // mappedStatement 就是我们写的mapper接口里的某个方法的所有信息,注意其描述的是一个方法,而不是一整个mapper接口的所有方法
  // 当然,它同样也包含某些mapper层次的设置,如对应的xml文件位置等,此处的Cache同样是mapper层次的设置
  Cache cache = ms.getCache();
  // 有Cache才会真正的去找二级缓存,否则直接就让委托的执行器去查询数据了。
  // 注意此处的Cache并不是缓存本身,而是mapper里的缓存配置
  if (cache != null) {
    // ....
      List<E> list = (List<E>) tcm.getObject(cache, key);
    // ....
      return list;
    }
  }
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

可以看到,要想真正启用缓存,还得在Mapper层级进行一次缓存配置,也就是所谓的

声明下Cache时可以设置一下参数
<cache eviction="FIFO" flushinterval="60000" size="512" readOnly="true"/>
当然也可以不进行任何参数配置,就单独声明下Cache,如下:
<cache/>

我们看一下此时MappedStatement里Cache的构成,可以说是套娃巅峰,把委派模式玩到了极致

7bdb618e3c024d728d0c6c1befad8796.png

我们通过源码看其实现

MapperBuilderAssistant.class

// MapperBuilderAssistant.class
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;
}

三、二级缓存的存取

上面我们已经看了如何使二级缓存生效,但真正的查询和存入还没有细看,现在来看看其存储的位置,及如何读取,我们直接看下源码


1. 缓存源码分析

// CachingExecutor.class query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
if (cache != null) {
  // 是否清缓存,即如果Cache配置里含有flushCache=“true” 则进行 tcm.clear();
  flushCacheIfRequired(ms);
  if (ms.isUseCache() && resultHandler == null) {
    ensureNoOutParams(ms, boundSql);
    @SuppressWarnings("unchecked")
    // tcm 是一个成员变量,是通过new TransactionalCacheManager()赋值的,此处为查询
    List<E> list = (List<E>) tcm.getObject(cache, key);
    if (list == null) {
      list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
      // 通过 tcm 进行结果的存储
      tcm.putObject(cache, key, list); // issue #578 and #116
    }
    return list;
  }
}

在进行下一步前,我们有必要看一下获取缓存,为什么要传两个入参,即cache和key?


3bfcd48405f6464cb7f5561f03a5375a.png

cache:mapper级别的缓存配置及二级缓存

key:方法的全限定名及完整的sql即sql入参

接下来,我们不难发现已经出现了 存储、获取、清除这三种方法,且都围绕着

public class TransactionalCacheManager {
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
  public Object getObject(Cache cache, CacheKey key) {
      // 先把缓存设置作为key,查询出一个TransactionalCache
      return getTransactionalCache(cache).getObject(key);
    }
    private TransactionalCache getTransactionalCache(Cache cache) {
        // 作为第一次查询,不难发现,所谓的value居然也是依托缓存设置来构造的
      return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
    }
    // ....
}
public class TransactionalCache implements Cache {
    private final Cache delegate;
    private boolean clearOnCommit;
    // 临时待加缓存,查询数据库返回的结果,首先会放在这里,等本次事务提交后,才会加入到真正的二级缓存中,即调用套娃对象,层层深入,最终存入HashMap
    // 在事务提交前,其他会话甚至本会话自己都无法看见该缓存,更无法使用该缓存
    private final Map<Object, Object> entriesToAddOnCommit;
    // 查二级缓存没查到时,会把key值存在这个miss的集合中
    private final Set<Object> entriesMissedInCache;
    public TransactionalCache(Cache delegate) {
      this.delegate = delegate;
      this.clearOnCommit = false;
      this.entriesToAddOnCommit = new HashMap<>();
      this.entriesMissedInCache = new HashSet<>();
    }
  public Object getObject(Object key) {
    // issue #116
    // 观察后,可以知道,这里的delegate其实就是我们说的mapper缓存设置
    // 由此可见,缓存设置中也能包含缓存的值,并且以完整sql为键,sql结果为值以map存储
    Object object = delegate.getObject(key);
    if (object == null) {
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }
}

由于Cache的套娃十分严重,实际形成了链状引用,而且受配置的影响很大,所以没有办法把每一种配置缓存间的相互调用阐述详尽。但是我们仍然可以讲出其核心思想。忽略掉中间的套娃,最终实现存储的缓存类为PerpetualCache.class,其包含了一个HashMap,键就是我们上面提及的key(混合了方法信息,完整sql等),值为sql返回值的字节数组

c6918fb757e241b18a1038a889e910f9.png


2. 二级缓存可见性

二级缓存并不是即时生效的,我们可以关注下TransactionalCache 类,这个类看名字也知道是事务有关,它有一个成员变量


private final Map<Object, Object> entriesToAddOnCommit;


这个变量我们上面源码分析里其实说了,就算查数据库,返回了结果,也不是立即就到我们说的终极位置——PerpetualCache的HashMap里。而是在这个变量里暂存,等待事务提交了,再把这里的数据存入真正的二级缓存处


而在此之前,即使是本会话,也没法从二级缓存中捞到东西,也就是说在一个事务里,你连续执行两次同样的sql,尽管二级缓存已经暂存了数据,但第二次sql经过CachingExecutor时,它并不会把这个数据给你,那么自然,其他的会话也是无法看见这个数据的。


因此,我们说二级缓存的数据,只在本事务提交后才正式可见,在此之前,其他会话甚至本会话自己,都无法使用该二级缓存


3. 一二级缓存优先级

看上图,不难明白,如果开启了二级缓存,则先查的是二级缓存(mapper级别),这和我们一般的认知相悖,因为大多数缓存层级,都是优先查一级缓存,未命中再去查的二级缓存。除了上面因为可见性问题导致的,先查一级缓存,Mybatis里在一二级都开启的情况下,优先使用的是二级缓存的数据,因此这里需要特别注意。

目录
相关文章
|
6月前
|
缓存 Java 数据库连接
Mybatis缓存相关面试题有多卷
使用 MyBatis 缓存机制需要注意以下几点: 对于频繁更新和变动的数据,不适合使用缓存。 对于数据的一致性要求比较高的场景,不适合使用缓存。 如果配置了二级缓存,需要确保缓存的数据不会影响到其他业务模块的数据。 在使用缓存时,需要注意缓存的命中率和缓存的过期策略,避免缓存过期导致查询性能下降。
105 0
|
2月前
|
缓存 Java 数据库连接
mybatis复习05,mybatis的缓存机制(一级缓存和二级缓存及第三方缓存)
文章介绍了MyBatis的缓存机制,包括一级缓存和二级缓存的配置和使用,以及如何整合第三方缓存EHCache。详细解释了一级缓存的生命周期、二级缓存的开启条件和配置属性,以及如何通过ehcache.xml配置文件和logback.xml日志配置文件来实现EHCache的整合。
mybatis复习05,mybatis的缓存机制(一级缓存和二级缓存及第三方缓存)
|
5天前
|
SQL 缓存 Java
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
本文详细介绍了MyBatis的各种常见用法MyBatis多级缓存、逆向工程、分页插件 包括获取参数值和结果的各种情况、自定义映射resultMap、动态SQL
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
|
5天前
|
SQL 缓存 Java
MyBatis如何关闭一级缓存(分注解和xml两种方式)
MyBatis如何关闭一级缓存(分注解和xml两种方式)
24 5
|
21天前
|
缓存 Java 数据库连接
使用MyBatis缓存的简单案例
MyBatis 是一种流行的持久层框架,支持自定义 SQL 执行、映射及复杂查询。本文介绍了如何在 Spring Boot 项目中集成 MyBatis 并实现一级和二级缓存,以提高查询性能,减少数据库访问。通过具体的电商系统案例,详细讲解了项目搭建、缓存配置、实体类创建、Mapper 编写、Service 层实现及缓存测试等步骤。
|
5月前
|
SQL 缓存 Java
MYBATIS缓存
MYBATIS缓存
|
4月前
|
SQL 缓存 Java
【面试官】Mybatis缓存有什么问题吗?
面试官:你说下对MyBatis的理解?面试官:那SqlSession知道吧?面试官:Mybatis的缓存有哪几种?面试官:那Mybatis缓存有什么问题吗?面试官:Mybatis分页插件是怎么
【面试官】Mybatis缓存有什么问题吗?
|
4月前
|
缓存 算法 Java
关于MyBatis的缓存详解
MyBatis 的缓存机制非常灵活,可以通过简单的配置来满足不同的性能需求。合理地使用缓存可以显著提高应用程序的性能,尤其是在处理大量数据库查询时。然而,开发者需要注意缓存的一致性和并发问题,特别是在使用可读写缓存时。
|
5月前
|
缓存 NoSQL Java
在 SSM 架构(Spring + SpringMVC + MyBatis)中,可以通过 Spring 的注解式缓存来实现 Redis 缓存功能
【6月更文挑战第18天】在SSM(Spring+SpringMVC+MyBatis)中集成Redis缓存,涉及以下步骤:添加Spring Boot的`spring-boot-starter-data-redis`依赖;配置Redis连接池(如JedisPoolConfig)和连接工厂;在Service层使用`@Cacheable`注解标记缓存方法,指定缓存名和键生成策略;最后,在主配置类启用缓存注解。通过这些步骤,可以利用Spring的注解实现Redis缓存。
75 2
|
5月前
|
SQL 缓存 Java
Java框架之MyBatis 07-动态SQL-缓存机制-逆向工程-分页插件
Java框架之MyBatis 07-动态SQL-缓存机制-逆向工程-分页插件