Mybatis 缓存(一)

Mybatis 缓存

用过Mybatis的人或者看过关于Mybatis的面试题的人。肯定了解过Mybatis的二级缓存,关于一级缓存和二级缓存起什么作用?怎么开启。这些东西在网上一找一大篇。随便找一篇看看就好了。这里着重从源码角度来看缓存的具体实现?并且思考伴随着缓存就一直存在的问题。比如

  1. 缓存是否会失效?缓存啥时候失效?啥时候将数据放进缓存?
  2. 缓存有没有有效期?缓存使用的算法?
  3. 缓存中的数据怎么保持和数据库数据的一致性?

所以,从下面开始,就分析分析Mybatis 缓存的问题

1. 简介

简单的介绍一下Mybatis的一二级缓存,介绍怎么用不是本文的重点,可以自行在网上找一个篇博客看看。这里旨在从源码分析,简单的介绍一下,Mybatis有两级缓存,一级缓存是开启,并且无法关闭,作用于一个SqlSession,二级缓存的出现是为了解决跨Session问题的。因为一级缓存的存在可能出现脏读

比如:

 @Test
  public void testShouldSelectClassUsingMapperClass(){
    try(
      SqlSession session1 = sqlMapper.openSession();
      SqlSession session2 = sqlMapper.openSession();
      ){
      ClassMapper mapper1 = session1.getMapper(ClassMapper.class);
      ClassMapper mapper2 = session2.getMapper(ClassMapper.class);


      long start = System.currentTimeMillis();
      System.out.println("session1开始查询");
      List<ClassDo> classDos = mapper1.listClassOwnerStudentByClassId(1, new String[]{"狗蛋"});
      System.out.println(classDos);


      System.out.println("session2开始更新");
      System.out.println(mapper2.updateClassNameById(1, "计科一班1"));


      System.out.println("session1开始查询");
      List<ClassDo> classDos1 = mapper1.listClassOwnerStudentByClassId(1, new String[]{"狗蛋"});
      System.out.println(classDos1);


      System.out.println("start:" + (System.currentTimeMillis()-start));

      session1.commit();
      session2.commit();
    }
  }

结果:

在这里插入图片描述

上面的代码,开了两个Session,没有利用二级缓存,只是一级缓存,可以看到,起初是session1查了数据库,名字为计科一班,之后session2更新数据库,名字为计科一班1,之后session1再次查询,获取到的还是session计科一班,出现了数据一致性的问题,多个session之前的缓存不能共享,所以,二级缓存就出现了

二级缓存的范围是namespace

好了,简介就先到这里把。看代码吧

2. 一级缓存

如果看过我之前的文章的话Mybatis中获取获取代理对象的之后的执行操作,对整个的流程有一个大体的了解,那么肯定看过这样的代码

// BaseExecutor的query方法,
 @Override
  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.");
    }
    //如果Mapper中配置了flushCacheRequired为true的话,就会清空本地缓存。每次查询都会清空
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      // lcnote 从一级缓存中查找,如果没有就会从数据库里面查找,记住这个localCache
      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) {//lcnote 这个deferredLoads是干嘛的,
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      
      
      // 下面的这个会判断localScope的作用域,如果为STATEMENT,就会清楚缓存。
      //有一个小问题?清楚缓存,这肯定是线程不安全的。有问题,如果两个线程同时操作,
      //不需要清楚缓存 一个需要清楚缓存,那不就得么的了。出现问题了?
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        //
        clearLocalCache();
      }
    }
    return list;
  }



// BaseExecutor的queryFromDatabase方法,
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list; 
    
   // 先上来放一个占位符号。key是上面构建的cacheKey,关于CacheKey的构建在上一篇中已经说了。
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      //做查询,
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      //这里的remove我不是很理解,与其说是占位,name在这里直接put覆盖就好了,这里这样做怕是为了防止doQuery发生异常,导致这个缓存是错误的,所以, 这里必须有finally,这都是细节细节。
      localCache.removeObject(key); 
    }
 
    localCache.putObject(key, list);//放在一级缓存中。


    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

这里的逻辑很简单,和大多的缓存使用一样,先从缓存中查找,缓存中有,就不去数据库查询,如果没有就从数据库查询,之后填充。

