mybatis源码分析:阅读Mybatis缓存笔记

Scroll Down

1、概述

缓存是Mybatis中非常重要的特性。应用程序和数据库都是单节点的情况下,合理使用缓存能够减少数据库IO,显著提升性能。但是在分布式环境下,如果使用不当,则可能会带来数据一致性的问题。Mybatis提供了一级缓存和二级缓存,其中一级缓存基于SqlSession实现,而二级缓存基于Mapper实现。

2、Mybatis缓存的使用

Mybatis的缓存分为一级缓存和二级缓存,一级缓存默认是开启的,而且不能关闭。至于一级缓存为什么不能关闭,Mybatis核心开发人员做出了解释:Mybatis的一些关键特性(例如通过association和collection建立级联映射、避免循环引用,加速重复嵌套查询等)都是基于Mybatis一级缓存实现的,而且Mybatis结果集映射相关代码重度依赖CacheKey,所以目前Mybatis不支持关闭一级缓存。
Mybatis提供了一个配置参数localCacheScope,用于控制一级缓存的级别,该参数的取值为SESSION、STATEMENT,当指定localCacheScope参数值为SESSION时,缓存对整个SqlSession有效,只有执行DML语句(更新语句)时,缓存才会被清除。当localCacheScope值为STATEMENT时,缓存仅对当前执行的语句有效,当语句执行完毕后,缓存就会被清空。
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="getUserById"
resultType="com.matrix.dao.User"
resultMap="user"
flushCache="false"
useCache="true"
timeout="10000"
fetchSize="256"
statementType="PREPARED"
resultSetType="FORWARD_ONLY">
    SELECT <include refid="userAllColumn"/> FROM user WHERE id = #{id}
</select>

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

3、Mybatis缓存实现类

Mybatis缓存的实现原理,Mybatis的缓存基于JVM堆内存实现,即所有的缓存数据都存放在Java对象中。Mybatis通过Cache接口定义缓存对象的行为,Cache接口代码如下:

public interface Cache {

  /**
   * @return The identifier of this cache
   */
  String getId();

  /**
   * @param key Can be any object but usually it is a {@link CacheKey}
   * @param value The result of a select.
   */
  void putObject(Object key, Object value);

  /**
   * @param key The key
   * @return The object stored in the cache.
   */
  Object getObject(Object key);

  /**
   * As of 3.3.0 this method is only called during a rollback 
   * for any previous value that was missing in the cache.
   * This lets any blocking cache to release the lock that 
   * may have previously put on the key.
   * A blocking cache puts a lock when a value is null 
   * and releases it when the value is back again.
   * This way other threads will wait for the value to be 
   * available instead of hitting the database.
   *
   * 
   * @param key The key
   * @return Not used
   */
  Object removeObject(Object key);

  /**
   * Clears this cache instance
   */  
  void clear();

  /**
   * Optional. This method is not called by the core.
   * 
   * @return The number of elements stored in the cache (not its capacity).
   */
  int getSize();
  
  /** 
   * Optional. As of 3.2.6 this method is no longer called by the core.
   *  
   * Any locking needed by the cache must be provided internally by the cache provider.
   * 
   * @return A ReadWriteLock 
   */
  ReadWriteLock getReadWriteLock();
}

(1)、getId():该方法用于获取缓存的Id,通常情况下缓存的Id为Mapper的命名空间名称。
(2)、putObject():该方法用于将一个Java对象添加到缓存中,该方法有两个参数,第一个参数为缓存的key,即Cachekey的实例;第二个参数为需要缓存的对象。
(3)、getObject():该方法用于获取缓存Key对应的缓存对象。
(4)、removeObject():该方法用于讲一个对象从缓存中移除。
(5)、clear():该方法用于清空缓存。
(6)、getReadWritedlock():该方法返回一个ReadWriteLock对象。该方法已经被弃用。
Mybatis中的缓存类采用装饰器模式设计,Cache接口有一个基本的实现类,即PerpetualCache类,该类的实现比较简单,通过一个HashMap实例存放缓存对象。需要注意的是,PerpetualCache类重写了Object类的equals()方法,当两个缓存对象的ID相同时,即认为缓存对象相同。另外,PerpetualCache类还重写了Object类的hashCode()方法,仅以缓存对象相同的ID作为因子生成hashCode。
除了基础的PerpetualCache类之外,Mybatis中为了对PerpetualCache类的功能进行增强,提供了一些缓存的装饰类。
image.png
这些缓存装饰器类功能如下:
(1)、BlockingCache:阻塞版本的缓存装饰器,能够保证同一时间只有一个线程到缓存中查找指定的key对应的数据。
(2)、FifoCache:先入先出缓存装饰器,FifoCache内部有一个维护具有长度限制的Key键值链表(LinkedList实例)和一个被装饰的缓存对象,Key值链表主要是维护Key的FIFO顺序,而缓存存储和获取则交给被装饰的缓存对象来完成。
(3)、LoggingCache:为缓存增加日志输出功能,记录缓存的请求次数和命中次数,通过日志给出缓存命中率。
(4)、LruCache:最近最少使用的缓存装饰器,当缓存容量满了之后,使用LRU算法淘汰最近最少使用的Key和value,LruCache中通过重写LinkedHashMap类的removeEldestEntry()方法获取最近最少使用的Key值,将key值保存在LruCache类的eldestKey属性中,然后当缓存中添加对象时,淘汰eldestKey对应的value值。
(5)、ScheduledCache:自动刷新缓存装饰器,当操作缓存对象时,如果当前时间与上次清空缓存的时间间隔大于指定的时间间隔,则清空缓存,清空缓存的动作由getObject()、putObject()、removeObject()等方法触发。
(6)、SerializedCache:序列化缓存装饰器,向缓存中添加对象时,对添加的对象进行序列化处理,从缓存中取出对象时,进行反序列化处理。
(7)、SoftCache:软引用缓存装饰器,SoftCache内部维护了一个缓存对象的强引用队列和软引用队列,缓存以软引用的方式添加到缓存中,并将软引用添加到队列中,获取缓存对象时,如果对象已经被回收,则移除key,如果未被回收,则将对象添加到强引用队列中,避免被回收,如果强引用队列已经满了,则移除最早入队列的对象的引用。
(8)、SynchronizedCache:线程安全缓存装饰器,SynchronizedCache的实现比较简单,为了保证线程安全,对操作缓存的方法使用synchronized关键字修饰。
(9)、TransactionalCache:事务缓存装饰器,该缓存与其他缓存的不同之处在于,TransactionalCache增加了两个方法,即commit()和rollback(),当写入缓存时,只有调用commit()方法后,缓存对象才会真正添加到TransactionalCache对象中,如果调用了rollback()方法,写入操作将被回滚。
(10)、WeakCache:弱引用缓存装饰器,功能和SoftCache类似,只是使用不同的引用类型。

