橘子学Mybatis09之Mybatis关于二级缓存的使用

本文详细介绍了MyBatis中的一级和二级缓存开启方式、源码设计以及查询时机。重点讲解了CachingExecutor作为装饰器如何增强查询性能,以及缓存的创建、存储和使用过程。
摘要由CSDN通过智能技术生成

前面我们说了一级缓存,但是实际上我们说那玩意其实不咋实用。于是既然设计了缓存体系,就不可能弄个不实用的给人们。所以这里就引出二级全局缓存。
全局缓存就是无视sqlSession,你可以理解为一个分布式的缓存。作为全局的访问。

一、二级缓存

1、开启方式

二级缓存默认是不开启的,所以他需要你手动去开启。开启方式需要满足下面四个条件。

1、需要在核心配置文件,我的是sqlMapConfig.xml中指定,在<setting 中的配置。其实最新版mybatis这个条件其实可以不用,因为在configuration中的cache默认就是true。
2、需要在mapper.xml中引进二级缓存Cache的标签。
3、需要在查询标签<select中引入useCache属性打开。,其实这个也是可以不用配置的,我们这里就先配置上。
4、需要有事务的存在。

第一步和第三步可以不写。
于是我们就来操作验证一下,我们首先在UserMapper.xml中配置第二步和第三步。

<mapper namespace="com.yx.dao.IUserDao">

  <cache></cache>

  <select id="findAll" resultType="com.yx.domain.User" useCache="true">
          SELECT * FROM `user`
  </select>
</mapper>

然后在代码中开启事务的提交。

@Test
public void test3() throws IOException {
 // 读取配置文件转化为流
 InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
 SqlSession sqlSession = factory.openSession();
 SqlSession sqlSession2 = factory.openSession();
 //这⾥不调⽤SqlSession的api,⽽是获得了接⼝对象,调⽤接⼝中的⽅法。使用JDK动态代理产生代理对象
 IUserDao userDao = sqlSession.getMapper(IUserDao.class);
 IUserDao userDao2 = sqlSession2.getMapper(IUserDao.class);
 // 第一次执行查询
 List<User> userList = userDao.findAll();
 userList.forEach(user -> {
   System.out.println(user.toString());
 });
 // 事务提交
 sqlSession.commit();

 System.out.println("*************************************************");

 // 第二次执行相同的查询
 List<User> userList2 = userDao2.findAll();
 userList2.forEach(user -> {
   System.out.println(user.toString());
 });
 // 事务提交
 sqlSession2.commit();
}

我们看下输出:

Opening JDBC Connection
Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
Created connection 203819996.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@c260bdc]
==>  Preparing: SELECT * FROM `user`
==> Parameters: 
<==    Columns: id, name
<==        Row: 1, 张飞
<==        Row: 2, 关羽
<==        Row: 3, 孙权
<==      Total: 3
User{id=1, username='张飞'}
User{id=2, username='关羽'}
User{id=3, username='孙权'}
*************************************************
As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66
Cache Hit Ratio [com.yx.dao.IUserDao]: 0.5// 缓存命中
User{id=1, username='张飞'}
User{id=2, username='关羽'}
User{id=3, username='孙权'}

我们看到缓存被命中了,第二次新的sqlsession中并没有执行sql,也没有创建连接,可见mybatis内部是维护着二级缓存的。而且我们看到命中率是0.5,其实就是我们查了两次,只有第二次命中了,就是百分之五十。

二、源码设计

1、装饰器的使用

我们前面说过装饰器模式,https://blog.csdn.net/liuwenqiang1314/article/details/135583787?spm=1001.2014.3001.5501
在这个二级缓存这里可以看到一个类起主要作用,就是CachingExecutor,他是Executor接口的实现类。而他和其他的SimpleExecutor,BatchExecutor有啥区别呢。
我们上一章知道BatchExecutor和SimpleExecutor通过继承BaseExecutor这个适配器来进行操作数据库。
而CachingExecutor是直接实现了Executor,实际上他是一个装饰器,他用来装饰其他的Executor。怎么证明呢。我们来想想Executor是在哪里创建呢,我们前面说Configuration的时候,知道Configuration不仅仅包罗万象,而且创建了很多基本操作类,其中就包括Exector。我们可以看到他的源码位于。
org.apache.ibatis.session.Configuration#newExecutor

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
   executorType = executorType == null ? defaultExecutorType : executorType;
   executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
   Executor executor;
   // 根据参数类型创建不同的executor ,默认是SimpleExecutor
   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);
   }
    /**
     * 我们看到这里有个配置就是当开启这个cache的时候,就去创建缓存的 CachingExecutor
     * 这里我们说cacheEnabled默认值就是true,protected boolean cacheEnabled = true;
     * 这个变量其实就是mybatis核心配置文件里面的那个setting里面配置的缓存开启,
     * 最后封装在Configuration的cacheEnabled里面,不过默认为true,不用配就开了
     */
   if (cacheEnabled) {
     executor = new CachingExecutor(executor);
   }
   executor = (Executor) interceptorChain.pluginAll(executor);
   return executor;
 }

