6.二级缓存

  • Post author:
  • Post category:其他




1.二级缓存概述

前一章节介绍了一级缓存即会话级别的缓存,同一个会话SqlSession才能共享缓存。如果多个 SqlSession 需要共享缓存,则需要开启二级缓存,开启二级缓存后,会使用 CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在CachingExecutor 进行二级缓存的查询。

二级缓存也称作是应用级缓存,与一级缓存不同的,是它的作用范围是整个应用,而且可以跨线程使用。所以二级缓存有更高的命中率,适合缓存一些修改较少的数据。在流程上是先访问二级缓存,再访问一级缓存。

image-20210802235146269

当二级缓存开启后,同一个命名空间(namespace) 所有的操作语句,都影响着一个

共同的 cache

,也就是二级缓存被多个 SqlSession 共享,是一个

全局的变量



2.开启二级缓存

二级缓存默认是不开启的,需要手动开启二级缓存开启二级缓存的条件也是比较简单

  • 通过直接在 MyBatis 配置文件中通过

    <settings>
    	<setting name = "cacheEnabled" value = "true" />
    </settings>
    
  • 在 Mapper 的xml 配置文件中加入

    <cache>

    标签

  • useCache 为true

  • flushCache 为false


设置 cache 标签的属性


  • type

    : 指定自定义缓存的全类名(实现Cache 接口即可),PerpetualCache

    默认值


  • eviction

    : 缓存回收策略,有这几种回收策略。默认值为**LRU **

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

  • flushinterval

    缓存刷新间隔,缓存多长时间刷新一次,默认不清空,设置一个毫秒值


  • size

    : 缓存存放多少个元素


  • readOnly

    : 是否只读;

    true 只读

    ,MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。MyBatis 为了加快获取数据,直接就会将数据在缓存中的引用交给用户。不安全,速度快。

    读写(默认)

    :MyBatis 觉得数据可能会被修改。默认值为

    false


  • blocking

    : 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。默认值为

    false



3.二级缓存初始化

解析mapper xml文件二级缓存的关键点。mybatis首先进行解析的是

cache-ref

标签,其次进行解析的是

cache

标签。二级缓存解析后,存入Cache

// 配置cache-ref
cacheRefElement(context.evalNode("cache-ref"));
// 配置cache
cacheElement(context.evalNode("cache"));



3.1解析cache

// 对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();
    // 构建Cache对象放入Configuration对象中
    builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
  }
}
// 使用了构建器模式,一步一步构建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)
      // 若缓存中找不到对应的key,是否会一直blocking
      .blocking(blocking)
      .properties(props)
      .build();
  // 把cache放入configuration对象中
  // Configuration类中的字段 key为namespace
  // protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
  configuration.addCache(cache);
  // 构建MapperStatement时使用  
  currentCache = cache;
  return cache;
}
public Cache build() {
  setDefaultImplementations();
  // 根据指定的type,implementation初始化Cache类型
  Cache cache = newBaseCacheInstance(implementation, id);
  setCacheProperties(cache);
  // 如果是默认的PerpetualCache,则使用装饰器模式 装饰基本的缓存Cache
  // 如LRU、FIFO等都是Cache的装饰器模式的类  
  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 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);
    }
    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);
  }
}



3.2解析cache-ref

private void cacheRefElement(XNode context) {
  if (context != null) {
    configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
    CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
    try {
      cacheRefResolver.resolveCacheRef();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteCacheRef(cacheRefResolver);
    }
  }
}
public class MapperBuilderAssistant extends BaseBuilder {
    public Cache useCacheRef(String namespace) {
      if (namespace == null) {
        throw new BuilderException("cache-ref element requires a namespace attribute.");
      }
      try {
        unresolvedCacheRef = true;
        Cache cache = configuration.getCache(namespace);
        if (cache == null) {
          throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
        }
        currentCache = cache;
        unresolvedCacheRef = false;
        return cache;
      } catch (IllegalArgumentException e) {
        throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
      }
    }
    
    public MappedStatement addMappedStatement() {
        //...
        .useCache(valueOrDefault(useCache, isSelect))
        // 给MappedStatement设置缓存对象   
        .cache(currentCache);
        //...
    }
}



