侧边栏壁纸
博主头像
敢敢雷博主等级

永言配命,自求多福

  • 累计撰写 57 篇文章
  • 累计创建 0 个标签
  • 累计收到 2 条评论

目 录CONTENT

文章目录

Mybatis原理---缓存

敢敢雷
2020-03-03 / 0 评论 / 0 点赞 / 847 阅读 / 3,107 字
温馨提示:
部分素材来自网络,若不小心影响到您的利益,请联系我删除。

Mybatis提供对缓存对支持

  • 一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就 将清空,默认打开一级缓存。
  • 二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源, 如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要 实现 Serializable 序列化接口(可用来保存对象的状态),可在它的映射文件中配置
  • 对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存 Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将 被 clear。

一级缓存

image.png
我们已经知道一级缓存是基于PerpetualCache的,现在来根据源码看一下PerpetualCache创建过程把

localCache创建

PerpetualCache创建时,是和Executor一起创建的,查看源码,创建sqlSession的代码

 private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

进入configuration.newExecutor()

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

默认创建的是SimpleExecutor,进入

public SimpleExecutor(Configuration configuration, Transaction transaction) {
    super(configuration, transaction);
  }

代码是直接使用父类的构造方法
image.png```
第一个localCache就是我们的一级缓存咯

localCache的使用

关于缓存的使用,肯定是在Executor方法使用时使用
image.png
进入该方法

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

根据之前的知识,BoundSql是我们将要执行的sql语句,关于CacheKey,就是我们的缓存的key,无论一级缓存还是二级缓存,都是基于一个HashMap存储的

缓存Key值创建

下面看看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());
    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;
  }

根据一系列的cacheKey.update方法的传参,我们不难判断,在缓存的key创建,

  1. 根据MappedStatement的id(ms.getID)
    image.png
  2. 查询时要求的结果集中的结果范围(rowBounds.getOffset,rowBounds.getLimit())
    image.png
  3. 查询所产生的最终要传递给JDBC的Sql语句字符串(boundSql.getSql())
    image.png
  4. 传递给Statement要设置的参数值(cacheKey.update(value))
    image.png
    经过上面的一系列update方法,所创建的缓存key如图所示
    image.png
    有兴趣的朋友可以看下update的具体方法,其实现主要就是维持一个名为updateList的ArrayList对象add一个object
    回到query方法,缓存的key获得到了,接下来就是根据这个key去寻找值对吧

缓存key的get

进入下一个query方法

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);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

关于第一个Cache对象,它是属于二级缓存的东西,我们暂时先不讲,接着进入下一个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 {
        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;
  }

来了list = resultHandler == null ? (List) localCache.getObject(key) : null;
这串代码是先判断resultHandler是不是为空,毋庸置疑,他就是为空的,判断为ture,调用一个方法localCache.getObject()
我们知道PerpetualCache里面有个cache对象,它是一个HashMap对象

public class PerpetualCache implements Cache {

  private final String id;

  private Map<Object, Object> cache = new HashMap<Object, Object>();
}

直接通过gatKey的方法获得了HashMap中的value,就这样,获得了缓存value,是不是很简单
接着看看这个Map是在哪里put值的

缓存的value值

继续进入queryFromDatabase方法

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);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

大家都知道hashMap的key是不能重复的,就是现在,查找到了list后,调用了一个localCache.putObject(key, list),就是这样一个简单的过程,put了一个以key为键的list为值的value

一级缓存清空

当Session flush或close之后,一级缓存将清空
close我们都知道,就是手动关闭,那必然会清空
其实对于某一个SqlSession对象而言,只要执行update操作(update、insert、delete),都会将这个SqlSession对象中对应的一级缓存清空掉
来看下源码

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

可以看见,在调用下一层的doUpdate方法前,进行了一次clearLocalCache(),点进去查看下源码

public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }

就这样,清空了一级缓存

一级缓存总结

  • MyBatis对会话(Session)级别的一级缓存设计的比较简单,就简单地使用了HashMap来维护
  • 只要执行update操作(update、insert、delete),都会将这个SqlSession对象中对应的一级缓存清空掉
  • MyBatis的一级缓存就是使用了简单的HashMap,MyBatis只负责将查询数据库的结果存储到缓存中去, 不会去判断缓存存放的时间是否过长、是否过期,因此也就没有对缓存的结果进行更新这一说了

二级缓存

默认情况下是只开启了局部的sqlSession缓存(一级缓存),打开二级缓存需要配置

  1. 在xml文件中
    <settings>
        <!--默认为true-->
        <setting name="cacheEnabled" value="true"/>
    </settings>
  1. 在sql映射文件中
<cache/>
  1. 实体类实现序列化接口

就这样,二级缓存就能使用了

关于cache

cache还有很多东西可以设置

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

创建了一个 FIFO 缓存,并每隔 60 秒刷新,存数结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此在不同线程中的调用者之间修改它们会导致冲突
eviction即为回收策略
还有其他回收策略,分别为:

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

默认为LRU
flushInterval(刷新间隔)可以被设置为任意的正整数,而且它们代表一个合理的毫秒 形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。

size(引用数目)可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。默认值是 1024。

readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存 会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 false。

二级缓存的实现

二级缓存和一级缓存一样,都是在调用Executor时开始使用,接下来看看二级缓存是怎么实现的
回到之前configuration.newExecutor方法

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的
executor又被重新new成了一个CachingExecutor类型,我们点进去

public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }

噢~根据装饰模式看,原来它是Executor的装饰类,所以,实际上在设置cacheEnabled为false前,我们一直使用的Executor的装饰类,现在都清楚了
那我们就进入CachingExecutor的query方法把

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);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

第二个关键字来了—Cache,他就是我们在sql映射文件配置的cache,它是一个接口,那不多说,查看它的实现类把
image.png
果然和我们之前配置的一样
所以,关于二级缓存的使用,一直是我们的装饰类CachingExecutor一直在暗箱操作

关闭二级缓存

关闭二级缓存有四种方式

  1. 主配置文件中设置cacheEnabled为false
    < setting name=“cacheEnabled” value=“false”/ >
  2. mapper配置文件中去掉< cache >
    < cache eviction=“FIFO” flushInterval=“60000” size=“512” readOnly=“true”/ >
  3. 将查询< select > 标签useCache属性设置为false
    < select useCache=“false” >
  4. 将查询< select > 标签flushCache属性设置为true
    < select flushCache=“true” >

二级缓存原理

二级缓存指的就是同一个namespace下的mapper,二级缓存中,也有一个map结构,这个区域就是一级缓存区域。
一级缓存中的key是由sql语句、条件、statement等信息组成一个唯一值。一级缓存中的value,就是查询出的结果对象。
一级缓存是默认使用的。
二级缓存需要手动开启。
image.png
在这里,顺便提一下,mybatis的二级缓存是属于序列化,序列化的意思就是从内存中的数据传到硬盘中,这个过程就是序列化
反序列化意思就是相反而已
也就是说,mybatis的二级缓存,实际上就是将数据放进了硬盘文件中去了

总结

简单点说

  • 映射语句文件中的所有select语句将会被缓存。
  • 映射语句文件中的所有insert,update和delete语句会刷新缓存。
  • 二级缓存是基于一级缓存的,一级缓存的数据是存储在一个HashMap中,二级缓存数据是通过序列化放进了硬盘文件
    image.png
0

评论区