我们前面说过装饰器的时候,我们说就是套娃,这里可以看到他把上面创建的BatchExecutor,或者是SimpleExecutor或者是ReuseExecutor。为了方便说下面我们统一叫做SimpleExecutor。所以这里就套娃了,他把SimpleExecutor套入CachingExecutor做装饰器的增强。
于是我们就跟入这个增强类也就是CachingExecutor来看一下这个增强发生的地方。

public class CachingExecutor implements Executor {

  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  // 套娃就发生在这里,我们看到他把SimpleExecutor交给了CachingExecutor 的delegate
  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }

我们看到他把SimpleExecutor交给了CachingExecutor 的delegate,那么增强到底发生在哪里呢,既然缓存是针对查询的,那么我们就去看CachingExecutor的查询方法。也就是org.apache.ibatis.executor.CachingExecutor#query

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
   throws SQLException {
 // 去MappedStatement中获取缓存,开启了缓存,其实就是UserMapper.xml中的<cache></cache>这个配置开启了没有
 // 因为MappedStatement封装的就是mapper文件,所以这个变量就是封装在MappedStatement中的
 Cache cache = ms.getCache();
 // 如果存在,也就是开启了
 if (cache != null) {
  // 如果你在sql中配置了flushCache="true",那就会来刷新缓存,实时查询,保证一致性。但是一般不配置,不然缓存其实就没用了,但是有些查询可能需要这个实时性,那就需要在这类sql上配置。而且update操作就会刷新缓存。避免脏数据,而且清空的就是这个MappedStatement ,其他的并不清空。
   flushCacheIfRequired(ms);
   // 如果你的sql标签上指定了useCache=true,现在也是默认的了,不用管了
   if (ms.isUseCache() && resultHandler == null) {
     ensureNoOutParams(ms, boundSql);
     @SuppressWarnings("unchecked")
  // 此时有缓存,直接就拿了,tcm就是装饰器增强的业务,list是为了尊重sql的排序,sql查出来啥顺序就是啥顺序,set和map可能会导致顺序和sql的不一致
     List<E> list = (List<E>) tcm.getObject(cache, key);
     if (list == null) {
       // 这里是你开启了<cache></cache>,然后发现没缓存,其实就是第一次,那也来这里查一次库
       list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
       // 设置第一次查的结果去缓存
       tcm.putObject(cache, key, list); // issue #578 and #116
     }
     return list;
   }
 }
 // 如果没开启<cache></cache>,那就去查SimpleExecutor这个装饰器被装饰的Executor中的查询方法,
 return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

2、何时创建cache

我们说我们配置了cache标签才会使用到,所以那一定是在解析这个标签的时候才触发的。但是我们又知道在核心配置文件中的那个已经没用了,我们主要是在mapper中配置的那个cache才是有用的,于是我们就应该去找到解析mapper封装MappedStatement 的地方去找。路子就是
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);中的build方法,往下
org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
然后其中的mapperElement(root.evalNode(“mappers”));是解析mapper文件的,我们点进去。其中的mapperParser.parse();一看就是解析的,点进去。
configurationElement(parser.evalNode(“/mapper”));这句一看就是找到根目录开始解析。点进去。
org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement

private void configurationElement(XNode context) {
   try {
     String namespace = context.getStringAttribute("namespace");
     if (namespace == null || namespace.isEmpty()) {
       throw new BuilderException("Mapper's namespace cannot be empty");
     }
     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"));
   } catch (Exception e) {
     throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
   }
 }

cacheElement(context.evalNode(“cache”));就是判断这个缓存标签的,我们就知道这里是处理的,点进去看看。

private void cacheElement(XNode context) {
   if (context != null) {
     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);
     Properties props = context.getChildrenAsProperties();
     // 我们看到这里开始使用新的缓存,其实就是这里创建的。
     builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
   }
 }

org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache

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;
 }

此时就创建出来缓存对象了。也就是我们前面说的装饰器模式的那个Cache,不过后面你会套娃,给他不断增强。虽然默认的也没有。我们来看下他怎么build的,我们点进去build的实现。

