1 Star 0 Fork 0

cxylk / Java-Notes

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
二级缓存.md 27.36 KB
一键复制 编辑 原始数据 按行查看 历史
cxylk 提交于 2021-04-22 14:40 . :rocket:mybatis插件体系

缓存体系

前面分析了一级缓存,现在来分析二级缓存。首先来看下Mybatis的缓存体系,复习下前面的内容:

二级缓存组件实现

定义:二级缓存也叫应用级缓存,与一级缓存不同的是它的作用范围是整个应用,并且可以跨线程使用。所以二级缓存有更高的命中率,适合缓存一些修改较少的数据。

设计:如果让我们自己去设计一个缓存应该需要考虑哪些因素?

1.存储:是将数据存储在内存还是硬盘,或者与第三方集成?比如redis。

2.淘汰策略,是FIFO(先进先出)还是LRU(最近最少使用)或者其他。。

3.怎么对过期数据进行清理?怎么保证线程安全?缓存命中率?序列化。。。

我们来看看mybatis是怎么设计的。

通过实现Cache接口,并且每个实现Cache接口的类的内部都委托了一个Cache,这样就可以在拥有Cache所有功能的同时又能添加自己的功能。然后处理完自己的功能后又交给下一个实现Cache接口的类继续添加功能。这就是装饰者+责任链的设计模式

下面通过代码来看看这个过程

    @Test
    public void cacheTest1(){
        //cacheId就是添加了@cacheNamespace注解的类
        Cache cache = configuration.getCache("cxylk.mybatis.UserMapper");
        User user = Mock.newUser();
        //设置缓存
        cache.putObject("lk",user);
        //从缓存中获取
        cache.getObject("lk");
    }

通过debug来看下此时cache的值

由于没有设置ScheduledCache和BlockingCache,所以上面的delegate中是没有出现这两个cache的。最终是由PerpetualCache来进行内存存储。

接下来进入对应的代码实现:

1.首先进入SynchronizedCache,这个类是用来进行同步的,因为二级缓存是跨线程使用的,所以要保证该操作是一个线程安全的

//加锁实现同步  
@Override
  public synchronized void putObject(Object key, Object object) {
    delegate.putObject(key, object);
  }

2.该方法加完锁后,将该操作交给下一个实现Cache接口的类LoggingCache

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

可以看到该方法什么都没干,但是它的getObject方法增加了自己的功能:

  @Override
  public Object getObject(Object key) {
    //请求数
    requests++;
    final Object value = delegate.getObject(key);
    if (value != null) {
      //统计命中率
      hits++;
    }
    if (log.isDebugEnabled()) {
      //输出debug日志
      log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
    }
    return value;
  }

其中getHitRatio用来计算命中率

  private double getHitRatio() {
    //命中数量/请求数量
    return (double) hits / (double) requests;
  }

可以看到该方法就是用来统计命中率,如果开启了debug日志的话会打印缓存命中日志。

3.接下来会进入SerializableCache中

  @Override
  public void putObject(Object key, Object object) {
    if (object == null || object instanceof Serializable) {
      delegate.putObject(key, serialize((Serializable) object));
    } else {
      throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);
    }
  }

在putObject中使用serialize((Serializable) object)来序列化当前的value,将其转换为字节数组

为什么要这样做呢?

假设现在有两个线程都拿到了同一个对象,其中一个线程修改了这个对象的属性,但是另外一个线程却不想拿到这个线程修改后的对象,这个时候怎么办呢?就是将当前对象序列化,然后在获取的时候反序列化,这样就算拿到的对象的值是一样的,但是它们的id是不一样的

获取时候的操作:

  @Override
  public Object getObject(Object key) {
    //反序列化
    Object object = delegate.getObject(key);
    return object == null ? null : deserialize((byte[]) object);
  }

4.接下去会进入LruCache的putObject方法中,此时的value已经是一个字节数组了

  @Override
  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);
    cycleKeyList(key);
  }

LurCache使用了一个LinkedHashMap来保证存放在其中的元素是有序的,这样就可以将最近最少使用的元素淘汰。这个后面在说

5.最后进入PerpetualCache的putObject方法中

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

这个cache就是一个hashMap,所以最后会将数据存放到内存中。

