MyBatis缓存机制详解

MyBatis缓存机制详解

  • MyBatis的缓存机制介绍
    • 一级缓存
          • 一级缓存调用流程
          • 一级缓存原理解释
          • 一级缓存使用注意事项
    • 二级缓存

MyBatis的缓存机制介绍

   Mybatis包含一个非常强大的的查询缓存机制,可以减少系统直接和数据库数据的IO操作,大幅度提高查询效率。同时MyBatis的缓存即使可以很方便的进行配置和定制。MyBatis默认定义了两级缓存

一级缓存

     一级缓存是在没有配置默认情况下,MyBatis开启的缓存,是相对于同一个SqlSession而言的。在所有查询条件都一样情况下,MyBatis第一次会直接和数据库交互,但是第二次查询则直接从一级缓存中拿数据。下面是对一级缓存的一个示意图:当Mapper调用Executor的时候,首先检查一级缓存的作用域,其默认值是SESSION,这种情况表示缓存一个会话中执行的所有查询。也可以设置成STATEMENT,此时,本地会话会用在语句执行上,对相同的SqlSession也不会再次共享数据。
MyBatis缓存机制详解_第1张图片

一级缓存调用流程

     一级缓存本质的是一个缓存数据的容器,那么对数据究竟怎么做缓存的呢?同时为什么一级缓存是基于SqlSession级别的缓存,这里需要深入到MyBatis做Select查询的时候,究竟在和数据库交互之前做了什么。我们略过之前的代码,直接进入MyBatis的执行流程去看,从下面执行栈中我们可以看到,随着执行流程的一步一步推到,发现当MyBatis委托DefaultSqlSessio执行selectList的时候,使用的执行器是CachingExecutor。我们知道SqlSession执行查询的真正操作是Executor进行操作的。所以我们直接进入CachingExecutor去查看,究竟发生了什么。
MyBatis缓存机制详解_第2张图片

一级缓存原理解释

这里是SqlSession将查询委托给Executor的方法,这里的Executor接口的实现类就是CacheExecutor

DefaultSqlSession.java
 @Override
  public  List selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

下面我们进入CachingExecutor.class查看执行流程,可以看到在进行query()方发查询之前,先创建了CacheKey,这个CacheKey就是一级缓存的容器类,里面使用ArrayList进行存储数据,而创建CacheKey的工作,是委托给MyBatisSimpleExetutor进行的,创建代码如下:整个创建过程,都用到了cackeKey.update()方法,这个方法就是创建一个唯一键。方便下一次方法执行的时候,调用的相同的方法时候,使用缓存执行器。这里的key以五个规则来定:ms.getId()、rowBounds.getOffset()、rowBounds.getLimit()、boundSql.getSql()、参数value

  @Override
  public  List 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);
  }
  //创建执行器的过程
   @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 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中能查到数据,那么就获取数据,要不就委托给CacheExecutor继续执行。

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

这里的tmc.getObject(cache,key) 中 tmc是TransactionalCacheManager 既缓存事务管理器,它的作用是对TransactionalCache进行管理,获取每个SqlSession的TransactionalCache,而TransactionalCache的作用是存储Cache,MyBatis使用的Cache有一下几种:可以看出,实现类还是非常丰富的,我们也可以自定义缓存实现类。
MyBatis缓存机制详解_第3张图片
这一步的执行非常重要,正真的从缓存中获取数据,如果获取不到,那么久直接和数据库交互。最重要的执行方法是(List)localCache.getObject(key),这里的localCache是一级缓存容器,使用HashMap来进行数据存储。
如果缓存中获取不到数据,那么就直接在数据库中获取。 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);,获取到之后,再执行localCache.putObject(key, list); 将数据存在缓存容器中。本质调用的是map.put()方法。
至此,我们可以看到,一级缓存的整个调用过程就完成了。最重要的执行还是以创建容器,查询数据,和数据库交互为主。这里要非常注重跟源代码,从代码中获取这个执行链进行分析。
MyBatis缓存机制详解_第4张图片

  @SuppressWarnings("unchecked")
  @Override
  public  List 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 list;
    try {
      queryStack++;
      list = resultHandler == null ? (List) 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;
  }
  
一级缓存使用注意事项
  • MyBatis在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。

  • 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用;

  • 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用;

  • SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用;

二级缓存

二级缓存是利用一级缓存的数据在SqlSession调用commit或者close时导入到二级缓存的,那么如果在利用一个SqlSession实现了更新等操作时便会刷新一级缓存从而导致在提交了事务或关闭时,一级缓存传入到二级缓存的数据是空的,如下图:当执行完executor时候,那么对数据会进行一个保存,此时如果二级缓存开着,同时SqlSession关闭或者将要提交,二级缓存就会生效。
MyBatis缓存机制详解_第5张图片
这里就比较关键了如果没有开启二级缓存那么执行器便是默认的SimpleExecutor在这里面保存的便是一级缓存,如果开启了二级缓存,便会对执行器进行包装(CachingExecutor中便保存还未提交的二级缓存数据),这里再把Executor的图细化便是这样:
MyBatis缓存机制详解_第6张图片

你可能感兴趣的:(Java)