4.二级缓存的功能

  • 存储【核心功能】

    • 内存:最简单就是在内存当中,不仅实现简单,而且速度快。内存弊端就是不能持久化,且容易有限。

    • 硬盘:可以持久化,容量大。但访问速度不如内存,一般会结合内存一起使用。

    • 第三方集成:在分布式情况,如果想和其它节点共享缓存,只能第三方软件进行集成。比如Redis.

  • 溢出淘汰【核心功能】

    无论哪种存储都必须有一个容易,当容量满的时候就要进行清除,清除的算法即溢出淘汰机制。

    • FIFO:先进先出

    • LRU:最近最少使用

    • WeakReference: 弱引用,将缓存对象进行弱引用包装,当Java进行gc的时候,不论当前的内存空间是否足够,这个对象都会被回收

    • SoftReference:软件引用,基机与弱引用类似,不同在于只有当空间不足时GC才才回收软引用对象。

  • 其他功能

    • 过期清理:指清理存放数据过久的数据
    • 线程安全:保证缓存可以被多个线程同时使用
    • 写安全:当拿到缓存数据后,可对其进行修改,而不影响原本的缓存数据。通常采取做法是对缓存对象进行深拷贝。



5.二级缓存的设计

这么多的功能,如何才能简单的实现,并保证它的灵活性与扩展性呢?这里MyBatis抽像出Cache接口,其只定义了缓存中最基本的功能方法:

  • 设置缓存
  • 获取缓存
  • 清除缓存
  • 获取缓存数量

然后上述中每一个功能都会对应一个组件类,并基于装饰者加责任链的模式,将各个组件进行串联。在执行缓存的基本功能时,其它的缓存逻辑会沿着这个责任链依次往下传递。

image-20210902235343304

这样设计有以下优点:

  1. 职责单一:各个节点只负责自己的逻辑,不需要关心其它节点。
  2. 扩展性强:可根据需要扩展节点、删除节点,还可以调换顺序保证灵活性。
  3. 松耦合:各节点之间不没强制依赖其它节点。而是通过顶层的Cache接口进行间接依赖。



5.1 BlockingCache

防止高速缓存未命中时对数据库的大规模访问,它设置了对高速缓存键的锁定

public class BlockingCache implements Cache {
    
  public BlockingCache(Cache delegate) {
    this.delegate = delegate;
    this.locks = new ConcurrentHashMap<>();
  }  
    
  @Override
  public void putObject(Object key, Object value) {
    try {
      delegate.putObject(key, value);
    } finally {
      releaseLock(key);
    }
  }

  @Override
  public Object getObject(Object key) {
    // 获取值,并发情况下如果key正在获取,一直blocking,直到有对应的数据进入缓存
    acquireLock(key);
    Object value = delegate.getObject(key);
    // 如果没有值则一直阻塞,直到putObject   
    if (value != null) {
      releaseLock(key);
    }
    return value;
  }
    
  private void acquireLock(Object key) {
    CountDownLatch newLatch = new CountDownLatch(1);
    while (true) {
      // 如果传入key对应的value已经存在,就返回存在的value,不进行替换。如果不存在,就添加key和value,返回null  
      CountDownLatch latch = locks.putIfAbsent(key, newLatch);
      if (latch == null) {
        break;
      }
      try {
        if (timeout > 0) {
          boolean acquired = latch.await(timeout, TimeUnit.MILLISECONDS);
          if (!acquired) {
            throw new CacheException(
                "Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());
          }
        } else {
          latch.await();
        }
      } catch (InterruptedException e) {
        throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
      }
    }
  }    
    
}



5.2 SynchronizedCache

锁定缓存,防止并发

public class SynchronizedCache implements Cache {

  private final Cache delegate;

  public SynchronizedCache(Cache delegate) {
    this.delegate = delegate;
  }
    
  @Override
  public synchronized void putObject(Object key, Object object) {
    delegate.putObject(key, object);
  }

  @Override
  public synchronized Object getObject(Object key) {
    return delegate.getObject(key);
  }    
    
}



5.3 LoggingCache

记录缓存的命中率,以同一个mapper xml 文件所有开启二级缓存的查询为基数

public class LoggingCache implements Cache {

  private final Log log;
  private final Cache delegate;
  protected int requests = 0;
  protected int hits = 0;

  public LoggingCache(Cache delegate) {
    this.delegate = delegate;
    this.log = LogFactory.getLog(getId());
  }
    