这里也是一级缓存的实现,但是它和一级缓存不同的是:一级缓存存的是一个完整的对象,而二级缓存存的是序列化后的值。原因很简单,一级缓存是不能跨线程使用的,所以不需要保证线程安全

接下来说说怎么对缓存进行扩展或者更改属性值:

首先有一个CacheNamespace注解声明了缓存空间,在这里面可以更改属性值

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CacheNamespace {
  //缓存的最终实现。默认是PerpetualCache
  Class<? extends org.apache.ibatis.cache.Cache> implementation() default PerpetualCache.class;

  //淘汰策略,默认是LRU
  Class<? extends org.apache.ibatis.cache.Cache> eviction() default LruCache.class;

  //缓存刷新间隔
  long flushInterval() default 0;
	
  //大小
  int size() default 1024;

  //序列化,true表示开启
  boolean readWrite() default true;

  boolean blocking() default false;

  /**
   * Property values for a implementation object.
   * @since 3.4.2
   */
  //属性值
  Property[] properties() default {};

}

缓存实现

现在我们将原来缓存的最终实现改为自定义的磁盘实现

@CacheNamespace(implementation = DiskCache.class,properties = {@Property(name = "cachePath",value = "D:\\workspace\\learn-mybatis\\target\\cache")})
public interface UserMapper {

DiskCahe是自定义的实现类,这里不再展示。通过这种方式,就将原来默认的perpetualCache改为了磁盘存储。

淘汰策略

将淘汰策略LRU改为FIFO

    /**
     * 溢出淘汰 FIFO
     * @CacheNamespace(eviction = FifoCache.class, size = 10)
     */
    @Test
    public void cacheTest3() {
        Cache cache = configuration.getCache("cxylk.mybatis.UserMapper");
        User user = Mock.newUser();
        for (int i = 0; i < 12; i++) {
            cache.putObject("lk:" + i, user);// 设置缓存
        }
        System.out.println(cache);
    }

默认大小是1024,我们设置为10,然后放进12个对象,这时候前2个就会被淘汰

FIFO源码实现

  private final Cache delegate;
  //队列
  private final Deque<Object> keyList;
  private int size;

  public FifoCache(Cache delegate) {
    this.delegate = delegate;
    this.keyList = new LinkedList<>();
    this.size = 1024;
  }  

  @Override
  public void putObject(Object key, Object value) {
    cycleKeyList(key);
    delegate.putObject(key, value);
  }

cycleKeyList就是循环利用key的意思

 private void cycleKeyList(Object key) {
    //将key添加到队列末尾
    keyList.addLast(key);
    //如果队列容量>当前设置的大小
    if (keyList.size() > size) {
      //移除第一个
      Object oldestKey = keyList.removeFirst();
      //并且从缓存中删除
      delegate.removeObject(oldestKey);
    }
  }

缓存中相对应的也会被删除

现在我们换回原来的默认实现LRU

在LruCache中有个setSize方法

public void setSize(final int size) {
    //第三个参数为true表示按访问顺序
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
      private static final long serialVersionUID = 4267176411845948333L;

      @Override
      protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
        boolean tooBig = size() > size;
        if (tooBig) {
          eldestKey = eldest.getKey();
        }
        return tooBig;
      }
    };

这时候在getObject的时候

  @Override
  public Object getObject(Object key) {
    //将元素访问一遍
    keyMap.get(key); //touch
    return delegate.getObject(key);
  }

这时候我们没有对元素进行访问,所以它还是按FIFO方式进行清除。当我们访问一个元素:

可以看到,此时刚刚被访问过的lk2便被放在了最后。

序列化

    /**
     * @CacheNamespace(readWrite =false) true 序列化 false 非序列化
     */
    @Test
    public void cacheTest4() {
        Cache cache = configuration.getCache("cxylk.mybatis.UserMapper");
        User user = Mock.newUser();
        cache.putObject("lk", user);// 设置缓存
        // 线程1
        Object lk = cache.getObject("lk");
        // 线程2
        Object lk1 = cache.getObject("lk");
        System.out.println(lk==lk1);//输出false
    }

因为经过了序列化和反序列化(java的序列化),所以不会相等

