mybatis二级缓存原理

在mybatis中,一级缓存是默认开启的,无法关闭;二级缓存可以选择开启或者关闭,这篇博客主要介绍二级缓存的原理,具体的使用,不再介绍了

解析

二级缓存在使用的时候,需要先进行解析,对二级缓存的处理,主要体现在几个方面

  1. 对mapper.xml文件的解析,在解析时,会解析节点
  2. 初始化executor对象时,会判断是否开启二级缓存
  3. 在sql执行时,会使用到二级缓存

解析节点

org.apache.ibatis.builder.xml.XMLMapperBuilder#cacheElement

这是解析节点的方法,至于怎么调用过来的,可以看下前面mybatis源码的博客,有介绍过
这里解析的是mapper.xml中的节点

private void cacheElement(XNode context) {
    if (context != null) {
      String type = context.getStringAttribute("type", "PERPETUAL");
      //这里是根据type从存放别名的map中获取对应的class类
      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);
  }
}

可以看到,这个方法中,是解析节点中各个属性的配置,在解析出来配置的属性之后,会通过builderAssistant.useNewCache()这个方法,把所有属性聚合到一个cache对象中
在useNewCache方法中,会通过

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();

这么一段代码,初始化一个cache对象,所以我们需要注重看的是这里的cache对象是怎么初始化的,我们来看下其build()方法

public Cache build() {
   // 设置默认的缓存实现类
   setDefaultImplementations();
   Cache cache = newBaseCacheInstance(implementation, id);
   setCacheProperties(cache);
   // issue #352, do not apply decorators to custom caches
   // 判断是否是PerpetualCache
   if (PerpetualCache.class.equals(cache.getClass())) {
     for (Class<? extends Cache> decorator : decorators) {
       cache = newCacheDecoratorInstance(decorator, cache);
       setCacheProperties(cache);
     }
     // 在这里,会开始包装二级缓存需要使用的cache
     cache = setStandardDecorators(cache);
   } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
     cache = new LoggingCache(cache);
   }
   return cache;
}

这个方法中,简单看下逻辑:先设置默认的缓存实现类,然后判断当前的implementation是否是PerpetualCache;如果是,核心逻辑在

cache = setStandardDecorators(cache);

private Cache setStandardDecorators(Cache cache) {
    try {
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      if (size != null && metaCache.hasSetter("size")) {
        metaCache.setValue("size", size);
      }
      if (clearInterval != null) {
        cache = new ScheduledCache(cache);
        ((ScheduledCache) cache).setClearInterval(clearInterval);
      }
      /**
       * 在lruCache基础之上,包装了一层SerializedCache
       */
      if (readWrite) {
        cache = new SerializedCache(cache);
      }
      /**
       * 在SerializedCache基础之上,包装了一层LoggingCache
       */
      cache = new LoggingCache(cache);
      cache = new SynchronizedCache(cache);
      if (blocking) {
        cache = new BlockingCache(cache);
      }
      return cache;
    } catch (Exception e) {
      throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
  }

可以看到,这里在初始化cache对象的时候,会一层套一层,各种cache对象,所以,我们可以理解为,最后我们返回的是BlockingCache(如果开启了blocking),但是这个对象的内部,会套了多层cache

初始化executor对象

为什么会说到这个点呢?因为我们知道,如果开启了二级缓存,会先调用org.apache.ibatis.executor.CachingExecutor#query()这个方法,如果二级缓存中,没有获取到结果,会调用到org.apache.ibatis.executor.BaseExecutor#query()方法,所以,我们可以认为CachingExecutor是二级缓存的处理逻辑
那为什么二级缓存开启了,会先调用CachingExecutor呢?就要看初始化executor的逻辑

org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSession()
	org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource
		org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    // 默认的executor是SimpleExecutor
    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);
    }
    //如果二级缓存开启,创建cachingExecutor,这里的executor会被包装在cachingExecutor中; 这里的cacheEnabled,是在解析全局配置文件时,解析节点时,赋值的,如果设置了cacheEnable为true,这里的属性值就会是true
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    /**
     * 上面的逻辑是根据不同的executorType类型来初始化不同的执行器
     * 下面是为executor生成动态代理对象
     */
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

这个方法,我们可以看到,在初始化executor对象时,如果开启了二级缓存,会在executor外面再包装一层CachingExecutor;所以,这也是 为什么在sql执行的时候,会先调用到CachingExecutor的query()方法

调用

在sql执行时,会通过sqlSession的selectList/selectOne来完成,在内部,会调用executor的query()方法,所以对于开启了二级缓存的场景下,会先调用CachingExecutor的query()方法

@Override
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) {
      /**
       * 这里的cache是mybatis的二级缓存。如果二级缓存不为null,并且二级缓存开启了,就从二级缓存中获取数据,否则,就从一级缓存中取数据,如果一级缓存中
       * 也没有数据,就从数据库中获取,并将查询结果存入到一级缓存和二级缓存中
       *
       * flushCacheIfRequired():是指xml中,select、update、delete、insert节点的flushCache属性,默认:insert、delete、update是true,select是false(这里的默认值是在解析xml文件的时候,添加的)
       */
      flushCacheIfRequired(ms);
        /**
         * 这里对应的是useCache属性;该属性值:如果是select标签,是true,非select标签为false
         * mapper.xml文件中  标签的属性
         */
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        /**
         * 下面是真正从二级缓存中查询数据,是通过责任链 + 装饰器模式实现的
         * 类似于套娃的形式,一层套一层
         */
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
            /**
             * 调用一级缓存;
             * 从一级缓存中查到数据之后,调用tcm的put方法,这里的put是把对应的对象,放到了一个中间变量map集合中,这样做,是为了防止脏数据
             * 只有在事务提交的时候,才会真正的把数据写入到二级缓存
             */
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    //调用一级缓存
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

这里的注释也比较明白,简单来说,就是先从二级缓存中查询,如果二级缓存可以查到,就返回,查不到,就去一级缓存中查询,所谓的一级缓存,是BaseExecutor来处理的,我们先看下从二级缓存获取的逻辑:
mybatis二级缓存原理_第1张图片
这里可以看到,就是上面解析节点时,包装的层数,现在在使用的时候,会一层一层的调用,最后实际是存储在PerpetualCache中

你可能感兴趣的:(mybatis,java,缓存)