Mybatis原理剖析之二级缓存(五)

概述

由于一级缓存存在这样或那样的问题(具体问题参考 Mybatis原理剖析之一级缓存(四)),Mybatis设计并研发了二级缓存,在一定程度上完善了Mybatis的缓存机制。

MyBatis的二级缓存是Application级别的缓存,它可以提高对数据库查询的效率,以提高应用的性能。本文将深入分析MyBatis的二级缓存的设计以及实现原理。

二级缓存

追踪ExecutorType

Mybatis原理剖析之一级缓存(四)这篇文章我们了解到,当打开一个会话时,一个SqlSession对象会创建一个Executor对象,而这个Executor对象则是用来执行实际的数据库操作。

实际上,当我们开启二级缓存的时候,Mybatis就是对这个Executor做了手脚,如果不开启二级缓存,Mybatis默认生成的是BaseExecutor,而开启二级缓存之后,Mybatis生成的则是CachingExecutor。

接下来,我们通过源代码来证实我上面所说的理论。

首先我们打开SqlSession用来创建Executor的代码,即DefaultSqlSession的构造方法。

public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
    this.configuration = configuration;
    this.executor = executor;
    this.dirty = false;
    this.autoCommit = autoCommit;
}

我们发现,executor并不是DefaultSqlSession创建的,而是上层创建好之后传进来的,因为DefaultSqlSession是有DefaultSqlSessionFactory创建,所以我们打开DefaultSqlSessionFactory的openSession()方法。

@Override
public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

非常简单的一行代码,执行器类型由configuration.getDefaultExecutorType()方法得到,可以推断,一定是Mybatis在初始阶段加载XML配置文件的时候,初始化了这个DefaultExecutorType字段,而DefaultExecutorType这个字段的默认值是BaseExecutor,如果配置了二级缓存,则会变为CachingExecutor。

接下来证实一下我们的猜想,打开configuration的getDefaultExecutorType()方法。

public void setDefaultExecutorType(ExecutorType defaultExecutorType) {
    this.defaultExecutorType = defaultExecutorType;
}

看这里其实我们看不出来什么,因为不知道defaultExecutorType的值具体是什么,所以我们还是要回到解析XML阶段,解析二级缓存标签的部分。

所以我们需要了解一下,二级缓存是如何配置的,只有这样,我们才能够找到解析标签的代码,然后顺藤摸瓜,再追踪到configuration。

二级缓存配置流程

若要使某个select查询语句支持二级缓存,需要保证以下三个部分配置。

  • 在SqlMapConfig.xml,也就是全局配置文件中的标签下添加标签,name属性设置为cacheEnabled,value属性设置为true。它的意思是全局开启二级缓存,如果不配置这个属性,那么在mapper.xml里面不管怎么配置也不会起作用。
  • select语句所在的mapper.xml的标签下,添加或者标签。
    注意:为什么这里可以添加二选一的标签呢,稍后我们会详细解释。
  • 该select语句对应标签的useCache属性设置为true。表示本条select语句开启二级缓存,而与其他的select语句是否缓存没有任何关系。
cache与cache-ref

首先要明确 : 二级缓存是面向mapper的缓存。也就是说一个mapper对应一个二级缓存,但是其实这句话也不完全正确,有些业务场景其实是可以多个mapper对应一个缓存的。比如数据库中有两个表,这两个表联系非常密切,那么缓存的过程中,完全把这两个表的查询结果放入同一个缓存。

那么问题来了,既然多个mapper共用一个缓存,那么这个二级缓存配置到哪个mapper.xml文件里边呢,其实这个是随意指定的,当配置好这个mapper.xml的标签之后,其他需要共用二级缓存的标签则只需要添加标签,并将namespace属性值设置为配置cache标签的mapper.xml的namespace值即可。

解析cache

解析cacheEnabled

代码如下

XMLConfigBuilder

private void parseConfiguration(XNode root) {
    try {
      propertiesElement(root.evalNode("properties"));
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      settingsElement(root.evalNode("settings"));
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

private void settingsElement(XNode context) throws Exception {
      
      ...
      
      configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
      
      ...
      
}

Configuration

protected boolean cacheEnabled = true;

public void setCacheEnabled(boolean cacheEnabled) {
	this.cacheEnabled = cacheEnabled;
}

cacheEnabled的解析比较简单,其实就是使用Configuration的boolean类型的cacheEnabled进行控制,默认值是true。
所以,如果不配置这个标签,只配置mapper里边的两步,也是可以使用二级缓存的。

解析cache和cache-ref

Configuration的configurationElement代码如下

private void configurationElement(XNode context) {
      
      ...
      
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      
      ...
      
}

首先看cacheElement方法,解析cache标签。

private void cacheElement(XNode context) throws Exception {
    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);
    }
}

首先,判断标签是否为空,若不为空,则表示本mapper开启二级缓存,然后解析cache的各种属性信息,比如数据淘汰策略。然后调用助手类builderAssistant的useNewCache方法。

public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    typeClass = valueOrDefault(typeClass, PerpetualCache.class);
    evictionClass = valueOrDefault(evictionClass, LruCache.class);
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(typeClass)
        .addDecorator(evictionClass)
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
}

根据builder模式,构建Cache对象,注意每一个Cache都有一个id,这个id其实就是mapper的namespace。
然后将cache赋值给builderAssistant的currentCache字段,读者朋友们,请记住这行代码,后边要用到。
看Cache代码。

public CacheBuilder(String id) {
    this.id = id;
    this.decorators = new ArrayList<Class<? extends Cache>>();
}