它们的值相等,但是id不一样。

关闭序列化,即设置readWrite=false后,两者就会相等。

好处:开启序列化后可以保证线程安全

坏处:需要耗费一定的时间来序列化和反序列化

ScheduledCache

在一定的有效期内清空缓存。是清空全部缓存,默认是1小时

this.clearInterval = 60 * 60 * 1000; // 1 hour

private boolean clearWhenStale() {
    //当前时间-最后一次清除时间>1小时,清除
    if (System.currentTimeMillis() - lastClear > clearInterval) {
        clear();
        return true;
    }
    return false;
}

@Override
public void clear() {
    //重置最后一次清除时间为当前时间
    lastClear = System.currentTimeMillis();
    //调用Cache的clean方法清除整个缓存
    delegate.clear();
}

责任链执行顺序

上面的责任链是怎么保证执行的顺序呢?在CacheBuilder类中定义了它的顺序

// 创建cache
public Cache build() {
    // 设置默认的缓存容器,过期策略的实现
    setDefaultImplementations();
    // 通过反射创建缓存容器对象,并设置id
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
    if (PerpetualCache.class.equals(cache.getClass())) {
      for (Class<? extends Cache> decorator : decorators) {
        // Lru
        cache = newCacheDecoratorInstance(decorator, cache);
        setCacheProperties(cache);
      }
      //构造执行链
      cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
      cache = new LoggingCache(cache);
    }
    return cache;
  }
 
  //设置默认的缓存容器,过期策略的实现
  private void setDefaultImplementations() {
    if (implementation == null) {
      implementation = PerpetualCache.class;
      if (decorators.isEmpty()) {
        decorators.add(LruCache.class);
      }
    }
  }
 
 //构造执行链
  private Cache setStandardDecorators(Cache cache) {
    try {
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      if (size != null && metaCache.hasSetter("size")) {
        metaCache.setValue("size", size);
      }
      // 默认为null,需要配置ScheduledCache才会被加入到执行链中
      if (clearInterval != null) {
          // 装饰定时刷新功能
        cache = new ScheduledCache(cache);
        ((ScheduledCache) cache).setClearInterval(clearInterval);
      }
      // 默认为true
      if (readWrite) {
        // 装饰序列功能
        cache = new SerializedCache(cache);
      }
      // 装饰日志功能
      cache = new LoggingCache(cache);
      // 装饰同步功能
      cache = new SynchronizedCache(cache);
      // 默认为false,BlockingCache
      if (blocking) {
          // 装饰防穿透功能
        cache = new BlockingCache(cache);
      }
      return cache;
    } catch (Exception e) {
      throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
  }

MapperBuilderAssistant类中,将cache加入configuration对象中

//创建新的缓存容器
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)
        //设置默认的缓存容器,若为null,默认为perpetualcache
        .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;
  }

二级缓存命中条件

二级缓存命中条件和一级缓存基本一样,不一样的地方在于二级缓存需要提交才会命中,至于为什么,后面再细说。

    @Test
    public void cacheTest5() {
        SqlSession sqlSession1 = sessionFactory.openSession();
        UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
        User user1 = mapper1.selectById(1);

        SqlSession sqlSession2 = sessionFactory.openSession();
        UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
        User user2 = mapper2.selectById(1);
    }

此时查询的话会发现进行了两次编译,即没有命中缓存

当对查询提交后

    @Test
    public void cacheTest5() {
        SqlSession sqlSession1 = sessionFactory.openSession();
        UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
        User user1 = mapper1.selectById(1);
		sqlSession1.commit();
        SqlSession sqlSession2 = sessionFactory.openSession();
        UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
        User user2 = mapper2.selectById(1);
    }

这个时候命中率为0.5,因为第二条sql语句命中了缓存

Cache Hit Ratio [cxylk.mybatis.UserMapper]: 0.5

当把commit换成close后,也会命中。因为底层(CachingExecutor)也会走commit。

还有种特殊情况:当设置了自动提交后,二级缓存是不会生效的,还是需要手动提交。

其他缓存命中条件和一级缓存一样,不再演示,如下图:

二级缓存配置

通过下面这张表来了解二级缓存有哪些配置