问题?

  1. 防止占位符有什么意义(EXECUTION_PLACEHOLDER)?

    说实话,这里的占位符我觉得没有什么意义,在之后的查看里面也没有这样的判断,会不会存在这样的一种情况?

    同样的方法,同一个SqlSession,如果查询查询在两个线程里面,先后有一点点的差距,就正好后面的线程拿到了这个占位符,在后序的代码里面好像也没有看到相关的处理。所以,后面的这个线程就拿到占位符了。

  2. 为什么要在finally里面要移除之前缓存的key?

    因为前面已经放置了占位符了,如果在try里面报错了,那么就可能导致后面的代码不会走下去,必须用finally来保证肯定能移除一个。

  3. 会不会有线程安全问题?

    会。

    先从数据结构开始。PerpetualCache是缓存的具体实现,实际的数据结构是HashMap。HashMap本就不是线程安全的。

    在从缓存的操作开始,缓存的清楚和更新都没有任何的同步机制。

不过话说回来,在使用SqlSession的时候,人家已经告知这不是一个线程安全的。!

一级缓存的具体实现类

PerpetualCache实现了Cache接口。这接口就是Mybatis中缓存的具体表现。具体的实现就是HashMap。

public class PerpetualCache implements Cache {

  private final String id;

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

  public PerpetualCache(String id) {
    this.id = id;
  }

  @Override
  public String getId() {
    return id;
  }

  @Override
  public int getSize() {
    return cache.size();
  }

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

  @Override
  public Object removeObject(Object key) {
    return cache.remove(key);
  }

  @Override
  public void clear() {
    cache.clear();
  }

  @Override
  public boolean equals(Object o) {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    if (this == o) {
      return true;
    }
    if (!(o instanceof Cache)) {
      return false;
    }

    Cache otherCache = (Cache) o;
    return getId().equals(otherCache.getId());
  }

  @Override
  public int hashCode() {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    return getId().hashCode();
  }
}

一级缓存什么时候失效?

只要引起数据不一致的场景都会让缓存失效,那就是 insert,delete,update这三种操作。问题是,清楚缓存的是全部清楚还是针对性的清除?

带着疑问看看源码。

之前文章中介绍过MapperMethod,这个方法会通过Type来判断这次查询是什么类型?我在这里就不具体分析了,对于INSERT,UPDATE,DELETE方法最总会走到Update方法里面去。

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result; //美其名曰,策略模式
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName()
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

最终就会走到下面的这个方法

  // CachingExecutor类的 update方法,delegate是 BaseExecutor
 @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    //是否要刷新二级缓存 MapperStatement是有Cache,并且flushCacheRequired为true
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
  }


 // BaseExecutor的update方法
  @Override
  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);
  }
 
  @Override
  public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }

问题

  1. 缓存什么时候清楚?

    在insert,delete,update 和SqlSession#Commit的时候会清楚掉。包括一级缓存和二级缓存。

    清楚二级缓存的要求是 MapperStatement有Cache,并且flushCacheRequired为true。就会清楚二级缓存。一级缓存每次都清楚。

  2. 缓存是全部清楚还是针对性的清楚?

    全部清楚。要注意,这个会将一级缓存全部清楚掉

  3. 这样的缓存有问题吗?

    有,我觉得有问题。清楚会将整个LocalCache全部清楚,那在极端情况下,缓存就会失效。如果一个查询,一个更新,一个查询,一个删除,一个查询,一个更新,这种密集的情况下。缓存就没有了。

这就意味着,只要sqlSession没有关闭并且没有commit,也没有更新,删除,插入的话,只有查找。那一级缓存里面的数据就不会清楚掉。

3. 二级缓存

二级缓存是在NameSpace级别的。二级缓存开启有两个条件

开启二级缓存的两个条件

  1. setting标签配置,这个就算不配,默认也是也是true

        <setting name="cacheEnabled" value="true"/>
    // 在解析Xml标签的时候,如果不配置,默认就是true
        configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
    
  2. 在Mapper的Xml中写

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

只有在NameSpace下面配置了缓存的,并且全局的开启了,才能启动二级缓存

cacheEnabled为true会有什么操作