  @Override
  public Object getObject(Object key) {
    requests++;
    final Object value = delegate.getObject(key);
    if (value != null) {
      hits++;
    }
    if (log.isDebugEnabled()) {
      log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
    }
    return value;
  }    
}



5.4 ScheduledCache

定期清除缓存

public class ScheduledCache implements Cache {

  private final Cache delegate;
  // mapper.xml文件中配置的flushInterval,缓存刷新间隔时间  
  protected long clearInterval;   
  protected long lastClear;

  public ScheduledCache(Cache delegate) {
    this.delegate = delegate;
    // 默认一小时清除一次  
    this.clearInterval = TimeUnit.HOURS.toMillis(1);
    this.lastClear = System.currentTimeMillis();
  }

  // 是否到时间清除缓存  
  private boolean clearWhenStale() {
    // 到了清除缓存的时间  
    if (System.currentTimeMillis() - lastClear > clearInterval) {
      // 清除缓存  
      clear();
      return true;
    }
    return false;
  }
    
  @Override
  public void clear() {
    // 更新最后一次清除缓存的时间  
    lastClear = System.currentTimeMillis();
    delegate.clear();
  }  
    
  @Override
  public void putObject(Object key, Object object) {
    clearWhenStale();
    delegate.putObject(key, object);
  }

  @Override
  public Object getObject(Object key) {
    // 到了清除缓存的时间,返回null  
    return clearWhenStale() ? null : delegate.getObject(key);
  }    
}    



5.5 LruCache

缓存满了之后淘汰策略(近期最少使用),主要依赖于LinkedHashMap实现。

LinkedHashMap继承于HashMap,它使用了一个双向链表来存储 Map 中的 Entry 顺序关系,对于 get、put、remove 等操作,LinkedHashMap 除了要做 HashMap 做的事情,还做些调整 Entry 顺序链表的工作。LruCache 中将 LinkedHashMap 的顺序设置为 LRU 顺序来实现 LRU 缓存,每次调用 get(也就是从内存缓存中取图片),则将该对象移到链表的尾端。调用 put 插入新的对象也是存储在链表尾端,这样当内存缓存达到设定的最大值时,将链表头部的对象(近期最少用到的)移除。

public class LruCache implements Cache {

  private final Cache delegate;
  // mapper.xml文件 配置的size 缓存存放多少个元素即为Map的大小  
  private Map<Object, Object> keyMap;
  private Object eldestKey;

  public LruCache(Cache delegate) {
    this.delegate = delegate;
    setSize(1024);
  }
  
  // mapper.xml文件 配置的size 缓存存放多少个元素
  public void setSize(final int size) {
    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;
      }
    };
  } 
    
  @Override
  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);
    cycleKeyList(key);
  }

  @Override
  public Object getObject(Object key) {
    keyMap.get(key); // touch
    return delegate.getObject(key);
  }
  
  // 每次把结果放入缓存,需要先把结果放入LinkedHashMap,当Map满了之后,需要移除近期最少使用的 
  private void cycleKeyList(Object key) {
    keyMap.put(key, key);
    if (eldestKey != null) {
      delegate.removeObject(eldestKey);
      eldestKey = null;
    }
  }    



5.6 FifoCache

先进先出策略。元素不停的加入缓存直到缓存满为止,当缓存满时,清理过期缓存对象,清理后依旧满则删除先入的缓存

public class FifoCache implements Cache {

  private final Cache delegate;
  private final Deque<Object> keyList;
  // mapper.xml文件 配置的size 缓存存放多少个元素  
  private int size;

  public FifoCache(Cache delegate) {
    this.delegate = delegate;
    this.keyList = new LinkedList<>();
    this.size = 1024;
  }
  // 设置数据量大小  
  public void setSize(int size) {
    this.size = size;
  }    
 
  @Override
  public void putObject(Object key, Object value) {
    cycleKeyList(key);
    delegate.putObject(key, value);
  }
    
  private void cycleKeyList(Object key) {
    keyList.addLast(key);
    if (keyList.size() > size) {
      Object oldestKey = keyList.removeFirst();
      delegate.removeObject(oldestKey);
    }
  }    
}    



5.7 PerpetualCache

缓存数据真正存储的地方(内存中),一级缓存已经介绍过。



6.CachingExecutor

