Mybatis-3 源码之缓存是如何使用的

Mybatis-3 源码之缓存是如何使用的

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

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

前期回顾:

上一篇文章主要讲述了 mybatis 一、二级缓存的创建过程(原文点此),重点主要放在了二级缓存的创建过程。要点如下:

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

本期呢,则主要讲讲这个缓存对象创建出来后,到底是怎么给他用的。由于开启二级缓存后,我们查询数据库的执行顺序如下,所以我们按照顺序来一步步深入:
缓存使用顺序

使用缓存第一步:创建 Executor 对象

有过一定源码基础的同学肯定知道,我们 Mybatis 底层执行增删改查操作时,执行对象实际上就是一个个 Executor。那么不例外,我们使用缓存肯定也要在 Executor 上做手脚,那么我们跟随源码来看下 Mybatis 究竟做了什么“手脚”吧:
首先是 sqlSessionFactory.openSession() 时调用的 openSessionFromDataSource 方法

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      // 每次新建 SQLSession 都新创建一个事务
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 这里每次新建 SQLSession 时都返回新的 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();
    }
  }

然后我们跟着代码进入这里的 newExecutor 方法:

  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);
    }
    // 判断之前传进来的 configuration 里是否开启缓存
    if (cacheEnabled) {
      // 这里传进去的 executor 就是后面 query 方法中的 delegate。
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

先说一句题外话,我们看到,根据传入的类型会创建不同类型的 Executor ,而这里的 BatchExecutor、ReuseExecutor 和 SimpleExecutor 实际上都继承了 BaseExecutor 方法,这里 Mybatis 采用了模板模式。定义了很多操作顺序,而由子类实现具体方法。后期会出一个设计模式的板块,敬请期待。
好了,言归正传。我们发现这里有一个很让人欣喜的判断:if (cacheEnabled),嘿我们昨天从 mybatis-config.xml 配置文件里读进来的好像就是这玩意儿!没错就是他,这里会根据你设置 cacheEnabled 的值来决定是否创建 CachingExecutor 。也就是说如果我们设置为 true,这里就会为这些 Executor 们包装上一层 CachingExecutor 。而这个 CachingExecutor 则是二级缓存的关键包装类。
OK,创建 SQLSession 的步骤完成了,我们紧接着来看他的查询方法究竟是怎么使用缓存的吧!

使用缓存第二步:生成缓存 Key

话不多说,我们直接上查询的源码吧,这里以 selectList 为例:
这里追踪源码时,不要忘记实现类是 CachingExecutor
CachingExecutor

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 根据 ms、参数、分页参数、sql 生成这个 statement 唯一的缓存 key
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

我们继续追踪生成 key 的方法:

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // 新建一个 CacheKey,并更新 cacheKey 的 hashcode
    CacheKey cacheKey = new CacheKey();
    // 附加计算当前 sql 的 id,即 <select id = "xxxx"><select>
    cacheKey.update(ms.getId());
    // 附加计算分页中的 offset
    cacheKey.update(rowBounds.getOffset());
    // 附加计算分页中的 limit
    cacheKey.update(rowBounds.getLimit());
    // 附加计算 sql 语句
    cacheKey.update(boundSql.getSql());
    // 取到参数映射
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    // 拿到配置中加载好的 处理类 注册簿,内部维护了一个 HashMap
    // 加载步骤为 org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration 方法中的 typeHandlerElement 方法
    // 以键值对形式存储每个类型的 typeHandler 如 Boolean.class -> new BooleanTypeHandler()
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    // 模仿DefaultParameterHandler逻辑
    for (ParameterMapping parameterMapping : parameterMappings) {
      // 判断这里的参数不是存储过程的 out 类参数
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        // 拿到属性名称
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          // 如果有附加参数,取出附加参数
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          // 参数为空的情况
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          // 如果有相应的类型处理器,参数为本身
          value = parameterObject;
        } else {
          // 创建一个 MetaObject
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        // 将参数也附加到 CacheKey 的 hashcode 计算中
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // 如果配置文件中 environment 标签不为空
      // issue #176
      // 再加上当前环境的 id 即 <environment id="development">
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

不知道你们好不好奇这个 update 方法,不管了,我们继续跟进去看看他到底对这些个东西们做了什么

package org.apache.ibatis.cache;
public class CacheKey implements Cloneable, Serializable {	
  // 乘数,固定初始值质数37,不会变
  private static final int DEFAULT_MULTIPLIER = 37;

  // 当前hashCode值,初始值是质数17,
  private static final int DEFAULT_HASHCODE = 17;

  // 乘数,默认值为质数37,不会变
  private final int multiplier;
  // 当前hashCode值,默认值为质数17,
  private int hashcode;
  // 所有更新对象的初始hashCode的和
  private long checksum;
  // 更新的对象总数
  private int count;

  /*
    8/21/2017 - Sonar lint flags this as needing to be marked transient.  
    While true if content is not serializable, 
    this is not always true and thus should not be marked transient.
  */
  // 已更新的所有 obj 的列表
  private List<Object> updateList;

  public CacheKey() {
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLIER;
    this.count = 0;
    this.updateList = new ArrayList<>();
  }
  
  public void update(Object object) {
    // 先计算传进来的这个 obj 的基础 HashCode,如果为空的话则是 1
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
    // 记录更新个数
    count++;
    // 计算 hashCode 的总和
    checksum += baseHashCode;
    // 将基础 HashCode 跟更新个数相乘
    baseHashCode *= count;
    // 最终得到新的 hashcode 为 固定数字 37 * 最新 hashcode 再加上 计算后的参数对象的 hashcode
    hashcode = multiplier * hashcode + baseHashCode;
    // 将传进来的 obj 放到已更新列表中
    updateList.add(object);
  }
}

具体的代码在这里,深刻的思想我也并没有研究出来。他这样做的原理我也没思考出来。但是目的我猜一定是为了让 hashcode 尽量的不重复,以做到在 map 中尽量散列分布,避免 hash 冲突。
生成了 缓存键 后,我们终于来到了查询步骤,话不多说,我们来看看 query 方法做了什么!

使用缓存第三步:查询使用二级缓存!

我们来详细看下 query 方法到底做了什么

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    // 这里是看我们有没有定义 Cache 对象,也就是我们在 Mapper 文件中有没有定义 <cache/> 标签
    // 如果有标签,在读取 Mapper 文件时会创建 Cache 对象来存储这个 Mapper 文件中所有需要缓存的东西
    Cache cache = ms.getCache();
    if (cache != null) {
      // 如果标签属性上标注了 flushCache="true" ,这里会先清空缓存
	  flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        // 确定本条不是一个有 OutParams 的存储过程,否则抛出异常
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        // 这里 TransactionalCacheManager 维护了一个以 Cache 为键,TransactionalCache 为值的一个 Map
        // 内部方法是尝试从 cache 中拿值
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          // 这里的 delegate 代表的是根据ExecutorType创建的几大执行器,例如 SimpleExecutor。
          // 也就是说,他这里只不过是先根据是否开启二级缓存,尝试是否能从缓存中拿到数据,
          // 但是如果真的没拿出来的话,真正查询还是交由传入的执行器来执行
          // 也就是传说中的 装饰器模式
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // 这里是往 TransactionalCache 中赋值
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

一步一步来,我们先看获取缓存,也就是 tcm.getObject 方法。这里 tcm 代表的是 TransactionalCacheManager 对象,是 CachingExecutor 的一个成员变量,也就是说随着 CachingExecutor 实例的创建而创建,随 CachingExecutor 实例回收而回收。那它是干啥的呢,它其实内部维护了一个以 Cache 为键,TransactionalCache 为值的一个 Map。我们来看看这个类的具体实现和方法:

public class TransactionalCacheManager {

  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

  public void clear(Cache cache) {
    getTransactionalCache(cache).clear();
  }

  public Object getObject(Cache cache, CacheKey key) {
    // 这里看上去是先根据 Cache 拿出内部 TransactionalCache,然后再从 TransactionalCache 中拿值。
    // 但实际上 TransactionalCache 是一个装饰器类,它负责装饰了 cache ,最终还是从 cache 中拿的值
    return getTransactionalCache(cache).getObject(key);
  }

  public void putObject(Cache cache, CacheKey key, Object value) {
    // 这里看上去跟上面的 getObject 方法一样,但是这里却不是给 cache put 值,
    // 而是给 TransactionalCache 内部维护的一个 HashMap 类型的变量 entriesToAddOnCommit put值
    // 这么做是为了保证事务的隔离性,缓存同样要等事务提交后统一刷到公共 cache 中
    getTransactionalCache(cache).putObject(key, value);
  }

  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }

  public void rollback() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.rollback();
    }
  }

  private TransactionalCache getTransactionalCache(Cache cache) {
    // 这里的 computeIfAbsent 相当于如下代码:
    /*
      if(null == transactionalCaches.get(cache)){
        transactionalCaches.put(cache, new TransactionalCache(cache));
      }
      或
      transactionalCaches.computeIfAbsent(cache, k -> new TransactionalCache(k));
     */
    return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
  }

}

