Mybatis-3 源码之缓存是怎么创建的

微信搜索 程序员的起飞之路 可以加我公众号,保证一有干货就更新~
二维码如下:

公众号

Mybatis-3 源码之缓存是怎么创建的

Mybatis 缓存问题其实也是面试高频的问题了,今天我们就从源码级别来谈谈 Mybatis 的缓存实现。

(本文源码均在 https://github.com/ccqctljx/Mybatis-3 中,会持续更新注释和 Demo)。

首先我们了解一下缓存是什么:缓存是一般的 ORM 框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力。直白一点就是,开了缓存后,同样的数据查询不必再次访问数据库,直接从缓存中拿即可。

那么面试官常问的 一级缓存 和 二级缓存 又都是什么呢?

一级缓存:一级缓存又称本地缓存,是在会话(SqlSession)层面进行的缓存。随会话开始而生,结束而死。MyBatis 的一级缓存是默认开启的,不需要任何的配置。

二级缓存:由于一级缓存随会话而生,就不能跨会话共享。二级缓存则是用来解决这个问题的,他的范围是 namespace 级别的,可以被多个SqlSession 共享,生命周期和 SqlSessionFactory 同步。只要是同一个 SqlSessionFactory 创建出来的会话,即可共享相同 namespace 级别的缓存。二级缓存需要配置三个地方:

第一个是在 mybaits-config.xml 配置文件中设置开启缓存:

第二个是要在 Mapper 文件中配置 标签

第三个是在需要使用缓存的语句上加入 useCache=“true”

那么一级二级缓存有没有执行顺序什么的呢?答案是有的,如果开启二级缓存那么执行顺序为:

缓存使用顺序

那么我们写个实例代码,来看下一二级缓存的效果吧

public class Demo {
  public static void main(String[] args) throws IOException {
​
    String resource = "mybatis/mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
​
    List<BookInfo> bookInfoList1 = sqlSession1.selectList("com.simon.demo.TestMapper.selectBookInfo");
    System.out.println(" sqlSession 1 query 1 ----------------------------- " + bookInfoList1);
​
    List<BookInfo> bookInfoList2 = sqlSession1.selectList("com.simon.demo.TestMapper.selectBookInfo");
    System.out.println("sqlSession 1 query 2 -----------------------------" + bookInfoList2);
​
    sqlSession1.commit();
    System.out.println("sqlSession 1 commit -----------------------------");
​
    List<BookInfo> bookInfoList3 = sqlSession2.selectList("com.simon.demo.TestMapper.selectBookInfo");
    System.out.println("sqlSession 2 query 1 ----------------------------- " + bookInfoList3);
  }
}

打印结果是:
在这里插入图片描述
由此我们能看到,只有第一次查询执行了 sql,其余两次查询均未去数据库中查询。这就是缓存的效用啦。

我们接下来去到源码来看一下究竟是如何生效的吧。

二级缓存创建过程一:加载配置类

首先,我们创建 SqlSessionFactory 工厂时,会从配置文件中加载所有的配置并生成 Configuration 对象,然后将 Configuration 对象放在 SqlSessionFactory 实例对象中维护起来。解析代码如下

package org.apache.ibatis.builder.xml;
public class XMLConfigBuilder extends BaseBuilder {
  ……
  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));// 解析配置文件里的 setting 标签
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      // 生成别名 map 放进 configuration 中后备使用
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));// 解析配置文件里的 mappers 标签
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
  
  /**
   * 把 settings 标签的所有配置加载成 Properties
   * @param context
   * @return
   */
  private Properties settingsAsProperties(XNode context) {
    if (context == null) {
      return new Properties();
    }
    Properties props = context.getChildrenAsProperties();
    // Check that all settings are known to the configuration class
    MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
    for (Object key : props.keySet()) {
      if (!metaConfig.hasSetter(String.valueOf(key))) {
        throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
      }
    }
    return props;
  }
  
  /**
   * 设置全局上下文属性
   */
  private void settingsElement(Properties props) {
    ……
    configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
    configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
    ……
  }
  ……
}

方法 settingsAsProperties 将配置文件中 setting 标签读为 Properties 对象,然后在 settingsElement 方法中全部赋给 configuration 对象,这其中就有对 cache 标签的处理,将 。这个 Configuration 是 BaseBuilder 中描述全局配置的一个类,后面会将它扔给 SqlSessionFactory ,作为全局上下文。

这里还有个方法比较重要,就是 typeAliasesElement 方法,这个方法是将我们配置好的一些别名类,以键值对的形式存储在 TypeAliasRegistry 类中的一个 HashMap 中,例如 “byte” -> Byte.class。这个 TypeAliasRegistry 也会被放入全局配置 Configuration 中。

