mybatis缓存

一级缓存

一级缓存是同一session内缓存,随着session的关闭而被清除。

先看下效果

        String resource = "mybatis-config.xml";
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream(resource));
        SqlSession sqlSession = sessionFactory.openSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user1 = mapper.findUserByNo("001");
        System.out.println(user1);
        User user2 = mapper.findUserByNo("001");
        System.out.println(user2);
        System.out.println(user1.equals(user2));

执行两次相同的mapper.findUserByNo方法,观察日志只向数据库发送一次查询请求。并且user1.equals(user2)是完全相同的两个对象。证明缓存命中,第二次查询读的缓存。

命中条件

必须同一会话session这个就不用多说了

必须是相同的mapper方法,相同的参数

mapper.findUserByNo("001");
mapper.findUserByNo("002");

这样不同的参数是不会命中缓存

中间没有执行过更新(update,insert,delete)操作

mapper.findUserByNo("001");
mapper.updateUser("002");
mapper.findUserByNo("001");

更新操作会清空缓存

session.clearCache()会清空缓存

源码实现

****CacheExecutor主要用来对Executor进行包装完成缓存的处理

cacheKey的创建

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  //创建cacheKey
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

创建cacheKey方法。CacheKey有一个update方法,加入新参数会重新构造计算该类的hashcode方法。

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  CacheKey cacheKey = new CacheKey();
  //语句id。就是mapper方法的全路径 包名+类名+方法名
  cacheKey.update(ms.getId());
  //分页条件
  cacheKey.update(rowBounds.getOffset());
  cacheKey.update(rowBounds.getLimit());
  //sql语句
  cacheKey.update(boundSql.getSql());
  //sql参数
  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因子
      cacheKey.update(value);
    }
  }
  if (configuration.getEnvironment() != null) {
    // 最后将运行环境值加
    cacheKey.update(configuration.getEnvironment().getId());
  }
  return cacheKey;
}

从key的构造可以看出调用必须是同一个mapper方法并且参数值相等。

缓存的存入和获取

在BaseExecutor.query方法中可以看到使用的PerpetualCache来存储缓存,其内部也是维护了一个map用来存储缓存数据。

BaseExecutor中操作缓存方法

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  ...
  List<E> list;
  try {
    queryStack++;
    //判断是否指定resultHandler,否则从本地缓存根据cacheKey获取数据
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if (list != null) {//缓存命中
      handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {//否则从数据库查询
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
  } finally {
    queryStack--;
  }
  ...
  return list;
}

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {//具体执行数据库查询
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    //将查询结果存放到cache中
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }
  
  public void clearLocalCache() {
    if (!closed) {//清空本地缓存
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }

在query方法中会先根据cacheKey进行缓存查找,如果找不到在使用queryFromDatabase方法进行数据库查询,数据库查询完后会将结果加入缓存。另外在Executor的update方法(update、insert、delete最后都会交给update方法),session的clearCache方法最后都会调用clearLocalCache来清空缓存。所以上面说缓存失效的几种场景就很好理解了。

生命周期

来看下几个对象的关系

public class DefaultSqlSession implements SqlSession {
  // 这里会是一个CacheExecutor实例
  private final Executor executor {
  	  /**
  	   *CacheExecutor的delegate是一个SimpleExecutor,SimpleExecutor继承自BaseExecutor
  	   */
      Executor delegate {
        //缓存对象
        PerpetualCache localCache;
      }
  };
}  

从上面的对象关系可以看出,缓存是session对象一个属性。会随着session的关闭二消失。

使用场景

有什么用呢?

二级缓存

开启使用

上面说的一级缓存是在同一session会话中,很有局限性,当session关闭时候缓存就消失了。二级缓存是session间共享的。

在mapper里添加配置开启二级缓存

<mapper namespace="com.test.mapper.UserMapper" >
   <cache ></cache>
</mapper>

        String resource = "mybatis-config.xml";
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream(resource));
        //开启第一个session
        SqlSession sqlSession = sessionFactory.openSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user1 = mapper.findUserByNo("001");
        System.out.println(user1);
        sqlSession.close();//关闭第一个session
        //开启第二个session
        sqlSession = sessionFactory.openSession();
        mapper = sqlSession.getMapper(UserMapper.class);
        User user2 = mapper.findUserByNo("001");
        System.out.println(user2);
        System.out.println(user1.equals(user2));

上面的程序执行开启两个session,执行相同的mapper方法,参数也一致。然后会发现只会向数据库发出一次查询请求,第二次走的缓存。

一个查询首先会从二级缓存查找,然后在从一级缓存查找,最后走数据库查询。

缓存的存储

具体代码看CacheExecutor.query方法主要逻辑

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
   //从statment获取缓存,这里就是二级缓存
  Cache cache = ms.getCache();
  if (cache != null) {
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      //从缓存中获取
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {//未命中,走查询
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); // 将查询结果放入缓存
      }
      return list;
    }
  }
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

这里有两个主要的变量tcm和cache。 cache是从MappedStatement获取的,而statement又是configuration初始化的时候创建的,因此是和mybatis同生命周期的,全局的。 这里cache实例的结构是这样的
image

cache是层层delegate包装。最后存储结构还是map。这些Cache类都实现了Cache接口。Cache三个主要接口方法就是putObject、getObject和removeObject。

SynchronizedCache是在缓存操作时候方法都加上了synchronized。

LoggingCache在缓存命中记录命中率。

SerializedCache对缓存数据对象都进行序列化和反序列化操作。这时候你就要知道为什么mapper返回的对象都要实现序列化接口了。

