Mybatis源码 - 缓存

SqlSession 使用门面模式,实际完成数据查询、更新操作的是Executor, 而Executor 分两大类,一类带cache和一类不带cache。

【DefaultSqlSession】

  @Override
  public int update(String statement, Object parameter) {
    //.....
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 实际调用executor.update 方法
      return executor.update(ms, wrapCollection(parameter));
 	//....
  }

  @Override
  public void commit(boolean force) {
     // 实际调用executor.commit 方法
      executor.commit(isCommitOrRollbackRequired(force));
      //....
  }

在这里插入图片描述

一缓存

一级缓存是SqlSession级别的缓存

<settings>
 	 <!--配置默认的执行器, 默认SIMPLE, 可选项SIMPLE、REUSE、BATCH-->
	<setting name = "defaultExecutorType" value = "SIMPLE" />
</settings>

如果没有配置defaultExecutorType默认 就是SimpleExecutor, 继承自BaseExecutor, 采用了模板方法设计模式。

public abstract class BaseExecutor implements Executor {
  protected Transaction transaction;
  protected Executor wrapper;
  protected PerpetualCache localCache;
}

BaseExecutor 存在一个属性PerpetualCache localCache, 这个就是本地缓存,即一级缓存。PerpetualCache 很简单, 就是一个内部封装了Map<Object, Object> cache 实现了Cache接口的缓存类。

public class PerpetualCache implements Cache {
  private final String id;
  private final Map<Object, Object> cache = new HashMap<>();
}

执行查询时,先从缓存中获取,存在则返回,不存在则查询数据库并将查询结果放在缓存中。
【BaseExecutor】

  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
      //.....
      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);
      }
  	//...
    return list;
  }
  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
   //.....
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    //...
  }

而且从上面的代码就可以看出一缓存是和Executor实例绑定的,而Executor是和SqlSession 实例绑定的。
【DefaultSqlSessionFactory.java】


  public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
      //.....
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);

	  // 创建Executor实例
      final Executor executor = configuration.newExecutor(tx, execType);
      // 创建SQLSession实例
      return new DefaultSqlSession(configuration, executor, autoCommit);
   	  //........
  }

在创建SqlSession时,就会同步创建一个Executor, 所以每一个SQLSession都会拥有一个独有的Executor, Executor上存在一级缓存的引用,SQLSession拥有Executor实例,这也是为什么,一级缓存是 SqlSession级别 的缓存

在这里插入图片描述

Mybatis是怎么保证正确获取缓存结果的呢。 Mybatis将影响查询结果的数据都封装到CacheKey中,同时重写了 equals方法。即PerpetualCache内部的Map,key类型是CacheKey

 public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    // Mapper的namespace + 元素id 
    cacheKey.update(ms.getId());
    // 伪分页,通常不会使用RowBounds分页
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    // 已解析的SQL语句,
    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);
      }
    }
    // 环境ID
    if (configuration.getEnvironment() != null) {
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

最终生成的结果, 前两个字段是hashcode和checksum。后续的字段便是id, start,end,sql, 参数,环境id

912292398:333997506:org.mybatis.mapper.UserMapper.test1:0:2147483647:SELECT * from test where id = ?:1:development

当参数SQL改变,参数改变那么就无法获取缓存数据。

二级缓存也是使用相同的规则,创建CacheKey

二级缓存

对于Mybatis版本: 3.5.9,默认是开启二级缓存的。早期版本可能默认是关闭的,可以手动开启

<settings>
 	 <!--全局性地开启或关闭所有映射器配置文件中已配置的任何缓存-->
	<setting name = "cacheEnabled" value = "true" />
</settings>

上面只是开启了全局的配置,真正要生效还需要配置自定义的缓存策略 只有在 mapper.xml 文件中设置了cache标签,才正真实现了二级缓存。

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

可用的清除策略有:

  • LRU – 最近最少使用:移除最长时间不被使用的对象。
  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
  • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
  • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

当开启缓存并配置缓存后,查询操作的执行流程 二级缓存 -> 一级缓存 -> 数据库。

上面介绍过,SQLSession中包含一个Executor。它内部会根据defaultExecutorType 创建不同的类型。如果配置了cacheEnabled=true,则会创建一个CachingExecutor。

【DefaultSqlSessionFactory】

  final Executor executor = configuration.newExecutor(tx, execType);

【Configuration】

  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) {
    //如果开启了二级缓存,而创建CachingExecutor 实例
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

当配置了cacheEnabled=true时,创建的Executor实际类型为CachingExecutor,它使用了装饰器模式。它内部维护了一个其他的Executor实现,通常是SimpleExecutor

public class CachingExecutor implements Executor {
  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();
}

select标签有两个属性配置缓存功能:

  • useCache:将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。仅select标签有效。
  • flushCache: 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。其他更新标签(update、delete、insert)默认是true。
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    
    // 获取在Mapper文件中配置的缓存  
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        // 从缓存中获取数据
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          // 如果找不到则委派给 SimpleExecutor(具体实现需要看配置)
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    // 如果没有配置二级缓存则 委派给 SimpleExecutor(具体实现需要看配置)
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
二级缓存的cache

那么问题来了MappedStatement中的cache 是什么,为什么可以保证不同SQLSession之间的数据共享。

如果是基于XML文件的方式, 那么在解析mapper.xml 文件的时候,会解析里面的cache元素, 创建一个Cache实例, 这个Cache实例被这个Mapper下的所有的查询操作共享的。

每一个Mapper.xml 都会有一个自己的MapperBuilderAssistant。经过下面的调用链,会对每个Mapper.xml(命名空间)生成独有的cache

在这里插入图片描述

【XMLMapperBuilder】

  private void cacheElement(XNode context) {
    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);
    }
  }

