• Mybatis 缓存 源码分析


    1. 结论

    老规矩,先说结论。给各位大兄弟来点总结。

    mybatis有两级缓存,分别是:

    • SqlSession级别
    • Mapper级别

    想必大家都对这个结论不陌生,但是有许多人其实并不明白具体原因。所以今天就和各位大兄弟一起来探讨一下具体代码。

    2. Show code

    这个缓存就得从创建执行器开始,org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType),创建执行器是从创建SqlSession开始的, 这个看过上一篇文章的大兄弟应有了解,就不一一说啦。先上代码。

      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);
        }
        if (cacheEnabled) { // 是否开启二级缓存,这个属性是从配置文件中解析出来的。二级缓存默认是true.
          executor = new CachingExecutor(executor);
        }
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
      }
    

    这段代码,我们可以看出,如果在配置文件中,没有开启二级缓存,则会直接创建一个Executor, 如果开启了二级缓存,则会把创建的执行器进行包装。 那我们就从没有开启二级缓存查看。(为什么要直接分析SimpleExecutor先不说,卖个关子)

    2.1 SimpleExecutor

    直接点进SimpleExecutor,发现没啥东西,直接点进父类构造函数。(BaseExecutor有三个默认实现,分别是:BatchExecutorReuseExecutorSimpleExecutor。所以小伙伴不要纠结为什么选择SimpleExecutor了)代码如下。

      protected BaseExecutor(Configuration configuration, Transaction transaction) {
        this.transaction = transaction;
        this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
        this.localCache = new PerpetualCache("LocalCache");
        this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
        this.closed = false;
        this.configuration = configuration;
        this.wrapper = this;
      }
    

    点进父类的构造函数,我们大概也看出来了,在父类的构造函数中,的确存在名字叫localCache。 那不行,我们只看到了Cahce,得找到使用的地方。缓存一般都是用来缓存查询内容的,那我们就去找select,query方法咯。

      private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        List<E> list;
        // 用来延迟加载,表示当前key已经被缓存
        localCache.putObject(key, EXECUTION_PLACEHOLDER);
        try {
          list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
          localCache.removeObject(key);
        }
        // 放入缓存中
        localCache.putObject(key, list);
        if (ms.getStatementType() == StatementType.CALLABLE) {
          localOutputParameterCache.putObject(key, parameter);
        }
        return list;
      }
    

    从一个query方法中,点到最后。我们不用关注其中细节,在最终query方法中,的确是放入缓存中。我们看一下localCache的具体类型。代码如下。

    public class PerpetualCache implements Cache {
    
      private final String id;
    
      private Map<Object, Object> cache = new HashMap<Object, Object>();
    
      public PerpetualCache(String id) {
        this.id = id;
      }
    }
    

    很简单,就是一个id 和 一个map。 ,这也就是说明了,mybatis是用map来缓存数据的。

    我们看到现在,算是明白了,mybatis中的一级缓存是自动开启的,不需要什么配置文件,也只是用map来缓存的。

    2.2 CacheExecutor

    从上面代码看,CacheExecutor是对SimpleExecutor进行了包装。那就进代码看看。

      public CachingExecutor(Executor delegate) {
        this.delegate = delegate;
        // 进行包装, 从实现类字面理解,就是延迟加载
        delegate.setExecutorWrapper(this);
      }
    

    我们不关注这么多, 我们就一个目的,搞清楚这个cache。 老规矩,查缓存从select或query方法找起来。

      public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
          throws SQLException {
        // 从mappedStatement中获取缓存。
        Cache cache = ms.getCache();
        if (cache != null) {
          flushCacheIfRequired(ms);
          if (ms.isUseCache() && resultHandler == null) {
            ensureNoOutParams(ms, boundSql);
            @SuppressWarnings("unchecked")
           	// 把cache和key传递进去,查看是否有缓存数据
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) {
              list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
              // 把数据放入缓存
              tcm.putObject(cache, key, list); // issue #578 and #116
            }
            return list;
          }
        }
        return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
      }
    

    我们基本看出来,这个ms.getCache()是至关重要的,因为在代码中,接下来的操作都和这个cache有关系。 那么唯一的一件事就是搞清楚这个cache是从哪来的。我们就从MappedStatement的方方法开始看,这个cache是如何生成的。

    public final class MappedStatement {
      ....
      public Builder cache(Cache cache) {
        mappedStatement.cache = cache;
        return this;
      }
      ...
    }
    

    我们不难看出来,这个Cache通过传递进来,然后直接赋值给了MappedStatement的变量。 那么我们就点击一下这个方法,看被哪调用。

    org.apache.ibatis.builder.MapperBuilderAssistant#addMappedStatement(...){
      MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
           	...
            .useCache(valueOrDefault(useCache, isSelect))
        		// 传入cache
            .cache(currentCache);
    }
    

    我们发现被这个方法,这个方法有点熟悉,就是我们前一篇文章讲到的,在解析mapper,构建MappedStatement的方法。这个传入的currentCache不是当期方法的,是当前类的,那我们就要看当那类是怎么构建出来的。我们直接看这个方法被哪调用,然后顺藤摸瓜,找到创建的地方。

    public void parseStatementNode() {
        String id = context.getStringAttribute("id");
        String databaseId = context.getStringAttribute("databaseId");
    
       	...
        // 通过使用builderAssistant添加mapper. 我们要接着看builderAssistant是如何构建的
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered, 
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
      }
    

    note: 看到这些代码并不陌生,我们现在反着推断了。 有的小伙伴肯定好奇,我是如何找到这些方法的, 如何你有这个疑问,就需要看一下前一篇mybatis的文章啦!!

    因为builderAssistant是类成员变量,那我们接着看parseStatementNode()方法被哪调用,然后找到构建builderAssistant的地方

     private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
        for (XNode context : list) {
          final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
          try {
            statementParser.parseStatementNode();
          } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
          }
        }
      }
    

    到这仍发现builderAssistant是传递过来的,那我们就接着往上翻代码。找到当前类是如何构建的。

    ....

    一直往上翻,有很多代码,道理相同,我们最后找到了如下代码。

    private void configurationElement(XNode context) {
        try {
          String namespace = context.getStringAttribute("namespace");
          if (namespace == null || namespace.equals("")) {
            throw new BuilderException("Mapper's namespace cannot be empty");
          }
          // 设置namespace
          builderAssistant.setCurrentNamespace(namespace);
          cacheRefElement(context.evalNode("cache-ref"));
          // 构建cache
          cacheElement(context.evalNode("cache"));
          parameterMapElement(context.evalNodes("/mapper/parameterMap"));
          resultMapElements(context.evalNodes("/mapper/resultMap"));
          sqlElement(context.evalNodes("/mapper/sql"));
          // 解析mappedStatement
          buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
        } catch (Exception e) {
          ...
        }
      }
    

    这个熟悉的感觉。 我们看大了给builderAssistant设置namespace。并且有关于cache的代码,这个时候就不要点走啦,得进去看看了,因为已经到了我们关心的点。点进cacheElement(context.evalNode("cache"));一探究竟。

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

    发现最终调用了org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache的方法,我们也如愿看到了cache的产生,并且是以namespace为单位的。

    自此,我们搞清楚了一条大概的逻辑。

    graph TD MappedStatement1 --> |imp|Mapper1 MappedStatement2 --> |imp|Mapper1 MappedStatement4 --> |imp|Mapper2 MappedStatement5 --> |imp|Mapper2 Mapper1 --> |imp|Cache1 Mapper2 --> |imp|Cache2 Cache1 --> |namespace1|builderAssistant1((builderAssistant)) Cache2 --> |namespace2|builderAssistant1((builderAssistant))

    3 细说

    现在大体逻辑是搞懂了,但是代码可能各位大兄弟看着有些糊涂,建议在电脑自己看看,加深一下印象。 毕竟看别人千遍,不如自己来实现一遍。

  • 相关阅读:
    JS数组常用方法参考---5、sort方法
    JS数组常用方法---1、课程介绍
    JS数组常用方法参考---4、unshift方法
    JS数组常用方法参考---3、队列对应的方法
    ES6课程---12、面向对象
    legend3---30、不同类型的网站打包方案
    数据库Sharding的基本思想和切分策略
    用sharding技术来扩展你的数据库(一)sharding 介绍
    什么是Scale Up和Scale Out?
    淘宝网采用什么技术架构来实现网站高负载的
  • 原文地址:https://www.cnblogs.com/lifacheng/p/13245990.html
Copyright © 2020-2023  润新知