LruCache实现LRU(least recently used)算法。缓存过大时候移除策略。

PerpetualCache这个就是是包装了下map。

tcm变量是TransactionalCacheManager类实例。里面存有本地session所持有各statment的cache。

Map<Cache, TransactionalCache> transactionalCaches;

key是Cache类型,代表不同的二级缓存。value是TransactionalCache。这个也实现了Cache接口。当session提交或rollback时候,会将session内所持有的缓存(tcm中保存)依次进行提交到二级缓存或清空。

TransactionalCacheManager类的代码

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) {
    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();
    }
  }
  //根据二级缓存对象获取当前session中是否有本地缓存
  private TransactionalCache getTransactionalCache(Cache cache) {
    //如果map没有就new一个TransactionalCache,然后cache会传入作为其delegate
    return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
  }

}

TransactionalCache提交到二级缓存方法

private void flushPendingEntries() {
  for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
    //这里的delegate是具体的二级缓存,transactionalCaches对应的key
    delegate.putObject(entry.getKey(), entry.getValue());
  }
  for (Object entry : entriesMissedInCache) {
    if (!entriesToAddOnCommit.containsKey(entry)) {
      delegate.putObject(entry, null);
    }
  }
}

这样二级缓存的存储和获取就都明白了。会过头来看下二级缓存是怎么初始化的呢。这个时候就要看下mybatis初始化过程,主要在解析mapper文件的时候

这里主要在XMLMapperBuilder类中进行处理。入口是parse方法,然后调用configurationElement方法,

private void configurationElement(XNode context) {
    String namespace = context.getStringAttribute("namespace");
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));
    //解析cache配置
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    sqlElement(context.evalNodes("/mapper/sql"));
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  
}
private void cacheElement(XNode context) {
    if (context != null) {//解析所有的cache节点配置内容
     //基础缓存类型,这里是PerpetualCache类
      String type = context.getStringAttribute("type", "PERPETUAL");
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      //下面是所有的cache配置
      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);
    }
  }
  public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();//将所有的配置属性设置好后,build创建实例
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
  }

再来看CacheBuilder的build过程

public Cache build() {
  setDefaultImplementations();
  //第一步是基础实现PerpetualCache
  Cache cache = newBaseCacheInstance(implementation, id);
  setCacheProperties(cache);
  // issue #352, do not apply decorators to custom caches
  if (PerpetualCache.class.equals(cache.getClass())) {
    //这里所有的装饰cache,默认配置会有一个LruCache
    for (Class<? extends Cache> decorator : decorators) {
      cache = newCacheDecoratorInstance(decorator, cache);
      setCacheProperties(cache);
    }
    //下面对cache进行一些标准装饰
    cache = setStandardDecorators(cache);
  } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
    cache = new LoggingCache(cache);
  }
  return cache;
}
private Cache setStandardDecorators(Cache cache) {
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      if (size != null && metaCache.hasSetter("size")) {
        metaCache.setValue("size", size);
      }
      if (clearInterval != null) {
        cache = new ScheduledCache(cache);
        ((ScheduledCache) cache).setClearInterval(clearInterval);
      }
      if (readWrite) {//cache配置readWrite为true,使用SerializedCache进行装饰
        cache = new SerializedCache(cache);
      }
      //添加log装饰
      cache = new LoggingCache(cache);
      //添加同步装饰
      cache = new SynchronizedCache(cache);
      if (blocking) {
        cache = new BlockingCache(cache);
      }
      return cache;
  }

看到这里就会明白为什么cache会包装这么多层了。我们看到LruCache和SerializedCache是可以通过配置去掉的。其它的几个装饰好像都是标准不能去掉的。

<cache
  eviction="LRU"
  flushInterval="60000"
  size="512"
  readOnly="false"/>

eviction配置对应缓存清理策略,默认是LRU会用到LruCache

readOnly对应是否是只读,默认false。可读写就会用到SerializedCache。

size缓存大小,默认1024。超过就会调用LruCache清理

flushInterval刷新间隔,默认是不设置,如果设置就会启用ScheduledCache。

缓存的清除

默认情况下select不会清空缓存,update,insert和delete都会清空statment的缓存。这是默认配置。可以在mapper文件的statment语句通过flushCache属性配置。

回到CacheExecutor类,flushCacheIfRequired用来清空缓存。query和update方法都会调用该方法。

 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);
      ...
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
  flushCacheIfRequired(ms);
  return delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
  Cache cache = ms.getCache();
  //根据flushCache配置是否清空缓存
  if (cache != null && ms.isFlushCacheRequired()) {
    tcm.clear(cache);
  }
}

这里tcm的key是共享的二级缓存Cache实例引用,在session内清了,也相当于整体清理了。引用也有一定好处。一处改处处改。这也是为什么要使用SynchronizedCache包装的原因吧。

使用场景

一些公共配置,字典,菜单,权限机构等等使用二级缓存可以提高效率。

总结

一级缓存是在同一个session可见。对在同一个session内多次相同的查询生效。二级缓存是session间共享的。缓存首先会从二级缓存查找,然后是一级缓存。由缓存CacheKey的构建可以知道,必须是调用同一个mapper的相同方法并且实例参数一致才被判断为相同查询。二级缓存的存储是以MappedStatement为单位的,也就是一个select标签方法。同一个session可能同时持有多个二级缓存,二级缓存的更新是在session提交或close的时候将本地缓存更新到二级缓存。

posted @ 2023-08-11 16:38  朋羽  阅读(11)  评论(0编辑  收藏  举报