跳到主要内容
  1. 所有文章/
  2. 《Mybatis3源码深度解析》笔记/

Mybatis源码-缓存实现

·📄 3339 字·🍵 7 分钟

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

MyBatis的缓存分为一级缓存和二级缓存,一级缓存默认是开启的,而且不能关闭。

缓存的使用 #

至于一级缓存为什么不能关闭,MyBatis核心开发人员做出了解释:MyBatis的一些关键特性(例如通过<association>和<collection>建立级联映射、避免循环引用(circularreferences)、加速重复嵌套查询等)都是基于MyBatis一级缓存实现的,而且MyBatis结果集映射相关代码重度依赖CacheKey,所以目前MyBatis不支持关闭一级缓存。MyBatis 提供了一个配置参数localCacheScope,用于控制一级缓存的级别,该参数的取值为SESSION、STATEMENT,当指定localCacheScope参数值为SESSION时,缓存对整个SqISession有效,只有执行DML语句(更新语句)时,缓存才会被清除。当localCacheScope值为STATEMENT时,缓存仅对当前执行的语句有效,当语句执行完毕后,缓存就会被清空。MyBatis的一级缓存,用户只能控制缓存的级别,并不能关闭。

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

  1. MyBatis主配置文件中指定cacheEnabled属性值为true。
    <configuration>
      <settings>
        <setting name="cacheEnabled" value="true"/>
      </settings>
    </configuration>
    
  2. 在MyBatisMapper配置文件中,配置缓存策略、缓存刷新频率、缓存的容量等属性,例如:
    <cache eviction="FIFO"
            flushInterval="60000"
            size="512"
            readOnly="true"/>
    
  3. 在配置Mapper时,通过useCache属性指定Mapper执行时是否使用缓存。另外,还可以通过flushCache属性指定Mapper执行后是否刷新缓存,例如:
    <select id="listAllUser"
                flushCache="false"
                useCache="true"
                resultType="com.blog4java.mybatis.example.entity.User" >
            select
            <include refid="userAllField"/>
            from user
        </select>
    

可以发现二级缓存是基于命名空间的,每个mapper都是一个不同的缓存key,执行DML之后缓存会清空,但是二级缓存是不推荐使用的,Mybatis二级缓存,你确定要用么?-腾讯云开发者社区-腾讯云 (tencent.com)

缓存实现类 #

MyBatis中的缓存类采用装饰器模式设计,Cache接口有一个基本的实现类,即PerpetualCache类,该类的实现比较简单,通过一个HashMap实例存放缓存对象。需要注意的是,PerpetualCache类重写了Object类的equals()方法,当两个缓存对象的 Id相同时,即认为缓存对象相同。另外,PerpetualCache类还重写了Object类的hashCode()方法,仅以缓存对象的Id作为因子生成hashCode。

我们可以使用MyBatis提供的缓存装饰器类对基础的PerpetualCache类的功能进行增强,使用不同的装饰器后,缓存对象则拥有对应的功能。

 public void testCache() {
    final int N = 100000;
    Cache cache = new PerpetualCache("default");
    cache = new LruCache(cache);
    cache = new FifoCache(cache);
    cache = new SoftCache(cache);
    cache = new WeakCache(cache);
    cache = new ScheduledCache(cache);
    cache = new SerializedCache(cache);
    cache = new SynchronizedCache(cache);
    cache = new TransactionalCache(cache);
    for (int i = 0; i < N; i++) {
        cache.putObject(i, i);
        ((TransactionalCache) cache).commit();
    }
    System.out.println(cache.getSize());
}

一级缓存实现原理 #

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

protected BaseExecutor(Configuration configuration, Transaction transaction) {
  // ...
  this.localCache = new PerpetualCache("LocalCache");
  this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
  // ...
}

// org.apache.ibatis.executor.BaseExecutor#createCacheKey可以看看缓存的key是怎么创建的。
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()); // Mapper Id
    cacheKey.update(rowBounds.getOffset()); // 偏移量
    cacheKey.update(rowBounds.getLimit()); // 条数
    cacheKey.update(boundSql.getSql()); // SQL语句
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // 所有参数值
    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);
      }
    }
    // Environment Id
    if (configuration.getEnvironment() != null) {
      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语句,缓存才会生效。接下来我们看一下BaseExecutor的query()方法相关的执行逻辑,代码如下:

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

BaseExecutor的update()方法

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的localCacheScope属性设置为STATEMENT,避免其他应用节点执行SQL更新语句后,本节点缓存得不到刷新而导致的数据一致性问题。

二级缓存实现原理 #

CachingExecutor负责缓存的实现,Configuration负责生成CachingExecutor。

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  Executor executor;
  // 根据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属性为ture,这使用CachingExecutor对上面创建的Executor进行装饰
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  // 执行拦截器链的拦截逻辑
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

CachingExecutor 是对其他执行器的增强。装饰器模式。

public class CachingExecutor implements Executor {

  private final Executor delegate;
  // 管理所有的二级缓存对象
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }
  
  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);
  }
  
  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);
        // 从MappedStatement对象对应的二级缓存中获取数据
        @SuppressWarnings("unchecked")
        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);
  }
  
   public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    // 如果需要刷新,则更新缓存
    // mapper的 flushcache属性 控制
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
  }
}

Mybatis使用Redis缓存 #

MyBatis除了提供内置的一级缓存和二级缓存外,还支持使用第三方缓存(例如Redis、Ehcache)作为二级缓存。本节我们就来了解一下在MyBatis中如何使用Redis作为二级缓存以及它的实现原理。MyBatis官方提供了一个mybatis-redis模块,该模块用于整合Redis作为二级缓存。使用该模块整合缓存,首先需要引入该模块的依赖,如果项目通过Maven构建,则只需要向pom.xml文件中添加如下内容:

最后,需要在classpath 下新增redis.properties 文件,配置 Redis 的连接信息。下面是redis.properties配置案例:

host=127.0.0.1
port=6379
password=admin
maxActive=100
maxIdle=20
whenExhaustedAction=WHEN_EXHAUSTED_GROW
maxWait=10
testonBorrow=true
testonReturn=true
timeBetweenEvictionRunsMillis=10000
numTestsPerEvictionRun=1000
minEvictableIdleTimeMillis=100
softMinEvictableIdleTimeMillis=-1

需要注意的是,使用Redis作为二级缓存,需要通过<cache>标签的type属性指定缓存实现类为org.mybatis.caches.redis.RedisCache。MyBatis启动时会解析Mapper配置信息,为每个命名空间创建对应的RedisCache实例,由于JedisPool实例是RedisCache类的静态属性,因此JedisPool实例是所有RedisCache对象共享的。

除了Redis外,MyBatis还提供了整合其他缓存的适配器。例如,ehcache-cache项目用于整合EhCache 缓存,oscache-cache项目用于整合OSCache缓存,memcached-cache项目用于整合Memcached缓存。