MyBatis系列:缓存

272 阅读4分钟

MyBatis提供了一级缓存和二级缓存,其中一级缓存基于SqlSession实现,而二级缓存基于Mapper实现。

MyBatis一级缓存

概述

Mybatis一级缓存默认是开启的,而且不能关闭。至于一级缓存为什么不能关闭,MyBatis核心开发人员做出了解释:MyBatis的一些关键特性(例如通过和建立级联映射、避免循环引用(circular references)、加速重复嵌套查询等)都是基于MyBatis一级缓存实现的。

MyBatis提供了一个配置参数localCacheScope,用于控制一级缓存的级别,该参数的取值为SESSIONSTATEMENT

  • 当指定localCacheScope参数值为SESSION时,缓存对整个SqlSession有效,只有执行DML语句(更新语句)时,缓存才会被清除。
  • localCacheScope值为STATEMENT时,缓存仅对当前执行的语句有效,当语句执行完毕后,缓存就会被清空。

实现原理

SqlSession提供了面向用户的API,但是真正执行SQL操作的是Executor组件。Executor采用模板方法设计模式,BaseExecutor类用于处理一些通用的逻辑,其中一级缓存相关的逻辑就是在BaseExecutor类中完成的

BaseExecutor类中维护了两个PerpetualCache属性,代码如下:

public abstract class BaseExecutor implements Executor {
    // Mybatis一级缓存对象
    protected PerpetualCache localCache;
    // 存储过程输出参数缓存
    protected PerpetualCache localOutputParameterCache;

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

BaseExecutorquery()方法相关的执行逻辑,代码如下:

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 构建缓存cacheKey
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // 执行查询
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // 是否清空localCache,flushCache="true"
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      // 从缓存中换取查询的数据
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        // 缓存不存在从数据库中获取数据,然后回填到localCache;
        // 如果执行语句为存储过程,还需回填到localOutputParameterCache。
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      // 当localCacheScope = STATEMENT时,清空缓存。
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

MyBatis一级缓存的“坑”

在实际生产中务必将MyBatis的localCacheScope属性设置为STATEMENT,避免其他应用节点执行SQL更新语句后,本节点缓存得不到刷新而导致的数据一致性问题。

MyBatis二级缓存

如何使用MyBatis二级缓存?

MyBatis二级缓存的使用比较简单,只需要以下几步:

  1. 在MyBatis主配置文件中指定cacheEnabled属性值为true。

    <settings>
       ...
       <setting name="cacheEnabled" value="true"/>
    </settings>
    
  2. 在MyBatis Mapper配置文件中,配置缓存策略、缓存刷新频率、缓存的容量等属性。例如:

    <cache eviction="FIFO"
           flushInterval="60000"
           size="512"
           readOnly="true"/>
    
  3. 在配置Mapper时,通过useCache属性指定Mapper执行时是否使用缓存。另外,还可以通过flushCache属性指定Mapper执行后是否刷新缓存,例如:

    <select id="list"
             flushCache="false"
             useCache="true" resultType="User">
        select * from user
    </select>
    

通过上面的配置,MyBatis的二级缓存就可以生效了。执行查询操作时,查询结果会缓存到二级缓存中,执行更新操作后,二级缓存会被清空。

实现原理

MyBatis二级缓存是通过CachingExecutor实现的。Configuration类提供了一个工厂方法newExecutor(),该方法返回一个Executor对象。代码如下:

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

如果cacheEnabled属性值为true(开启了二级缓存),则使用CachingExecutor对普通的Executor对象进行装饰,CachingExecutor在普通Executor的基础上增加了二级缓存功能。

CachingExecutor类中维护了一个TransactionalCacheManager实例,TransactionalCacheManager用于管理所有的二级缓存对象。CachingExecutor代码如下:

20210313143111yJz4o7

最后,回顾一下MappedStatement对象创建过程中二级缓存实例的创建。XMLMapperBuilder在解析Mapper配置时会调用cacheElement()方法解析标签,cacheElement()方法代码如下:

20210313143221DTRkue

在调用MapperBuilderAssistant对象的addMappedStatement()方法创建MappedStatement对象时会将当前命名空间对应的二级缓存对象的引用添加到MappedStatement对象中。

20210313143253GQjc9v

MyBatis使用Redis缓存

MyBatis官方提供了一个mybatis-redis模块,该模块用于整合Redis作为二级缓存。使用步骤如下:

  1. 首先需要引入该模块的依赖;

  2. 然后需要在Mapper的XML配置文件中添加缓存配置。如下所示:

    <cache type  = "org.mybatis.caches.redis.redisCache" />
    
  3. 最后,需要在classpath下新增redis.properties文件,配置Redis的连接信息。

参考资料

  1. 美团技术团队-聊聊MyBatis缓存机制
  2. MyBatis 3源码深度解析