二级缓存创建过程二:创建 Cache 对象并绑定 Mapper

解析配置文件后,mybatis 知道自己需要开启二级缓存,于是开始了创建缓存之路,首先,先扫描所有 Mapper 文件位置,然后一个个分析过去(此处以 resource 为例分析):

package org.apache.ibatis.builder.xml;
public class XMLConfigBuilder extends BaseBuilder {
  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      // 遍历 mybatis-config.xml 文件下面的 mappers 节点的子节点
      for (XNode child : parent.getChildren()) {
        // 判断是否是 Package,如果是的话可以直接拿 Package 去加载包下的 mapper 文件
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          // 如果不是的话,就是 mapper 标签(因为 xml 中只允许写这两种标签)
          // 然后拿相应的属性,去分别作解析
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");// 解析 resource 表明位置的 mapper
          if (resource != null && url == null && mapperClass == null) {
            // 此处定义错误上下文,如果这里加载出错日志打印 ("### The error may exist in xxx");
            ErrorContext.instance().resource(resource);
            // 读取 配置文件 成流
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            // 解析具体的 mapper 文件
            mapperParser.parse();
          }// 解析 url 表明位置的 mapper
          else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          }// 解析 mapperClass 表明位置的 mapper
          else if (resource == null && url == null && mapperClass != null) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }
}

找到 Mapper 后,开始针对 Mapper 的解析:

package org.apache.ibatis.builder.xml;
public class XMLMapperBuilder extends BaseBuilder {
  ……
  public void parse() {
    // 因为是公共方法,多处调用,所以这里先判断有没有加载过
    if (!configuration.isResourceLoaded(resource)) {
      // 没加载过的话,先去加载资源,这里创建了 Cache 对象
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }
  
  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);
      // 这两行是开启二级缓存比较关键的两步
      // 这一步拿了别人的 cache 对象 设置给自己了
      cacheRefElement(context.evalNode("cache-ref"));
      // 在这一步中构建了 Cache 对象
      cacheElement(context.evalNode("cache"));
      // 解析参数 Map
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // 解析 resultMap
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      // 解析每个 sql 标签(mapper 中有两种 sql,一种是 下面要解析的四打标签,还有直接用 sql 标签的)
      sqlElement(context.evalNodes("/mapper/sql"));
      // 解析四大标签,并放入 configuration 中,这里也会为每个开启缓存的 statement 设置上面生成好的缓存对象,也就是 Cache
      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);
    }
  }
  ……
}

这里我们跟缓存相关的有三步,第一步 cacheRefElement 是看看 mapper 中是否标注了 标签,这个标签的意思是 我可以跟其他 namespace 的 mapper 共用一个 Cache。源码其实就是把 Configuration 中加载好的指定 mapper 的 Cache 对象引用给自己。我们重点看创建 Cache 对象的方法也就是 cacheElement(context.evalNode(“cache”));

private void cacheElement(XNode context) {
  if (context != null) {
    // 如果不指定类型,则默认缓存类型设置为 PERPETUAL
    String type = context.getStringAttribute("type", "PERPETUAL");
    // typeAliasRegistry 内部维护了一个 HashMap 并且预设了很多类别名,例如 "byte" -> Byte.class
    // 这里指的就是之前加载配置时 typeAliasesElement 方法所做的
    Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
    // eviction 意为驱逐、赶出。这里则代表着 缓存清除策略,即如何清除无用的缓存
    // 代码可以看到,默认是 LRU 即 移除最长时间不被使用的对象。
    // 官网文档共设有四种如下:
    /**
      LRU – Least Recently Used: Removes objects that haven't been used for the longst period of time.(清除长时间不用的)
      FIFO – First In First Out: Removes objects in the order that they entered the cache.(清除最开始放进去的)
      SOFT – Soft Reference: Removes objects based on the garbage collector state and the rules of Soft References.(软引用式清除)
      WEAK – Weak Reference: More aggressively removes objects based on the garbage collector state and rules of Weak References.(弱引用式清除)
    */
    String eviction = context.getStringAttribute("eviction", "LRU");
    Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
    // 刷新间隔,单位 毫秒,代表一个合理的毫秒形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用 update 语句时刷新。
    Long flushInterval = context.getLongAttribute("flushInterval");
    // 引用数目,要记住你缓存的对象数目和你运行环境的可用内存资源数目。默认值是1024。
    Integer size = context.getIntAttribute("size");// 下面是针对缓存对象实例是否只读的配置
    // 只读的缓存会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改(一旦修改,别人取到的也是修改后的)。这提供了很重要的性能优势。
    // 可读写的缓存会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是false。
    boolean readWrite = !context.getBooleanAttribute("readOnly", false);
    // 设置是否是阻塞缓存,如果是 true ,则在创建缓存的时候会包装一层 BlockingCache 。默认为 false
    boolean blocking = context.getBooleanAttribute("blocking", false);
    Properties props = context.getChildrenAsProperties();
    // 此方法构建了一个新的 Cache 对象并设置到了 configuration 中。
    builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
  }
  
  public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    // 此处使用建造者模式创建了 Cache,并且绑定了当前 Mapper 的命名空间并作为此 Cache 的 ID。
    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后,加入到 configuration 中等待调用。
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
  }

