likes
comments
collection
share

水煮MyBatis(十四)- 细说缓存刷新机制

作者站长头像
站长
· 阅读数 55

前言

连续写几个大章节,乏了,这一章写点轻松的。什么是tcm,其实在上一章有提到,是CachingExecutor的一个类属性【TransactionalCacheManager 】,主要用来处理二级缓存的刷新逻辑。tcm里包含一个cache和transactionCache的Map集合,这里的【TransactionalCache】,也是Cache的一个实现,不过由于其用法特殊,没有和其他实现类放在一起讨论。

从代码可以看出,虽然tcm的名称和事务沾点边,但其实无论是否开启事务,只要激活二级缓存,都需要用到tcm。

  // tcm是CachingExecutor的一个类属性
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();
  // 查询入口
  public <E> List<E> query(...){
      Cache cache = ms.getCache();
      // 关键就是这一行,tcm.getObject(cache, key),从指定的cache中,查找指定的key
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        // 调用baseExecutor里的查询
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
  }

为什么用tcm?

一起看看这一行代码 List<E> list = (List<E>) tcm.getObject(cache, key); 原本Cache的默认实现里,就包含一个map结构,用来存储缓存数据。缓存数据可以直接从cache中读取,比如:【cache.getObject(key)】。所以究竟是基于什么考虑,才用tcm的方式呢?

tcm读写

我们从TransactionalCacheManager的缓存读写方法来分析,源码如下:

  // 
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
  // 读取缓存数据
  public Object getObject(Cache cache, CacheKey key) {
    // 从TransactionalCache中读取数据
    return getTransactionalCache(cache).getObject(key);
  }
  // 数据写入TransactionalCache
  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }
  
  // 从transactionalCaches中获取TransactionalCache对象,如果不存在,则新建一个映射关系,并返回新建的TransactionalCache对象
  private TransactionalCache getTransactionalCache(Cache cache) {
    return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
  }

可以看到,数据写入了TransactionalCache,读取时,也是从TransactionalCache中返回的。需要注意的是新建TransactionalCache时,将前面一个参数传入了构造函数,从而与外部cache实例完成了绑定。 水煮MyBatis(十四)- 细说缓存刷新机制

TransactionalCache做了什么?

TransactionalCache是cache的集合,新建时传入了玩不cache实例,在类中,需要重点关注的是这两个属性:

  • entriesToAddOnCommit:等待刷入数据,在提交时刷入cache实例;
  • entriesMissedInCache: 读取缓存时,没有数据的key集合,提交时,创建value为null的entry对象,放入cache实例;

这里只列出主要的几个方法

// 新建TransactionalCache时,传入的外部cache实例
 private final Cache delegate;
 // 提交的时候是否需要清除缓存,受Options里清除策略影响
  private boolean clearOnCommit;
  // 等待刷入数据
  private final Map<Object, Object> entriesToAddOnCommit;
  // 读取缓存时,没有数据的key集合
  private final Set<Object> entriesMissedInCache;

  @Override
  public Object getObject(Object key) {
    // 从内嵌的cache对象读取缓存数据
    Object object = delegate.getObject(key);
    if (object == null) {
      entriesMissedInCache.add(key);
    }
    return object;
  }

  @Override
  public void putObject(Object key, Object object) {
    // 放入到临时集合,等待提交
    entriesToAddOnCommit.put(key, object);
  }

  // cachingExecutor提交时,调用此方法
  public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    // 数据刷入cache实例
    flushPendingEntries();
    // 重置下面三个数据
    clearOnCommit = false;
    entriesToAddOnCommit.clear();
    entriesMissedInCache.clear();
  }

  private void flushPendingEntries() {
    // 将entriesToAddOnCommit里所有的数据放入到cache实例
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        // 创建value为null的entry对象,放入cache实例
        delegate.putObject(entry, null);
      }
    }
  }

结构也比较简单,大概逻辑如下:

  • 读取数据时,如果cache实例中不存在,则放入entriesMissedInCache集合;
  • 保存数据时,放到临时集合【entriesToAddOnCommit】,等待提交再处理;
  • cachingExecutor提交时,触发commit方法,把数据刷入到cache实例;

为什么这么做?

官方的说法如下

The 2nd level cache transactional buffer. 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.

原因有三:

  1. 如果事务回滚,则将数据丢弃,不会放入到cache实例,这样可以有效避免脏数据;
  2. 事务期间,数据不会加载到cache实例,其他sqlSession不会读到临时数据;
  3. blockingCache中,release方法中,如果对象不存在,会抛出异常:Detected an attempt at releasing unacquired lock. This should never happen.,TransactionalCache会将查询过的所有key,无论是否存在缓存数据,都放入cache实例,一定程度上可以避免这个异常;

思考

tcm作为CachingExecutor的一个类对象,为什么还需要在内部维护一个cache的集合呢?毕竟一个executor的生命周期比较短暂,数据库操作结束就被关闭,而且二级缓存的实现类一般都会统一配置。原因可能有两个:

  1. 不同Mapper可以配置不同的eviction策略,那么就会有不同的cache实例;
  2. 一个事务之内,会同用sqlSession,CachingExecutor会处理多个数据库操作; 基于上面两个原因,tcm就需要维护一个cache集合了。
转载自:https://juejin.cn/post/7244820297278767161
评论
请登录