1. 书接上回

大家好,我是方圆,上一篇帖子从根上了解Mybatis的一级、二级缓存(一)写了一级缓存,这篇写二级缓存,彻底搞理解就得了!

2. 准备工作

  • 上一篇帖子中User和Department实体类仍然要用,这里就不再赘述了
  • 要启用二级缓存,需要在xml文件中指定cache标签,UserMapper.xml和DepartmentMapper.xml中咱们要用到的东西如下
UserMapper.xml
    <select id="findAll" resultType="User">
        select * from user
    </select>
    <cache />
Department.xml
    <select id="findAll" resultType="entity.Department">
        select * from department;
    </select>
    <cache readOnly="true"/>
  • 这里能够看见Department.xml中的cathe标签指定了readOnly特点,咱们就这个引子把这个说一下,还挺有意思的

2.1 cathe标签中readOnly特点

  • readOnly默以为false,这种情况下经过二级缓存查询出来的数据会进行一次Serializable的序列化深复制,在这里大家需要回想一下介绍一级缓存时举的比如:一级缓存查询出来回来的是该目标的引证,咱们对它修改之后,再查询时触发一级缓存取得的便是被修改过的数据。而二级缓存的序列化机制则不同,它获取到的是缓存深复制的目标,之后咱们对目标的操作不会影响二级缓存。

  • 为什么会有这种机制? 由于二级缓存是能够跨SQLSession的,咱们不能保证其他SQLSession不对二级缓存进行修改,所以这也是一种维护机制

  • 如果更改为true的话,那么它就会变得和一级缓存一样,回来的是目标的引证,这样做的好处是避免了深复制的开支,可是缺陷也如咱们上文中所述

  • ok,咱们测验一下这个比如,Department和User的查询都履行了两遍(注意业务提交之后才干使二级缓存收效)

        InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        // 开启二级缓存需要在同一个SqlSessionFactory下,二级缓存存在于 SqlSessionFactory 生命周期,如此才干射中二级缓存
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(xml);
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        DepartmentMapper departmentMapper1 = sqlSession1.getMapper(DepartmentMapper.class);
        System.out.println("----------department第一次查询 ↓------------");
        List<Department> departments1 = departmentMapper1.findAll();
        System.out.println("----------user第一次查询 ↓------------");
        List<User> users1 = userMapper1.findAll();
        // 提交业务,使二级缓存收效
        sqlSession1.commit();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
        DepartmentMapper departmentMapper2 = sqlSession2.getMapper(DepartmentMapper.class);
        System.out.println("----------department第2次查询 ↓------------");
        List<Department> departments2 = departmentMapper2.findAll();
        System.out.println("----------user第2次查询 ↓------------");
        List<User> users2 = userMapper2.findAll();
        sqlSession1.close();
        sqlSession2.close();
  • Department和User的同一条查询句子都履行了两遍,由于Department咱们制定了readOnly为true,那么两次查询回来的目标一致,而User则反之,Debug试一下
    从根上理解Mybatis的一级、二级缓存(完)

2.2 了解下cache的其他特点

特点 描绘 补白
eviction 缓存回收战略 默许LRU
type 二级缓存的完成类 默许完成PerpetualCache
size 缓存引证数量 默许1024
flushInterval 守时铲除时刻距离 默许无
blocking 堵塞获取缓存数据 若缓存中找不到对应的 key ,是否会一直堵塞,直到有对应的数据进入缓存。默许 false

3. 二级缓存的原理

  • 在加载Mapper文件的时分,有专门对cache标签的加载步骤,咱们进入XMLMapperBuillder中configurationElement办法,看如下两句中心代码
      cacheRefElement(context.evalNode("cache-ref"));
      // 加载二级缓存 咱们要点看这一句
      cacheElement(context.evalNode("cache"));

3.1 cacheElement办法

  • 源码如下,结合注释一同看
  // 能够发现下边的加载办法都是对咱们在第二节中cache标签特点的加载
  private void cacheElement(XNode context) {
    if (context != null) {
      // 二级缓存完成类,默许是PerpetualCache,咱们在一级缓存也提到过
      String type = context.getStringAttribute("type", "PERPETUAL");
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      // 缓存铲除战略,默许LRU
      String eviction = context.getStringAttribute("eviction", "LRU");
      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      // 守时铲除距离
      Long flushInterval = context.getLongAttribute("flushInterval");
      // 缓存引证数量
      Integer size = context.getIntAttribute("size");
      // readOnly上文咱们提到过,默许false
      boolean readWrite = !context.getBooleanAttribute("readOnly", false);
      // blocking 默许false
      boolean blocking = context.getBooleanAttribute("blocking", false);
      Properties props = context.getChildrenAsProperties();
      // 创立缓存目标,持续看这个办法
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }

3.2 builderAssistant.useNewCache办法

  • 哟,咱们发现,创立Cache目标运用的是制作者模式
  // 这办法的一坨参数都是cache标签的特点
  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;
  }
  • 不过咱们留心一下制作者的第三行代码,它增加了一个装修器,其他行的办法不过是简单的赋值操作,所以咱们看看addDecorator办法