创建完毕后,这里调用了 configuration.addCache(cache) 方法将生成好的 cache 放进了 configuration 对象中,实际上就是将 cache 对象put 进了 Configuration 类内部维护的一个 StrictMap中,而这个 StrictMap 则是继承自 HashMap, 也就是说归根结底这里是将 cache 以 currentNamespace 为Key 放入了一个 HashMap 中。

二级缓存创建过程三:为每个sql语句绑定 cache

在生成 Cache 对象后,Mapper 文件会将本 mapper 中所有的语句标签生成一个个 MappedStatement ,在这个过程中,会给每个 statement 绑定上二级缓存,使得他可以直接使用。

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");// 如果数据库 id 不为空且匹配不上的话,不进行下面的加载工作
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
​
    String nodeName = context.getNode().getNodeName();
    // 此处拿的是标签,insert | update | delete | select
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    // 是否是 select 语句
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    // 是否清除缓存
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    // 是否使用二级缓存
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    // 结果是否排序
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    
    ······
    
    // 配置一系列属性,标签上的对应属性可以在这里看到
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");// 构建解析完成的 MappedStatement ,也就是将 <select></select> 标签中的东西转为对象
    // 此处绑定了二级缓存
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

构造 mappedStatement 的过程像构建 Cache 一样又臭又长,此处就不再赘述,感兴趣的小伙伴可以自行去看~

以上就是二级缓存的创建过程。二级缓存如此复杂,那么一级缓存呢?

一级缓存创建过程:

一级缓存的创建过程其实比二级缓存要简单得多,他不用考虑跨会话执行的问题,所以仅仅在创建当前会话(SQLSession)时,新建一个缓存对象即可,也就是代码中的 localCache ,如:

  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);
      // 这里返回的 Executor 每次都是新的
      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();
    }
  }
  
  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    // 这里如果传进来的 executorType 为空,则采用默认的,如果默认的为空,则采用 simple
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;// 注意这里创建的所有类型的 Executor 实际上都继承自 BaseExecutor
    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) {
​
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }
  
public class SimpleExecutor extends BaseExecutor {public SimpleExecutor(Configuration configuration, Transaction transaction) {
    // 这里执行了父类的构造方法
    super(configuration, transaction);
  }
  ······
}public abstract class BaseExecutor implements Executor {private static final Log log = LogFactory.getLog(BaseExecutor.class);protected Transaction transaction;
  protected Executor wrapper;protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
  protected PerpetualCache localCache;
  protected PerpetualCache localOutputParameterCache;
  protected Configuration configuration;protected int queryStack;
  private boolean closed;protected BaseExecutor(Configuration configuration, Transaction transaction) {
    this.transaction = transaction;
    this.deferredLoads = new ConcurrentLinkedQueue<>();
    // 这里新建了一个新的缓存
    this.localCache = new PerpetualCache("LocalCache");
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    this.closed = false;
    this.configuration = configuration;
    this.wrapper = this;
  }
  ······
}

这个 PerpetualCache 是最普通的缓存,内部维护了一个 HashMap 作为缓存承载体。

正如注释所说,每次新开一个会话时,这个 Executor 都会被新建。于是内部维护的缓存自然是每次都更新,也就不存在跨 SQLSession 一说了。

总结一下:

  • 一级缓存的创建随着每次 SQLSession 的开启而创建,仅仅是 Executor 中维护的一个 简单缓存对象,内部以 HashMap 做实现。
  • 二级缓存的创建过程是先读取 mybatis-config.xml 文件确认缓存开启,然后根据 mapper 文件中的 cache 或 cache-ref 标签来创建缓存对象,以 namespace 为id 放在 Configuration 中,并且在解析 mapper 文件中每个 sql 语句时将 cache 对象绑定上。

本期内容到此结束,其中主要讲述了 mybatis 一、二级缓存的创建过程,重点主要放在了二级缓存的创建过程。具体是如何使用的,缓存又在什么时候被清空,敬请期待下期文章,笔者会一一道来。 如果有没看懂的地方或讲错的地方,欢迎留言询问和质疑~

读书越多越发现自己的无知,Keep Fighting!

欢迎友善交流,不喜勿喷~

Hope can help~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值