CachingExecutor缓存执行器,用于处理二级缓存的。二级缓存和一级缓存相对独立的逻辑,而且二级缓存可以通过参数控制关闭,而一级缓存是不可以的。综上原因把二级缓存单独抽出来处理。抽取的方式采用了装饰者设计模式,即在CachingExecutor 对原有的执行器进行包装,处理完二级缓存逻辑之后,把SQL执行相关的逻辑交给实至的Executor处理。



6.1二级缓存命中条件

二级缓存的命中场景与一级缓存类似,不同在于二级可以跨会话使用,还有就是二级缓存的更新,必须是在会话提交之后。

  • 相同的MapperStatement ID
  • SQL语句相同
  • 参数相同
  • RowBounds行范围相同
  • 相同的environmentId(防止切换数据库)
  • 没有使用ResultHandler来自定义返回数据
  • 没有配置UseCache=false 来关闭缓存
  • 没有配置FlushCache=true 来清空缓存
  @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) {
      // 如果需要的话刷新缓存,即FlushCache=true
      flushCacheIfRequired(ms);
      // 查询使用缓存UseCache=true
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        // 查询缓存
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          // 缓存中无数据
          // 委托模式,交给SimpleExecutor等实现类去实现方法。
          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) {
    Cache cache = ms.getCache();
    // FlushCache=true,清除缓存
    if (cache != null && ms.isFlushCacheRequired()) {
      tcm.clear(cache);
    }
  }



7.二级缓存结构

二级缓存的生效,是在会话提交之后。


为什么要提交之后才能命中缓存?

image-20210904232436333

二级缓存是可以跨线程会话使用的。

如上图两个会话在修改同一数据,当会话二修改后,在将其查询出来,假如它实时填充到二级缓存,而会话一就能过缓存获取修改之后的数据,但实质是修改的数据回滚了,并没真正的提交到数据库。

所以为了保证数据一至性,二级缓存必须是会话提交之后才会真正填充,包括对缓存的清空,也必须是会话正常提交之后才生效。

为了实现会话提交之后才变更二级缓存,MyBatis为每个会话设立了若干个暂存区,当前会话对指定缓存空间的变更,都存放在对应的暂存区,当会话提交之后才会提交到每个暂存区对应的缓存空间。为了统一管理这些暂存区,每个会话都有一个唯一的事物缓存管理器。

image-20210904235708657



7.1TransactionalCacheManager

事物缓存管理器

public class TransactionalCacheManager {

  // 缓存暂存区,一个会话可能使用多个Cache
  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 transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
  }

}



7.2TransactionalCache

缓存暂存区

public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);
  // 具体实现的缓存
  private final Cache delegate;
  // 是否已经清除暂存区缓存
  private boolean clearOnCommit;
  // 暂存区存入的缓存值(key -> value),待commit提交至二级缓存
  private final Map<Object, Object> entriesToAddOnCommit;
  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;
    }
  }

  // 放入缓存,加入暂存区中,等待commit刷新至二级缓存
  @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();
  }

  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 notifiying a rollback to the cache adapter. "
            + "Consider upgrading your cache adapter to the latest version. Cause: " + e);
      }
    }
  }

}
  • 查询操作query

    当会话调用query() 时,会基于查询语句、参数等数据组成缓存Key,然后尝试从二级缓存中读取数据。读到就直接返回,没有就调用被装饰的Executor去查询数据库,然后在填充至对应的暂存区。

  • 更新操作update

    当执行update操作时,在执行update之前清空缓存。这里清空只针对暂存区,同时记录清空的标记,以便当会话提交之时,依据该标记去清空二级缓存空间。

    如果在查询操作中配置了flushCache=true ,也会执行相同的操作。

  • 提交操作commit

mmit.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 notifiying a rollback to the cache adapter. ”

+ “Consider upgrading your cache adapter to the latest version. Cause: ” + e);

}

}

}

}


- 查询操作query

  当会话调用query() 时,会基于查询语句、参数等数据组成缓存Key,然后尝试从二级缓存中读取数据。读到就直接返回,没有就调用被装饰的Executor去查询数据库,然后在填充至对应的暂存区。

- 更新操作update

  当执行update操作时,在执行update之前清空缓存。这里清空只针对暂存区,同时记录清空的标记,以便当会话提交之时,依据该标记去清空二级缓存空间。

  > 如果在查询操作中配置了flushCache=true ,也会执行相同的操作。

- 提交操作commit

  当会话执行commit操作后,会将该会话下所有暂存区的变更,更新到对应二级缓存空间去。



版权声明:本文为wangbo199308原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。