4、Mybatis一级缓存实现原理

Mybatis的一级缓存是SqlSession级别的缓存,在介绍Mybatis核心组件时,有提到过SqlSession提供了面向用户的API,但是真正执行SQL操作的是Executor组件。Executor采用模版方法设计模式,BaseExecutor类用于处理一些通用的逻辑,其中一级缓存相关的逻辑就是在BaseExecutor类中完成的。
一级缓存使用PerpetualCache实例实现,在BaseExecutor类中维护了两个PerpetualCache属性,代码如下:

// Mybatis一级缓存对象
protected PerpetualCache localCache;
// 存储过程输出参数缓存
protected PerpetualCache localOutputParameterCache;

其中,localCache属性用于缓存Mybatis查询结果,localOutputParameterCache属性用于缓存存储过程调用结果。这两个属性在BaseExecutor构造方法中进行初始化,代码如下:

this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");

Mybatis通过CacheKey对象来描述缓存的Key值。在进行查询操作时,首先创建CacheKey对象。如果两次查询操作CacheKey对象相同,就认为这两次查询执行的是相同的SQL语句。CacheKey对象通过BaseExecutor类的createCacheKey()方法创建。

@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
      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 = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
}

从上面的代码可以看出,缓存的key与下面这些因素有关:
(1)、Mapper的ID,即Mapper命名空间与select|update|insert|delete标签的ID组成的全局限定名。
(2)、查询结果的偏移量及查询的条数
(3)、具体的SQL语句及SQL语句中需要传递的所有参数。
(4)、Mybatis主配置文件中,通过environment标签配置的环境信息对应的ID属性值。
执行两次查询时,只有上面的信息完全相同时,才会认为这两次查询执行的是相同的SQL语句,缓存才会生效。

@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.");
    }
    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 {
        // 若缓存中获取不到,则调用queryFromDatabase()方法从数据库中查询
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
}

需要注意的是,如果localCacheStatement属性设置为STATEMENT,则每次查询操作完成后,都会调用clearLocalCache()方法清空缓存。除此之外,Mybatis会在执行完任意更新语句后清空缓存。

@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    clearLocalCache();
    return doUpdate(ms, parameter);
}

可以看到,Mybatis在调用doUpdate()方法完成更新操作之前,首先会调用clearLocalCache()方法清空缓存。

5、Mybatis二级缓存实现原理

Mybatis二级缓存默认情况下是关闭的,因此需要通过设置cacheEnabled参数值为true来开启二级缓存。
SqlSession将执行Mapper的逻辑委托给Executor组件完成,而Executor接口有几种不同的实现,分别为SimpleExecutor、BatchExecutor、ReuseExecutor。另外,还有一个比较特殊的CachingExecutor,CachingExecutor用到了装饰器模式,在其他几种Executor的基础上增加了二级缓存功能。
Executor实例采用工厂模式创建,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);
    }
    // 如果cacheEnabled属性为true,则使用CachingExecutor对Executor进行装饰。
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

CachingExecutor在普通的Executor的基础上增加了二级缓存的功能,CachingExecutor类的属性信息:

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

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

public class TransactionalCacheManager {
  // 通过HashMap对象维护二级缓存对应的TransactionalCache实例。
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

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

  public Object getObject(Cache cache, CacheKey key) {
    // 获取二级缓存对应的TransactionalCache对象,然后根据缓存Key获取缓存对象。
    return getTransactionalCache(cache).getObject(key);
  }
  
  public void putObject(Cache cache, CacheKey key, Object value) {
    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) {
    // 获取二级缓存对应的TransactionalCache对象
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
      // 如果获取不到,则创建,然后添加到Map中
      txCache = new TransactionalCache(cache);
      transactionalCaches.put(cache, txCache);
    }
    return txCache;
  }
}

二级缓存的工作机制:

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 调用createCacheKey()方法创建缓存Key
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

@Override
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")
        // 从MappedStatement对象对应的二级缓存中获取数据
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          // 如果缓存数据不存在,则从数据库中查询数据
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // 将数据存放到MappedStatement对象对应的二级缓存中
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

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

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

最后通过useNewCache方法来创建Cache。