然后调用configuration的addCache添加Cache。

protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");

 public void addCache(Cache cache) {
    caches.put(cache.getId(), cache);
}

至此,Cache标签解析结束,解析结果为每一个mapper对应一个Cache,并保存在一个变异的HashMap(StrictMap)内,key为mapper的namespace。

解析cache-ref的过程与解析cache大同小异,此处略过吧。

追踪ExecutorType

等等,我们解析了半天,可是并没有发现有改变DefaultExecutorType的行为啊,难道是我们的分析有误?我们回过头再重新看一下DefaultSqlSessionFactory类的构造DefaultSqlSession的过程,也就是openSessionFromDataSource()方法。

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);
      //生成一个执行器(事务包含在执行器里)
      final Executor executor = configuration.newExecutor(tx, execType);
      //然后产生一个DefaultSqlSession
      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();
    }
 }

再看这行代码

final Executor executor = configuration.newExecutor(tx, execType);

难道是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);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

仔细看前几个if-else,不管怎么样创建Executor,前几个if-else必有一个会执行的,这说明了什么呢,我们再看看ExecutorType这个枚举类型。

public enum ExecutorType {
  SIMPLE, REUSE, BATCH
}

好家伙,感情只有SIMPLE, REUSE, BATCH这三种执行器,CachingExecutor并不属于真正的执行器,再看下边这行代码

if (cacheEnabled) {
      executor = new CachingExecutor(executor);
}

如果设置了cacheEnabled,就new了一个CachingExecutor,并把executor传了进去。
等等,你想到了什么?对,如果你对设计模式有了解的话,那一定知道这里肯定用到了设计模式,并且很有可能是代理模式或者装饰器模式。姑且先这么认为吧,打开CachingExecutor源代码。

public class CachingExecutor implements Executor {

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

  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);

  @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
	//刷新缓存完再update
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
  }
  @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) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, parameterObject, boundSql);
        @SuppressWarnings("unchecked")
        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);
  }
}

果然,是装饰器模式,如果你还没看懂,那么我推荐你看一下我的另外一篇博客设计模式之装饰器模式

简单来讲,其实就是从MappedStatement中获取到Cache,然后做了一些关于缓存的事,具体做了啥事,我们在这里暂时不关心,然后,使用真正的执行器执行sql语句。

但是有一个地方不太对,之前我们是把Cache放入了Configuration中啊,这次这个Cache怎么从MappedStatement中跑出来了呢,其实有一段代码,我们忘了分析了。

打开MapperBuilderAssistant类的addMappedStatement方法(这里我略去了一些代码,看起来更清晰)

public MappedStatement addMappedStatement(
      String id,
      SqlSource sqlSource,
      StatementType statementType,
      SqlCommandType sqlCommandType,
      Integer fetchSize,
      Integer timeout,
      String parameterMap,
      Class<?> parameterType,
      String resultMap,
      Class<?> resultType,
      ResultSetType resultSetType,
      boolean flushCache,
      boolean useCache,
      boolean resultOrdered,
      KeyGenerator keyGenerator,
      String keyProperty,
      String keyColumn,
      String databaseId,
      LanguageDriver lang,
      String resultSets) {
 
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType);
    statementBuilder.resource(resource);
    statementBuilder.fetchSize(fetchSize);
    statementBuilder.statementType(statementType);
    statementBuilder.keyGenerator(keyGenerator);
    statementBuilder.keyProperty(keyProperty);
    statementBuilder.keyColumn(keyColumn);
    statementBuilder.databaseId(databaseId);
    statementBuilder.lang(lang);
    statementBuilder.resultOrdered(resultOrdered);
    statementBuilder.resulSets(resultSets);
    setStatementTimeout(timeout, statementBuilder);

    ...
    
    setStatementCache(isSelect, flushCache, useCache, currentCache, statementBuilder);

    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
}

发现,构建完MappedStatement之后,又调用了setStatementCache方法设置了一下currentCache,将Cache添加到了MappedStatement,然后将MappedStatement添加到了configuration。currentCache哪来的?请看上边有一行加粗的解释。

到此,我们基本明白了二级缓存的基本流程。

  • 解析cacheEnabled,并赋值给Configuration的boolean类型的cacheEnabled成员变量。
  • 解析mapper.xml的标签,并转化为对象,赋值给MapperBuilderAssistant的currentCache。
  • MapperBuilderAssistant添加MappedStatement对象之后,设置了一下currentCache,然后将MappedStatement添加到Configuration。
  • DefaultSqlSessionFactory调用openSession过程中,首先调用了Configuration的newExecutor,根据ExecutorType创建Executor,然后装饰器模式构建CachingExecutor对象。
  • DefaultSqlSessionFactory调用openSessionFromDataSource,将CachingExecutor赋值给DefaultSqlSession的executor成员变量。
  • 执行真正的sql操作时,调用CachingExecutor相应操作。首先根据并在sql操作之前,根据MappedStatement获取Cache,然后在sql操作之前和之后进行相应的缓存操作。

由于Cache由MappedStatement持有,所以二级缓存是跨SqlSession的。

缓存优先级

如果你对Mybatis配置了二级缓存,那么二级缓存,一级缓存,数据库的优先级为:

二级缓存 > 一级缓存 > 数据库

温馨提示

  • 如果您对本文有疑问,请在评论部分留言,我会在最短时间回复。
  • 如果本文帮助了您,也请评论,作为对我的一份鼓励。
  • 如果您感觉我写的有问题,也请批评指正,我会尽量修改。
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值