3.2.1 addDecorator办法

  private final List<Class<? extends Cache>> decorators;
  public CacheBuilder addDecorator(Class<? extends Cache> decorator) {
    if (decorator != null) {
      this.decorators.add(decorator);
    }
    return this;
  }
  • 以上咱们能够发现在CacheBuilder中,有decorators字段专门存装修器,addDecorator办法则是向其间增加装修器。不知道大家还记不记得,缓存的父类Cache,它有许多完成类都在decorators包下,只有PerpetualCache在impl包下,咱们再看看
    从根上理解Mybatis的一级、二级缓存(完)
  • 当时咱们说一级缓存的时分把这里一笔带过了,这里又圆了回来。可是咱们现在需要回到方才制作者创立缓存目标的代码处,发现增加的装修器就一个LruCache呀,那其他装修器在哪儿用了呀
    从根上理解Mybatis的一级、二级缓存(完)
  • 慢慢来,咱们接着看

3.2.2 制作者的build办法

  • 直接看源码注释
  public Cache build() {
    // 这个办法没啥意思,便是在没指定缓存完成类的时分指定PerpetualCache.class
    // 没有装修器的时分指定LruCache.class装修器,略过略过
    setDefaultImplementations();
    // 默许创立PerpetualCache
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);
    // PerpetualCache会在这里被装修
    if (PerpetualCache.class.equals(cache.getClass())) {
      for (Class<? extends Cache> decorator : decorators) {
        // 这里装修的是LruCache
        cache = newCacheDecoratorInstance(decorator, cache);
        setCacheProperties(cache);
      }
      // 这里,它会呈现咱们上图中的大部分基础装修器,想看吗?
      cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
      cache = new LoggingCache(cache);
    }
    return cache;
  }
  • 想看吗?
    从根上理解Mybatis的一级、二级缓存(完)

3.2.3 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);
      }
      // readOnly特点相关的读写缓存
      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);
    }
  }
  • ok,到这里咱们就把二级缓存的创立说完了,咱们再去Debug一下,看看它收效的机制,直接进入CachingExecutorquery办法

3.3 CachingExecutor的query办法

  • 咱们看看它的履行逻辑

  private final TransactionalCacheManager tcm = new TransactionalCacheManager();
  @Override
  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.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);
  }
  • 上述逻辑仍是很明晰的,不过咱们再上文中提到过,只有业务提交的时分才会将二级缓存保存,那咱们是不是应该去看看putObject办法

3.3.1 putObject办法,想看的业务提交后保存

  • 它先走的是这个办法
  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }
  • 再深入putObject办法
  // 二级缓存终究被放在这个map里,注意字段名有OnCommit
  private final Map<Object, Object> entriesToAddOnCommit;
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }
  • OnCommit提示咱们,在业务提交之后二级缓存才会被增加,上文咱们测验二级缓存的时分特意写了一行sqlSession1.commit();代码,这便是为了让二级缓存收效,咱们看看commit办法的终究调用

3.3.2 终究调用到TransactionalCache的commit办法

  • 源码如下,逻辑比较简单,它在这里将之前咱们放入entriesToAddOnCommit的缓存真正存入二级缓存中
  private final Cache delegate;
  public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
  }
  // 这个办法会将entriesToAddOnCommit已有的二级缓存加入到Cache中
  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);
      }
    }
  }

3.4 它为什么要在业务提交后才干收效?

  • 由于二级缓存能够在不同的SQLSession间收效嘛,所以… 我画个图你就理解了
    从根上理解Mybatis的一级、二级缓存(完)
  • 看哈,如果SQLSession1先修改了数据,再查询数据,如果二级缓存此刻就收效的话,那么SQLSession2调用同样的查询从二级缓存中获取数据,可是SQLSession1回滚了业务,那么此刻就会导致SQLSession2从二级缓存获取的数据变成脏数据了,这便是为什么二级缓存要在业务提交后才干收效的原因

3.4.1 rollBack办法也要看一看

  • 这个办法很简单呐,业务回滚了把entriesToAddOnCommit清了便是了
  public void rollback() {
    unlockMissedEntries();
    reset();
  }
  private void reset() {
    clearOnCommit = false;
    entriesToAddOnCommit.clear();
    entriesMissedInCache.clear();
  }

4. Debug下试试

  • 测验代码如下
   SqlSession sqlSession1 = sqlSessionFactory.openSession();
   DepartmentMapper departmentMapper1 = sqlSession1.getMapper(DepartmentMapper.class);
   System.out.println("----------department第一次查询 ↓------------");
   List<Department> departments1 = departmentMapper1.findAll();
   // 使二级缓存收效
   sqlSession1.commit();
   SqlSession sqlSession2 = sqlSessionFactory.openSession();
   DepartmentMapper departmentMapper2 = sqlSession2.getMapper(DepartmentMapper.class);
   System.out.println("----------department第2次查询 ↓------------");
   List<Department> departments2 = departmentMapper2.findAll();
  • 第一次Query办法,会去数据库中查
    从根上理解Mybatis的一级、二级缓存(完)
  • 第2次Query,直接从二级缓存中拿
    从根上理解Mybatis的一级、二级缓存(完)

5. 尾声

做个总结吧

  • 二级缓存在不同SQLSession下共享
  • 二级缓存需要在业务提交后才干收效
  • 履行Insert、Delete、Update句子会使当时namespace下的二级缓存失效
  • 二级缓存本质上也是个HashMap
  • 特别的readOnly标签,默以为false,每次回来的二级缓存深复制的目标

收!