我们看回到 getObject 方法,这里调用了 getTransactionalCache 方法从内部维护的 HashMap 中拿到了一个 TransactionalCache 实例并调用它的 get 方法。这里的 computeIfAbsent 方法是 1.8 中针对 HaspMap 的方法,具体示意我写在注释里了,大家感兴趣的话可以自行查询~
这一步需要注意的是,在 get 不到值的时候 new 出来的 TransactionalCache 实际上是一个包装类,进一步包装了 cache。
我们来看下 TransactionalCache 的构造方法和 get 方法你就懂了:

public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);

  private final Cache delegate;
  private boolean clearOnCommit;
  private final Map<Object, Object> entriesToAddOnCommit;
  private final Set<Object> entriesMissedInCache;

  public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<>();
    this.entriesMissedInCache = new HashSet<>();
  }
  
  @Override
  public Object getObject(Object key) {
    // issue #116
    // 注意这里拿是在 delegate 中拿的而不是 entriesToAddOnCommit 中
    Object object = delegate.getObject(key);
    if (object == null) {
      // 记录未命中缓存的 CacheKey,后面 commit 的时候会放置一个 null 值进主缓存
      entriesMissedInCache.add(key);
    }
    // issue #146: https://github.com/mybatis/mybatis-3/issues/146
    // 这里是防止 事务提交后清除缓存 这个动作已经执行了,但是缓存中还是能拿到东西。
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }
}