如果设置为true,在openSession的时候,会将创建好的ExecutorCachingExecutor包装。

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

    //如果开启了缓存就用CachingExecutor来包装之前搞好的Executor,
    // CachingExecutor里面有一个TransactionalCacheManager,这是mybatis实现二级缓存的重点。
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    //在interceptorChain应用Executor。
    //waring 这里也很重要,这是是实现mybatis拦截器的主要实现。主要看这个方法,Interceptor类里面的wrap方法
    // 其实这里就是生成了代理对象,调用 Plugin.wrap(target, this);来生成代理对象,在这里会解析interceptor的上面的注解,组成map,key是接口的class。
    // value是set,里面存放的需要拦截的方法。如果需要生成代理对象就生成代理对象,InvocationHandler是Plugin。 具体的还得看看方法里面是怎么写的。
    executor = (Executor) interceptorChain.pluginAll(executor);

    return executor;
  }

默认的的执行器的类型是ExecutorType.SIMPLE,上面的代码已经很明确了,先是根据执行器的类型来创建执行器。如果cacheEnabled为true,就会用CachingExecutor来包装。这用到了代理的设计模式。这是静态代理还是动态代理?

问题?

  1. 用到了什么设计模式

    代理的设计模式

  2. 静态代理还是动态代理

    静态代理。

CachingExecutor长什么样子

public class CachingExecutor implements Executor {

  private final Executor delegate;

  // 二级缓存,一级缓存在 BaseExecutor中的localCache
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

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

 
  @Override
  public void close(boolean forceRollback) {
    try {
      // issues #499, #524 and #573
      if (forceRollback) {
        tcm.rollback();
      } else {
        tcm.commit();
      }
    } finally {
      delegate.close(forceRollback);
    }
  }

  @Override
  public boolean isClosed() {
    return delegate.isClosed();
  }

  @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
  }

  @Override
  public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
    flushCacheIfRequired(ms);
    return delegate.queryCursor(ms, parameter, rowBounds);
  }

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);//waring 这里很重要,会解析sql,首先会调用所有的sqlNode节点,然后会替换掉,把之前的参数变为?。然后生成映射关系。对于Collection会生成一个特殊的参数。
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);//waring  这里构建cacheKey的目的是什么?这里会判断参数,更新cacheKey。并且每一个update,都会计算hashcode方法
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);//二级缓存实在sqlSessionFactory中的。默认是不开启的,这里肯定是将二级缓存放在Configuration中。
  }

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {

    // 这个缓存是在mapper.xml中配置的。在解析xml文件的时候配置的。
    Cache cache = ms.getCache(); // lcnote 这里的cache是干嘛的?

    if (cache != null) {
      flushCacheIfRequired(ms);
      // MapperStatement如果要配置缓存,并且resultHandler为null。就去查二级缓存。
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);//lcnote  这个没看懂

        //
        @SuppressWarnings("unchecked")
        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); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

  private void flushCacheIfRequired(MappedStatement ms) {
    // ms中配置了cache,并且Mapper标签配置的flushCacheRequired是true。表示是需要清楚缓存
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {
      tcm.clear(cache);
    }
  }
  
  //中间还省略了一大段。
}

可以看到CachingExecutor实现了Executor接口,也就意味着,他拥有Executor的所有的功能,可以在一些特定的接口上应用自己的处理逻辑,之后将处理下放到被代理对象。交给被代理对象来处理。

这就是二级缓存实现地方,在CachingExecutorTransactionalCacheManager对象就是二级缓存实现的重点。

NameSpace中Cache是怎么解析的?对应的实体类是什么?会和MapperStatement关联吗?

一切的开始还得从解析XML文件开始。这里只是展示这其中几个步骤。如果不清楚解析操作的,可以看看Mybatis中mapper是怎么和XMl关联起来的

//XMLMapperBuilder#configurationElement
// 解析 mapper 标签。这每一个都是一个标签属性,这里要关注cacheElement方法,看他是怎么解析cache标签的。
  private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.isEmpty()) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));//解析各个标签元素
      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"));//waring 这里很重要,真正的开始解析select|insert|update|delete标签
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }
cacheElement,解析cache标签
  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);
    }
  }