public Cache build() {
    setDefaultImplementations();
    // 如果你自己实现了cache,这里就会执行你的那个缓存方式,创建你的cache,比如你用redis,就会创建redis方式的cache,至于怎么创建,后面我们再看
    // 底层就是反射,你要是没实现,那就还是他自己的PerpetualCache
    Cache cache = newBaseCacheInstance(implementation, id);
    // cache的seeting有多个配置,这里设置进去你自己配置的多个,后面我们来看,类似这样
    /*<cache>
    <property name="" value=""/>
    <property name="" value=""/>
    </cache>*/
    setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
    // 你没自定义扩展,这里就是默认的PerpetualCache就进这里
    if (PerpetualCache.class.equals(cache.getClass())) {
      // 针对你自己的decorators,来进行增强装饰,你也可以自己配置
      for (Class<? extends Cache> decorator : decorators) {
        cache = newCacheDecoratorInstance(decorator, cache);
        setCacheProperties(cache);
      }
      // 没有你自己的,那就直接进行装饰,装饰的方法也是根据你的配置eviction="FIFO"就是把默认的换成FIFO
      /**
       *  <cache size="" flushInterval="" blocking="" readOnly="">
       *     <property name="" value=""/>
       *     <property name="" value=""/>
       *   </cache>
       */

      /**
       MetaObject metaCache = SystemMetaObject.forObject(cache);
       if (size != null && metaCache.hasSetter("size")) {
       metaCache.setValue("size", size);
       }
       // 配的定时清理就是任务的装饰器,下面有各自的,可以多配,顺序套娃即可,你加了才会给你配置,不加就是默认那两个LoggingCache,SynchronizedCache
       if (clearInterval != null) {
       cache = new ScheduledCache(cache);
       ((ScheduledCache) cache).setClearInterval(clearInterval);
       }
       if (readWrite) {
       cache = new SerializedCache(cache);
       }
       // 这两个是都有的
       cache = new LoggingCache(cache);
       cache = new SynchronizedCache(cache);
       if (blocking) {// 你赔了这个就是走的阻塞队列装饰增强
       cache = new BlockingCache(cache);
       }
       return cache;
       */
      cache = setStandardDecorators(cache);
    }
    // 如果不是默认的,那就给你加强一个装饰器,就是日志装饰器的套娃,其实就是一旦你不默认打日志出来让你看看
    else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
      cache = new LoggingCache(cache);
    }
    return cache;
  }

所以你能看到他会整合你的配置,进行对应的增强,创建出新的cache对象(可能是redis这种),后面再通过装饰器模式,进行功能的增强。这样,cache对象就创建出来了,最终放到mappstatement里面。于是我们就知道CachingExecutor 在使用缓存的时候,需要通过MappedStatement 来获取缓存。Cache cache = ms.getCache();
也就是org.apache.ibatis.executor.CachingExecutor#query

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql){
    // 去MappedStatement中获取缓存,开启了缓存,其实就是UserMapper.xml中的<cache></cache>这个配置开启了没有
    // 因为MappedStatement封装的就是mapper文件,所以这个变量就是封装在MappedStatement中的
    Cache cache = ms.getCache();
   

三、一级二级缓存的时机

我们现在知道一级缓存和二级缓存了,那么问题来了,当我开启二级缓存的时候,是先触发一级缓存呢还是二级缓存呢。我们来看下创建Exector的地方。
org.apache.ibatis.session.Configuration#newExecutor

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);
   }
   /**
    * 我们看到这里有个配置就是当开启这个cache的时候,就去创建缓存的 CachingExecutor
    * 这里我们说cacheEnabled默认值就是true,protected boolean cacheEnabled = true;
    * 这个变量其实就是mybatis核心配置文件里面的那个setting里面配置的缓存开启,
    * 最后封装在Configuration的cacheEnabled里面,不过默认为true,不用配就开了
    */
   if (cacheEnabled) {
     executor = new CachingExecutor(executor);
   }
   executor = (Executor) interceptorChain.pluginAll(executor);
   return executor;
 }

我们看到最后其实当你开启二级缓存的时候他会给你创建出来的是executor = new CachingExecutor(executor);就是CachingExecutor。
CachingExecutor包装了(套娃了)SimpleExecutor,而SimpleExecutor的查询功能是放在适配器BaseExecutor中的。所以他的处理链路是。
CachingExecutor->BaseExecutor->SimpleExecutor
CachingExecutor是去查二级缓存的,BaseExecutor是去查一级缓存的,所以其实是先二级再一级。可以debug看一下。
而缓存是放在MappedStatement中,按照每个mapper的维度来封存。

1、debug验证

断点跟这个方法就看到了。今天乙流,就不弄了,很简单的。
org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值