也就是这里的 get 实际上是从 delegate 即 传入的 cache 中拿的。这里如果没拿到,会记录一个 未命中 CacheKey,这个操作后面 commit 的时候我们详说。总之,这里第一次进来肯定是查不到的,也就是这会返回一个 null。返回到我们的 query 的代码,这里他判断如果拿出来的 list 为空,则调用被包装类的 query 方法,即 SimpleExecutor 的 query 方法,即 BaseExecutor 的query 方法。这里就涉及到了一级缓存使用的过程。

使用缓存第四步:查询使用一级缓存!

我们来看下这个方法做了些什么。

  @SuppressWarnings("unchecked")
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // 判断有没有刷新缓存的必要(属性 flushCache="true" )
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      // 这里判断是否指定 ResultHandler,如果没指定则尝试从缓存中拿,指定了则直接查数据库
      // 此处的缓存是一级缓存,因为 localCache 是每个 Executor 自己维护的。
      // 随着每次close,都会被清空。 新建的 Executor 也无法使用上次的。
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        // 如果从缓存中拿出数据,这里处理的是存储过程相关的 sql 和 参数
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      // 这里判断缓存范围如果是 STATEMENT 级别的话,清空本地缓存
      // 即 <setting name="localCacheScope" value="STATEMENT"/>
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

这个 localCache 就是我们一直说的 一级缓存 对象,看完这里大家一定很好奇,这里只见到了拿缓存的方法(localCache.getObject)但是没看到在哪放的呀。大家稍安勿躁,我们来看看这个 queryFromDatabase 方法:

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // localCache 内部维护了一个空的 HashMap ,这一步是先在localCache中放一个占位对象。
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      // 从数据库中查询
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      // 不管查询是否失败,先从map中删掉占位对象
      localCache.removeObject(key);
    }
    // 这里把 list 存到本地缓存中
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      // 当 statementType="CALLABLE"的时候,也就是调用存储过程的时候,设置 out 类参数
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

呐,看到了吧。查完后 localCache.putObject 方法就是放缓存的。这里为什么放置占位对象笔者也没太想懂,各位看官大佬有想法可以留言讨论哦。
我们再看回 query 方法,会发现这里有一步清除缓存的判断,这里的 localCacheScope 我觉得还是有必要拿出来说一下的,这是禁用一级缓存的必要手段。我们可以在 mybatis-config.xml 这个配置文件中,设置相应的 settings 来关闭一级缓存例如:

  <settings>
    <setting name="localCacheScope" value="STATEMENT"/>
  </settings>

官网给这个配置的解释是:

MyBatis uses local cache to prevent circular references and speed up repeated nested queries. By default (SESSION) all queries executed during a session are cached. If localCacheScope=STATEMENT local session will be used just for statement execution, no data will be shared between two different calls to the same SqlSession.

谷歌翻译:MyBatis使用本地缓存来防止循环引用并加快重复的嵌套查询。 默认情况下(会话),将缓存会话期间执行的所有查询。 如果 localCacheScope = STATEMENT 本地会话仅用于语句执行,则对同一SqlSession的两个不同调用之间不会共享数据。