这里就能得到一些信息

信息

  1. 默认的缓存的类型是PERPETUAL
  2. 默认的淘汰算法是LRU
  3. 默认是readOnly和blocking是False
  4. 默认的flushInterval和size是Null

问题?

  1. 注意到这些值都是别名,那么这些别名是在什么时候注册进去的?

    // 在创建Configuration的时候
    
      public Configuration() {
        //省略一部分
        typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
        typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
        typeAliasRegistry.registerAlias("LRU", LruCache.class);
        typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
        typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
        //省略一部分
      }
    
    
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props)干了什么事情?
// 这里的代码看名字就大致知道是干嘛的?创建出新的Cache。 
public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
  // cache的id是当前的nameSpace(就是mapper接口上的nameSpace)
    Cache cache = new CacheBuilder(currentNamespace)
       // 属性,默认的原始的缓存的实现就是PerpetualCache,在上面的判断之外,在多一次判断
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
         // 增加装饰器,再次证明,默认的是LruCache
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        // 下面的都是设置属性了
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        // 这个就是在Cache下面设置的属性。
        .properties(props)
        .build();
   // 将构建好的Cache放在MapperStatement中去。作为一个属性放在里面
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
  }
TransactionalCacheManager 中是什么?TransactionalCache是什么

从下面的代码中可以看到,TransactionalCacheManager里面维护了一个HashMap,HashMap的的key是Cache,Value是TransactionalCache,并且TransactionalCache也是实现了一个Cache的接口。同时,他也是一个装饰的Cache,将真正的操作交给delete来完成。

/**
 * @author Clinton Begin
 */
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();
    }
  }

  private TransactionalCache getTransactionalCache(Cache cache) {
    return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
  }

}

 //TransactionalCache
  
/**<p>二级缓存的buffer,在调用commit或者rollback的时候会将这个缓存添加进去。</p>
 * The 2nd level cache transactional buffer.
 * <p>
 * This class holds all cache entries that are to be added to the 2nd level cache during a Session.
 * Entries are sent to the cache when commit is called or discarded if the Session is rolled back.
 * Blocking cache support has been added. Therefore any get() that returns a cache miss
 * will be followed by a put() so any lock associated with the key can be released.
 *
 * @author Clinton Begin
 * @author Eduardo Macarron
 */
public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);
  //代理
  private final Cache delegate;
  // 标志位置
  private boolean clearOnCommit;
  //存放的是put时候的值,在commit之后,才会完全的缓存起来,,具体的可以看commit方法
  private final Map<Object, Object> entriesToAddOnCommit;
  // 存放的是缓存中没有值的key。在commit的时候,判断,如果在entriesToAddOnCommit里面没有找到这个key就放置一个null。具体的可以看commit方法
  private final Set<Object> entriesMissedInCache;

  public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<>();
    this.entriesMissedInCache = new HashSet<>();
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    return delegate.getSize();
  }

  @Override
  public Object getObject(Object key) {
    // issue #116
    Object object = delegate.getObject(key);
    if (object == null) {
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

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

  @Override
  public Object removeObject(Object key) {
    return null;
  }

  @Override
  public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
  }
  //这里的逻辑也是很简答, 放值,清空
  public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
  }
  // 在rollback的时候会将entriesMissedInCache里面的值从缓存里面移除。并且重置entriesToAddOnCommit和entriesMissedInCache
  public void rollback() {
    unlockMissedEntries();
    reset();
  }

  private void reset() {
    clearOnCommit = false;
    entriesToAddOnCommit.clear();
    entriesMissedInCache.clear();
  }

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

  private void unlockMissedEntries() {
    for (Object entry : entriesMissedInCache) {
      try {
        delegate.removeObject(entry);
      } catch (Exception e) {
        log.warn("Unexpected exception while notifying a rollback to the cache adapter. "
            + "Consider upgrading your cache adapter to the latest version. Cause: " + e);
      }
    }
  }

}