分别说下每个配置的意思:

1.cacheEnabled:当等于false时,整个二级缓存关闭

2.useCache:下面通过代码演示:

    @Options(useCache = false)
    User selectById(Integer id);

这个时候再执行上面的查询将不会再命中缓存。

3.flushCache:修改操作默认清除,查询默认不清除。这个配置需要注意下

    @Select({"select * from user where id=#{0}"})
    @Options(flushCache = Options.FlushCachePolicy.TRUE)
    User selectById(Integer id);

现在修改代码如下

    @Test
    public void cacheTest5() {
        SqlSession sqlSession1 = sessionFactory.openSession(true);
        UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
        User user1 = mapper1.selectById2(1);
//        mapper1.selectById(1);
        sqlSession1.commit();
        SqlSession sqlSession2 = sessionFactory.openSession();
        UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
        User user2 = mapper2.selectById2(1);
    }

这个时候是能够命中缓存的,但如果将mapp1.selectById(1)注释掉,那么就不会命中缓存。

因为前面的selectById2和selectById都在一个会话中,所以整个会话的缓存都会被清除。虽然selectById2没有开启清除缓存的配置。

还有一点需要注意,上面的配置一样需要提交commit才会生效,也就是说所有对缓存的更改都必须要提交

4.cacheNamespace和,前者用于类注解,后者用于xml配置文件,但是两者不能同时出现。这时就会出现一个问题:当开启了@cachNameSpace注解的类中有一个方法的sql映射到了xml文件中,而这时这个sql是不会命中缓存的,因为这两个用的不是一个缓存,但是又不能同时配置cacheNamespace和,那怎么办呢?就需要引用缓存空间了。

    <cache-ref namespace="cxylk.mybatis.UserMapper"/>
    <select id="selectByUser" resultMap="result_user" >
        .....

UserMapper是加了@cacheNamespac注解的接口,再执行查询就会命中缓存

cacheNamespacRef也是一样的,如果有一个接口配置了cacheNamespace注解额,那么我们在另外一个接口中就可以用该注解引用,这样的话两个接口就会共用一个缓存空间。

二级缓存之暂存区

前面我们说了对二级缓存所有的更改都必须要提交,为什么呢?因为二级缓存是应用级缓存,它是跨会话的,如果不提交就将数据写入数据库,会产生脏读的情况,如下图:

会话2先执行修改操作,将数据写入数据库,但是此时发生异常进行回滚,会话2又执行查询操作将回滚的数据写入到二级缓存,那么会话1就是查询到数据库中不存在的数据,即脏读。而当使用commit提交后,数据才会被写入到缓存区

注意上面的修改操作并不会真正去修改二级缓存,它是加了虚线的,实际上是清空暂存区。下面会详细说这个过程

结构

首先开启两个缓存空间

1.
@CacheNamespace
public interface BlogMapper {
    ...
}
2.
@CacheNamespace
public interface UserMapper {
    ...
}

测试代码

    @Test
    public void cacheTest6(){
        //当前只有1个会话
        SqlSession sqlSession=sessionFactory.openSession();
        sqlSession.getMapper(UserMapper.class).selectById(1);
        sqlSession.getMapper(BlogMapper.class).selectById(1);
        System.out.println(sqlSession);
    }

通过debug查看此时session的值,并且对比暂存区和缓存区结构

从上图可以看到,首先打开了一个会话,然后执行二级缓存,二级缓存中有一个事务缓存管理器,在它里面存放了两个暂存区,为什么有两个呢?因为我们通过注解开启了两个缓存空间。所以说缓存区的数量取决于我们开启了几个缓存空间而每个缓存空间都会对应一个唯一的缓存区,这个缓存区就是Cache,或者说它的实现类,也就是我们上面所说的那条责任链

从这里也可以看到它们之间的关系为1:1:1:n:1,事务缓存管理器和暂存区都是依赖于CacheExecutor的,而CacheExecutor和会话是1:1的,也就是说,如果会话关闭了,那么事务缓存管理器和暂存区也就不存在了

当执行commit操作后,暂存区的数据就会被写到缓存区中

二级缓存的存取流程

查询