欸,是不是奇怪的知识又增加了。话不多说我们接着看 query 查询完成后的事情吧:

使用缓存第五步:放置二级缓存!

查询完毕后,就调用了 tcm.putObject,好我知道大家肯定找不到了,这里我再放一边 put 方法的源码:

  public void putObject(Cache cache, CacheKey key, Object value) {
    // 这里看上去跟上面的 getObject 方法一样,但是这里却不是给 cache put 值,
    // 而是给 TransactionalCache 内部维护的一个 HashMap 类型的变量 entriesToAddOnCommit put值
    // 这么做是为了保证事务的隔离性,缓存同样要等事务提交后统一刷到公共 cache 中
    getTransactionalCache(cache).putObject(key, value);
  }
  
  private TransactionalCache getTransactionalCache(Cache cache) {
    // 这里的 computeIfAbsent 相当于如下代码:
    /*
      if(null == transactionalCaches.get(cache)){
        transactionalCaches.put(cache, new TransactionalCache(cache));
      }
      或
      transactionalCaches.computeIfAbsent(cache, k -> new TransactionalCache(k));
     */
    return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
  }

这里我们再进一步追入 putObject 方法来看看。

  @Override
  public void putObject(Object key, Object object) {
    // 这里的putObject 方法只是将 obj 放到了当前事务的缓存中即 entriesToAddOnCommit 中。
    // 所以事务不提交的话,在 delegate 中是拿不到的。用以保证事务缓存隔离
    entriesToAddOnCommit.put(key, object);
  }

这里可以看到,这仅仅是在 TransactionalCache 实例内部的一个 HashMap 中暂存了一下,而并没有调用 delegate 的 put 方法。这也就是说为什么两个事务在提交前都读不到互相的缓存。其实这里可以衍生出很多有趣的 demo,例如 关闭一级缓存后,即使在同一个开启了二级缓存 sqlsession 中查询两次,也需要查询两次数据库。具体更多有意思的 demo 可以留言一起交流~
这里 put 进了临时的 map 中,那么什么时候合并进主存中呢?是的,就是当事务提交时,当 CachingExecutor 执行 commit 时,会顺带调用 tcm 的提交方法:

  @Override
  public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();
  }

这里面就将当前事务的临时缓存存入了主缓存:

  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }

  // txCache.commit
  public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    // 当事务提交时,这里统一刷缓存
    flushPendingEntries();
    reset();
  }
  
  /**
   * 这个方法是将本次事务缓存中的所有缓存刷到 delegate 中
   * 做到了缓存的事务隔离
   */
  private void flushPendingEntries() {
    // 遍历 entry
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      // 如果未命中的 CacheKey 在 当前内部缓存中没有的话,则放置一个 null 进主缓存
      // 目的应该是防止缓存击穿(大量查询一个不存在的值)
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }

这里说到了我们之前放过的 entriesToAddOnCommit ,这里如果没命中缓存,且在提交的时候也没查出来,那么就会向主缓存中放一个 null 值占位。目的我猜测是防止缓存击穿。
那么这里有缓存,我们进行增删改的时候,会刷新缓存嘛?我们继续看

使用缓存第六步:更新时清除缓存!

我们分别写了三个语句,并用 insert | update | delete 三个方法执行:

    sqlSession1.insert("com.simon.demo.TestMapper.insertBookInfo");
    sqlSession1.update("com.simon.demo.TestMapper.updateBookInfo");
    sqlSession1.delete("com.simon.demo.TestMapper.deleteBookInfo");

有点源码基础的同学其实知道这里三个方法 共用了同一个 update 方法
方法共用
那么这个 update 方法内部对缓存又进行了什么操作呢?(注意这里选择实现类时,要选择 CachingExecutor )
选择 CachingExecutor

  @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    // 先根据需要看是否清除缓存
    flushCacheIfRequired(ms);
    // 在调用 被包装类的 update 方法
    return delegate.update(ms, parameterObject);
  }
  
  private void flushCacheIfRequired(MappedStatement ms) {
    // 获取当前缓存
    Cache cache = ms.getCache();
    // 除非配置,不然 insert | update | delete 三大标签的 flushCacheRequired 默认为 true
    // 这里可以看加载生成 Mapper 的默认赋值 -> 
    // org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode -> 
    // org.apache.ibatis.builder.MapperBuilderAssistant.addMappedStatement
    if (cache != null && ms.isFlushCacheRequired()) {
      // 调用缓存清除方法
      tcm.clear(cache);
    }
  }