重中之重来了 CacheBuilder的build方法干了什么事情?
// CacheBuilder的build方法
public Cache build() {
   //还是设置属性,再次判断一下,如果没有设置并且没有指定的话,默认的实现就是PerpetualCache,并且装饰就是LruCache。
    setDefaultImplementations();
  // 通过cache的构造方法创建实例
    Cache cache = newBaseCacheInstance(implementation, id);
  
  // 设置属性
    setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
  
     // 如果cache不是PerpetualCache,那就不用装饰器了
    if (PerpetualCache.class.equals(cache.getClass())) {
      for (Class<? extends Cache> decorator : decorators) {
        cache = newCacheDecoratorInstance(decorator, cache);
        setCacheProperties(cache);
      }
       //设置标准的几个装饰器
      cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
      cache = new LoggingCache(cache);
    }
    return cache;
  }


 // 设置标准的装饰器
  private Cache setStandardDecorators(Cache cache) {
    try {
      
      //MetaObject这个方法底层就是通过反射拿到所有的方法和字段。
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      //如果有size,就设置size字段
      if (size != null && metaCache.hasSetter("size")) {
        metaCache.setValue("size", size);
      }
      //如果设置了clearInterval,就将上一步的cache用ScheduledCache包装一下
      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);
    }
  }

所以,到这里就很明确了。setting中配置了cacheEnabled为true,就会创建CachingExecutor在之前的执行器上面进行代理。CachingExecutor就是二级缓存实现的关键。在查询的时候,会先从二级缓存中查找,在从一级缓存中查找,在从数据库查找。

如果mapper的xml中的NameSpace中配置了cache标签,就会在CachingExecutor的TransactionalCacheManager中保存。在commit的时候才会将缓存真正的添加进来,并且清楚entriesToAddOnCommitentriesMissedInCache和重置标记位。在rollback的时候,会从真正的缓存里面将entriesToAddOnCommit里面的移除掉。并且reset。

问题?

  1. 真正的存储元素的缓存是什么?上面看到的有的是包装类,有的是原始类。这种怎么分别?

    这就说到Cache了,Cache接口要求实现这个接口的类都应该有一个String类型的构造参数,用于接受NameSpace。

    在这里插入图片描述

在看看实现类。

在这里插入图片描述

听这缓存的名字就应该知道个大概,perpetualCache(永久缓存,持久缓存),缓存的实现百分之九十九都是Map结构,这肯定也不例外,他用的是HashMap。

在这里插入图片描述

  1. cache标签里面配置的属性具体是什么意思?有哪些作用?

    type: 缓存的具体实现类,(这指的是真正的缓存的实现,有一个构造方法为String的实体类)

    eviction:(采用的包装类的类型)

    flushInterval:(刷新间隔)

    size:(大小)

    readOnly:(是否只读)

    blocking:(是否阻塞)

二级缓存的整个的 查询流程

调用query方法,做查询。先获取MapperStatement中的Cache。判断当前Cache是否要刷新(cache不是null并且flushCacheRequired为true)

  private void flushCacheIfRequired(MappedStatement ms) {
    // ms中配置了cache,并且Mapper标签配置的flushCacheRequired是true。表示是需要清楚缓存
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {
      tcm.clear(cache);
    }
  }

就会通过tcm来清楚这个缓存对应的TransactionalCache,在会从二级缓存中获取值

        List<E> list = (List<E>) tcm.getObject(cache, key);

//下面的是TransactionalCacheManager的方法
 public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }
// getTransactionalCache,
// 从transactionalCaches中获取这个Cache对应的TransactionalCache,如果没有就创建一个。
  private TransactionalCache getTransactionalCache(Cache cache) {
    return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
  }

如果没有,就会交给delegate来执行查询。查询结束之后,将值放在二级缓存中,在commit或者当前的SqlSession关闭的时候将数据真正的缓存下来,虽然下面列举的代码是CachingExecutor,在关闭SqlSession的时候会调用过来。


//CachingExecutor
@Override
  public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();
  }

  @Override
  public void close(boolean forceRollback) {
    try {
      // issues #499, #524 and #573
      if (forceRollback) {
        tcm.rollback();
      } else {
        tcm.commit();
      }
    } finally {
      delegate.close(forceRollback);
    }
  }


关于缓存的具体实现,在下一篇中再说吧

关于Mybatis 缓存的就分析到这里了。 如有不正确的地方,欢迎指出。谢谢。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值