【MapperBuilderAssistant】

  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();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
  }

使用CacheBuilder构造Cache实例,内部调用了setStandardDecorators,此处就是二级缓存的最关键的代码。

 private Cache setStandardDecorators(Cache cache) {
    try {
      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 = new SerializedCache(cache);
      }
      cache = new LoggingCache(cache);
      cache = new SynchronizedCache(cache);
      if (blocking) {
        cache = new BlockingCache(cache);
      }
      return cache;
    } catch (Exception e) {
      throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
  }

经常上述操作,最终创建SynchronizedCache(实现了Cache接口) 实例, 显然二级缓存使用装饰器模式。可以看到上面有一个 SerializedCache, 所以POJO必须是可序列化的(实现Serializable接口)

Mapper.xml文件中的每一个SQL元素(select、update、delete、insert)都会解析为一个MappedStatement,同一个Mapper.xml文件下解析出的MappedStatement 里面都包含一个指向同一个cache。 所以二级缓存是Mapper(命名空间)级别的
在这里插入图片描述

Mybatis 缓存顶级类Cache,提供了获取和设置缓存结果的方法。

public interface Cache {
  void putObject(Object key, Object value);
  Object getObject(Object key);

这样调用putObject方法,就能从SynchronizedCache一步步的调用,最终保存到Perpetual的Map中。



上面是MappedStatement中存放的cache。实际上, CachingExecutor还会再包装一层,这就是CachingExecutor另一个属性的意义。它用于管理TransactionalCache 。

private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public class TransactionalCacheManager {
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
}

public class TransactionalCache implements Cache {
  private final Cache delegate;
  private boolean clearOnCommit;
  private final Map<Object, Object> entriesToAddOnCommit;
  private final Set<Object> entriesMissedInCache;
}

字面意义就是事务缓存, 所以对它的缓存结果仅有在事务提交后才可见。

【TransactionalCache】

  @Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

调用TransactionalCache的 putObject方法并没有将数据 委派给内部的Cache delegate 存储。仅当提交时才会将数据最终保存。

  public void commit() {
    flushPendingEntries();
    reset();
  }

  private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }
缓存失效
一级缓存失效

1、一级缓存是SQLSession级别的, 所以必须是同一个SqlSession,才有可能生效
2、一级缓存 flushCache 必须为false
3、两次查询之间存在更新操作
4、手动调用了sqlSession.clearCache(), 会导致缓存失效
5、cacheKey 不相同, 即参数、sql、environment等

二级缓存失效

1、没有开启二级缓存
2、没有提交 (sqlSession.commit())。 SqlSession 在未提交的时候,SQL 语句产生的查询结果还没有放入二级缓存中。
3、存在更新操作
4、存在多表关联,此时产生脏数据

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值