这里有两个重点,一个是 isFlushCacheRequired 是在哪加载到的,实际上这就是在我们生成 MappedStatement 时加载进 ms 的:

  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) {

    if (unresolvedCacheRef) {
      throw new IncompleteElementException("Cache-ref not yet resolved");
    }

    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        // 这里定义了是否清除缓存区,默认值取决于是否是 select 类型的 sql
        // 如果是 select 的话,默认不清除缓存,不是 select 默认清除
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        // 这里定义了是否使用缓存,默认值也取决于是否是 select 类型的 sql
        // 如果是 select 的话,默认开启缓存
        .useCache(valueOrDefault(useCache, isSelect))
        // 这里将前面创造好的 Cache 对象绑定进 mappedStatement 对象
        // 这里将已有的缓存绑定入 MappedStatement 对象
        // 也就是说不管是什么类型的语句(包括 insert update delete)都有绑定缓存对象
        .cache(currentCache);

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
      statementBuilder.parameterMap(statementParameterMap);
    }
    // 做必要参数的非空校验
    MappedStatement statement = statementBuilder.build();
    // 在上下文中加入处理好的MappedStatement,以 id 为 key,实例为 value
    configuration.addMappedStatement(statement);
    return statement;
  }

第二个重点就是 tcm 的清理方法,即 tcm.clear 方法:

  // TransactionalCacheManager
  public void clear(Cache cache) {
    getTransactionalCache(cache).clear();
  }

这里实际上调用的是 map 中所存的 TransactionalCache 实例的 clear 方法:

  @Override
  public void clear() {
    // 提交时清除的 标志位
    clearOnCommit = true;
    // 当前内部缓存清除
    entriesToAddOnCommit.clear();
  }

大家有没有发现一个事情,这里执行完,实际上并没有清掉主缓存,而是只是清掉了当前事务的临时缓存。大家还记得我们的提交方法嘛?

  // txCache.commit
  public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    // 当事务提交时,这里统一刷缓存
    flushPendingEntries();
    reset();
  }

看到没,这里只有在提交(commit)的时候,才会去清主存。这么做也是防止不同事务之间的脏读。这里也可延伸出很多好玩的 demo,比如 sqlSession1 先 select 然后 commit 然后 insert ,sqlsession2 执行相同查询时不查数据库,而是返回 sqlSession1 第一次查询的值。
说到这里,我们的缓存好强大啊,那我们的缓存是完美的嘛?当然不是,我们接着来看:

使用缓存第七步:明白优缺点!

我们使用缓存当然要明白他的优势和缺点在哪里:

  • 优点:优点自然不用多说,我们可以减少查询数据库的次数,降低打开、关闭数据库连接的性能消耗。提高查询速度,缩短查询时间。
  • 缺点:其实最大的缺点在于很容易发生数据的不一致性,为什么这么说呢。我们知道,每个缓存是基于 Mapper 的,缓存的清空也是基于当前 Mapper 的 insert | update | delete 等更新操作。那么我们分两点来看:
    • 第一点是网上普遍说的针对一个表中的所有操作必须放到一个 Mapper 中,比如现在有 Mapper A 和 Mapper B,A 中有针对表 T 的读 sql,B 中则是对表的写 sql,那么这就会导致 A 中修改数据未刷新 B 的缓存,那么读到的数据就是有问题的。针对这个问题实际上是有解法的,我们大可使用 cache-ref 标签解决。在前篇 《Mybatis-3 源码之缓存是怎么创建的》 文中介绍了 cache-ref 标签。可以让两个 Mapper 使用同一个 Cache ,这样就解决了不刷新的问题
    • 第二个问题是第一个问题的加深版。因为我发现,分布式是无法解决上述问题的。针对两台机器上部署相同的微服务,假如 A 机器读,B机器写且提交,A再去读的话,就有可能会读到二级缓存的东西而导致数据出错。所以才会采用 Redis 之类的缓存手动做缓存失效和刷新。

整个缓存的流程到这里就基本结束了,其实其中还略过了很多东西,例如 缓存回收策略类的包装 是如何构建的,缓存是如何回收的 ,缓存失效策略具体是如何实现的等。我会在接下来的博客中一一解答这些问题,请大家期待~

后记

本来这篇文章到 第五步就截止了, @我GTR就不服AE86 大佬跟我提出,要不你把 mybatis 二级缓存的缺点也加上吧,这才延伸除了第六步和第七步的讨论。在这对于大佬的意见不胜感激~
其实严格意义上来说,我这篇文章分的步数步骤并不是非常的严谨,仅仅是根据代码顺序一步步走过来,具体还有不对的地方请各路大神不吝赐教,小弟谢过先~

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

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

Hope can help~

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值