下面通过代码演示这个过程:

    @Test
    public void cacheTest7() {
        SqlSession sqlSession1 = sessionFactory.openSession(true);
        UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
        mapper1.selectById(1);
        sqlSession1.commit();
        SqlSession sqlSession2 = sessionFactory.openSession();
        UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
        mapper2.selectById(1);
    }

执行第一个selectById,debug模式。

1、进入CacheExecutor的query方法:

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    //这里的ms中的id就是selectById的名称
    //开启缓存空间后,这个就不会为null
    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);
  }

需要注意下parameterObject这个参数,如果mapper接口中的方法参数只有一个,那么这个值就是参数值,如果是多个,那么它就是一个map结构,如果参数添加了@Param注解,那么key就是注解值,value就是参数,否则key就是param1,param2,arg1这种形式的参数。

2、flushCacheIfRequired方法

  private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    //判断当前方法上是否加了注解flushCache=true
    //因为当前方法没有加,所以不会执行清除缓冲区操作
    if (cache != null && ms.isFlushCacheRequired()) {
      //清除暂存区
      tcm.clear(cache);
    }
  }

3、继续回到query方法,现在执行到这里

    //判断是否使用注解,这里为true,往下继续执行 
	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;
      }

4、执行tcm.getObject方法,这个tcm也就是前面说的事务缓存管理器,并且参数传入的是cache

  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }

5.通过getTransactionalCache得到一个暂存区,它是通过当前的cache得到的,因为我们说过暂存区对应了一个唯一的缓存区,所以我们通过这个缓存区可以得到暂存区,就相当与通过key拿到了value

transactionalCaches的结构也说明了他们的关系:

private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

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

通过上面这段代码我们就得到了一个暂存区,并且暂存区有个属性cleanOnCommit为false,在下面会用到它。

  public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    //暂存区的具体数据结构
    this.entriesToAddOnCommit = new HashMap<>();
    //防止缓存穿透
    this.entriesMissedInCache = new HashSet<>();
  }

6、回到第4步,此时进入TransactionalCache的getObject方法:

  @Override
  public Object getObject(Object key) {
    // issue #116
    //从缓存中获取值,而不是查询暂存区
    Object object = delegate.getObject(key);
    //第一次进来,这里为null
    if (object == null) {
      //这个是为了防止缓存穿透的,所以此时设置了一个默认值
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

注意:假如第二次进来,缓存中能查到数据,但是此时clearOnCommit为true,那么它返回的是null,什么情况下它会为true呢?就是执行清除的时候

  @Override
  public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
  }

可以看到,上面的clear操作并没有真正去清除二级缓存,而是将clearOncommit置为true,然后清除暂存区

只有当我们执行commit操作后,才会去执行真正的清除缓存操作,或者将数据从暂存区填充到缓存区

  public void commit() {
    //判断clearOnCommit为true后
    if (clearOnCommit) {
      //真正的清除缓存
      delegate.clear();
    }
    flushPendingEntries();
    reset();
  }

clearOnCommit的作用就是防止脏读的

flushPendingEntries方法:

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

它的作用就是遍历暂存区,然后将数据填充到缓存区中

7、由第6步会返回一个null值,此时回到第3步,list为null

    //判断是否使用注解,这里为true,往下继续执行 
	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;
      }

如果缓存中没有就去数据库查,然后填充暂存区,否则直接返回缓存中的值

  @Override
  public void putObject(Object key, Object object) {
    //填充暂存区
    entriesToAddOnCommit.put(key, object);
  }

然后第二次查询的时候暂存区就能获取到相应的值。

上面就是查询的流程

修改

进入update方法

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

可以看到,首先会去执行flushCacheIfRequired方法清空暂存区缓存

  private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    //update的话,flushCache默认是为true的
    if (cache != null && ms.isFlushCacheRequired()) {
      tcm.clear(cache);
    }
  }

然后会去BaseExecutor中执行update方法,该方法会去执行clearLocalCache方法:

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

这里的localCache就是Cache的最终缓存实现PerpetualCache。

最后通过一张图来总结下上面的流程:

1
https://gitee.com/cxylk/Java-Notes.git
git@gitee.com:cxylk/Java-Notes.git
cxylk
Java-Notes
Java-Notes
main

搜索帮助