3.[源码]mybatis二级缓存源码分析(二)----二级缓存是如何实现的

上一篇我们介绍了mybatis的二级缓存作用范围, 二级缓存与一级缓存的结构关系, 今天就来介绍二级缓存本身是如何实现的~ 友情提示: 搭配 [源码]mybatis二级缓存源码分析(一)----一级缓存与二级缓存的结构关系 食用更香。

NO.1 |如何开启二级缓存

开启二级缓存的方式也比较简单, 如下:

第一步: MyBatis 配置文件中配置      第二步: 在Mapper.xml文件中配置标签, 一个Mapper.xml文件拥有唯一的namespace(命名空间)也可以配置,标签是为了引用其他的命名空间,那么当前命名空间将与引用的命名空间使用同一个缓存(对于同一命名空间下的多表查询可借助该标签避免脏读问题)

1.标签属性含义

在开启二级缓存的第二步中, 要在Mapper.xml文件中配置标签, 同时也可以为标签拥有的属性赋值, 那标签的属性们的含义都是什么?

type -代表着缓存的默认实现;size -代表缓存容量;eviction-代表溢出淘汰策略;flushInterval-代表缓存有效期;readOnly- 是否只读,若配置可读写,则需要对应的实体类能够序列化;blocking- 若缓存中找不到对应的key, 是否一直阻塞, 直到有对应的数据放入缓存;

2.产生的效果

做了如上配置后产生的效果如下

a.映射语句文件中的所有 select 操作的结果将会被缓存。b.映射语句文件中的所有 update操作( insert 、update 和 delete )会刷新缓存。c.缓存会使用最近最少使用(LRU, Least Recently Used)算法来淘汰不需要的缓存。d.缓存会间隔120000ms后清空一次缓存。e.缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。f.缓存会被视为读写缓存, 需要查询出来要被缓存的实体类实现Serializable接口。  这意味着获取到的对象并不是共享的,可以安全地被调用者修改, 而不干扰其他调用者或线程 。

关于readOnly="false"为何需要查询出来的缓存实体类实现序列化接口:

这是因为二级缓存为了保证读写安全, 开启了序列化功能, 缓存中保存的不再是查询出的对象本身, 而是查询出的对象进行序列化后的字节序列, 在获取数据的时候, 又会把存好的字节序列进行反序列化, 克隆出新对象, 进行返回。

所以对从二级缓存中得到数据做任何写操作, 都不会影响到缓存中原有的对象, 也就不会影响到其他来获取数据的调用者或线程。

tips: Java序列化就是指把Java对象转换为字节序列的过程。Java反序列化就是指把字节序列恢复为Java对象的过程。而在反序列化的时候会根据字节序列中保存的对象状态及描述信息, 重建对象。

NO.2 |二级缓存组件结构

从以上的描述中我们看出, Mybatis的二级缓存要实现的功能更加复杂, 比如: 线程安全, 过期清理, 命中率统计, 序列化….

Mybatis为了尽可能的职责分明的实现这些复杂逻辑, 在这里使用了一种设计模式: 装饰者+ 责任链(变种), 对二级缓存的功能组件进行设计。至于为什么说是一个责任链变种, 我们需要先了解一下经典责任链的定义。

tips: 责任链: (经典定义) 是一个请求有多个对象来处理,这些对象是一条链,但具体由哪个对象来处理,根据条件判断来确定,如果不能处理会传递给该链中的下一个对象,直到有对象处理它为止。

而责任链中的链, 是如何形成的呢? 举一个栗子, 比如我们的链式结构是a对象->b对象->c对象,  那我们就让a对象持有b对象, b对象持有c对象。从a对象开始, a对象的方法中可以调用b对象的方法, 而b对象的方法中也可以调用c对象的方法, 通过这样的方式, 便形成了一条责任链。

经典责任链的方式, 要根据条件判断, 虽然也许会经过链条上的很多对象, 但最终只有一个对象真正对请求进行了处理, 其他对象仅仅完成了向下传递。Mybatis的二级缓存使用的责任链模式则不同, 每一个链条上的对象不仅要调用自身持有的对象的方法, 完成了责任链的向下传递, 也要完成自己的功能实现。所以说Mybatis使用的是责任链的变种形式。

二级缓存的组件结构如下图所示:

3.[源码]mybatis二级缓存源码分析(二)----二级缓存是如何实现的_第1张图片

二级缓存组件的顶级接口是Cache, 定义了二级缓存的api, 比如设置缓存, 取出缓存。Cache下方有很多实现类, 正是这些实现类形成责任链, 组成了二级缓存。
实际上的结构是否如此呢, 在获取二级缓存的时候, 对二级缓存进行Debug, 就可以印证我们刚才的说法了。 3.[源码]mybatis二级缓存源码分析(二)----二级缓存是如何实现的_第2张图片

可以看出, 最上层是SyncronizedCache, 持有了一个名为delegate的LoggingCache类型对象, 以此类推, 直到链条上的最后一个Cache的实现类—PerpetualCache。而PerpetualCache本身持有了一个HashMap, 这才是二级缓存数据的真正存放地(缓存区)。

以查询为例,在调用二级缓存的getObject()方法的时候, 就会从链条的起始端, 比如SynchronizedCache, 开始调用SynchronizedCache的getObject()方法,  在getObject()方法里面, 每个实现类都有两部分的事情要做, 一个是完成自己特有的职能,  另一个是调用链条上的下一个Cache实现类的getObject()方法, 直到链条的尾端, 比如PerpetualCache。调用链虽然复杂, 但是每个实现类都是完成自己特有的附加功能, 而最终真正完成数据存储工作的只有PerpetualCache这个类。

先来看下PerpetualCache这个类的源码, 在这个类中的getObject方法, 仅仅是从map中取出数据

public class PerpetualCache implements Cache:
private Map cache = new HashMap<>();
@Overridepublic Object getObject(Object key) {  
  return cache.get(key);
}

而链条上的其他的Cache实现类是不是按照之前介绍的那样, 做自己的功能并调用自己持有的链条上的下一个实现类的方法呢, 我们也可以以几个实现类的源码为例来论证。比如: SynchronizedCache(负责线程安全)和 LoggingCache(负责命中率统计)。

在查看SynchronizedCache类的源码的时候, 不要忽略getObject方法上的synchronized关键字,这个方法在负责线程安全的问题的后, 便调用了责任链上的下一个对象的getObject()方法。

public class SynchronizedCache implements Cache:private final Cache delegate;@Overridepublic synchronized Object getObject(Object key) {  // 注意:看这里!!委派给下一个缓存实现类执行getObject()方法  return delegate.getObject(key);}

LoggingCache的getObject方法中,除了调用链条上的下一个对象的方法外,还会统计请求的次数和命中的次数,以此计算打印命中率。

public class LoggingCache implements Cache:private final Cache delegate;@Overridepublic Object getObject(Object key)  requests++; //请求次数  // 注意:看这里!! 委派给下一个缓存实现类执行getObject()方法  final Object value = delegate.getObject(key);  if (value != null) {    hits++;  // 命中次数  }  if (log.isDebugEnabled()) {    log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());  }  return value;}

NO.3 |事务缓存管理器

1.结构

我们都知道一个会话中的事务在未提交之前, 其他会话是不允许读到它未提交的数据的。在未加入二级缓存之前, 会话之间的都是如下图所示的样子, 各自为政, 互不干扰。

3.[源码]mybatis二级缓存源码分析(二)----二级缓存是如何实现的_第3张图片

通过上一篇的学习可以了解到, 二级缓存是可以跨会话的。那么这里我们要思考一下, 如果我们加入了二级缓存, 并且按照缓存的一贯思路(进行查询操作的时候先查缓存, 如果缓存中没有命中即查询数据库, 并且把查到的结果缓存到二级缓存中)来做, 会不会破坏原本的隔离性, 产生脏读? 来看下面一张图。
3.[源码]mybatis二级缓存源码分析(二)----二级缓存是如何实现的_第4张图片

会话1首先进行了修改操作, 然后进行了查询操作, 并且查询后就把查到的结果放入缓存中, 而此时会话2也进行了查询操作, 就会查到缓存中的结果直接返回, 尴尬的是会话1最终没有提交事务, 选择了回滚。这样就造成了会话2读到的数据不准确, 读到了会话1未提交的数据, 产生了脏读。

所以Mybatis的二级缓存在设计时针对这样的情况, 引入了事务缓存管理器。在事务缓存管理器中, 维护了一个本地暂存区(会话范围内可见), 本地暂存区又指向真正的缓存区(跨会话)。在进行查询操作的时候, 会到缓存区中查看是否命中。如果没有命中, 查询数据库得到数据后, 仅仅把查询的结果放入暂存区, 在提交事务的时候才要把暂存区中的数据刷新到缓存区。如果发生了回滚, 则清空本地暂存区缓存的数据, 不会刷新到缓存区, 这样一来就避免了脏读的产生。

接下来我们先来通过部分源码了解一下事务管理器的结构:

从以下代码可以看出每个CachingExecutor对应一个事务缓存管理器, 通过前面的学习我们知道, 每个会话中持有一个CachingExecutor(缓存执行器)。所以每个会话都有自己单独的事务缓存管理器

CachingExecutor:
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();

从以下代码我们得知, 在事务缓存管理器中维护了一个HashMap, 这个HashMap便是暂存区的集合, 而且这个map的key是cache(缓存区), 所以每一个缓存区都有对应的暂存区(TransactionalCache), 放在map中作为键值对被事务缓存管理器所维护, 因为每个会话都有自己单独的事务缓存管理器, 作为管理器属性集合中的一个对象—暂存区也只是会话可见的。

TransactionalCacheManager:private final Map transactionalCaches = new HashMap<>();

接下来看一下代表着暂存区的TransactionalCache, 可以看见其中也维护了一个Map, 这个map是暂存区真正用来暂存数据的地方, 而delegate属性, 代表的便是真正的缓存区(刚刚介绍过的, Cache的实现类组成的责任链, 完成了缓存区的维护), 有了与缓存区之间的关联, 在提交事务的时候, 就可以方便的把暂存区的数据刷新到缓存区了。

public class TransactionalCache implements Cache :private final Cache delegate;//指向缓存区private boolean clearOnCommit;private final Map entriesToAddOnCommit;//暂存区

介绍完事务管理器, 暂存区, 缓存区之间的结构关系, 我们来通过源码看下二级缓存进行查询和更新的过程。

2.查询

从之前文章的学习我们已经知道, 如果使用到二级缓存, 在查询时, 会调用二级缓存的query方法。这里主要看其中的**tcm.getObject(cache, key)tcm.putObject(cache, key, list)**方法, 一个是通过事务缓存管理器取数据的方法, 一个是通过事务管理器放入数据的方法。

CachingExecutor:
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
@Override
  public  List 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 list = (List) 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;
      }
    }
    // 委托给BaseExecutor执行
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
(1)tcm.getObject(cache, key)—>取出数据

在CachingExecutor的query()方法中, 先是调用了事务缓存管理器的getObject(cache, key)方法。可以看见TransactionalCacheManager在处理getObject()的时候先调用了getTransactionalCache(), 从map集合中取出当前缓存区对应的TransactionalCache(暂存区), 暂存区如果不存在, 则创建一个新的暂存区对象存入map, 然后调用获得的TransactionalCache的getObject()方法。

TransactionalCacheManager:
private final Map transactionalCaches = new HashMap<>();
public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
}
private TransactionalCache getTransactionalCache(Cache cache) {
    return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}

在TransactionalCache的getObject()方法中, 直接调用了其指向的缓存区的getObject()方法, 说明二级缓存在获取数据的时候会直接去缓存区(跨会话)取数据

而在clearOnCommit这个布尔值为true的时候, 即使缓存区命中数据也只能返回null, 这是因为, 只有在有更新操作且未提交的时候clearOnCommit才是true, 这种状态对于当前会话当前事务来说, 缓存区的数据已经不准确了, 所以最好的选择是重新查询数据库。

public class TransactionalCache implements Cache :
private final Cache delegate;//指向缓存区(链条式的Cache实现类)
private boolean clearOnCommit;//执行更新后clearOnCommit将变为true
private final Map entriesToAddOnCommit;//本地暂存//获取缓存数据, 从缓存区去查询
@Override
public Object getObject(Object key) {
  Object object = delegate.getObject(key); 
  if (object == null) {
    entriesMissedInCache.add(key);
   } 
  if (clearOnCommit) {//如果更新了数据, 缓存区就算有数据也要返回空, 要去数据库中取数据
        return null;  
   } else {
    return object;  
   }
}
(2)tcm.putObject(cache, key, list)—>放入数据

在query()方法中, 没有从缓存区中取到数据, 而重新查询了数据的情况下, 就要调用tcm.putObject(), 通过事务管理器设置数据到缓存。与getObject()一样, TransactionalCacheManager的putObject()方法也要先调用getTransactionalCache()获得TransactionalCache(暂存区), 然后调用TransactionalCache的putObject()方法。

TransactionalCacheManager:
private final Map transactionalCaches = new HashMap<>();
public void putObject(Cache cache, CacheKey key, Object value) {
  getTransactionalCache(cache).putObject(key, value);
}
private TransactionalCache getTransactionalCache(Cache cache) {
  return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}

如果我们继续查看, 就会发现在TransactionalCache的putObject()方法中, 数据仅被存到了暂存区中

public class TransactionalCache implements Cache:
@Overridepublic void putObject(Object key, Object object) {
  entriesToAddOnCommit.put(key, object); // 存数据, 存到暂存区
}

3.提交

在提交的方法中, 我们会把暂存区中的所有内容刷新到缓存区中

在我们调用sqlSession.commit()方法的时候, 也会调用当前会话持有的缓存执行器的commit()方法, 缓存执行器会执行事务缓存管理器的commit()方法。看一下事务缓存管理器的提交的源码, 在事务缓存管理器的commit()方法中, 会调用事务缓存管理器所有暂存区(TransactionalCache)的commit()方法。

TransactionalCacheManager:
private final Map transactionalCaches = new HashMap<>();
public void commit() {
  for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
  }
}

在TransactionalCache的commit()方法中, 如果有未提交的更新操作(clearOnCommit为true), 则要清空缓存区, 因为更新后, 缓存区的数据便是不准确的了。随后调用flushPendingEntries()和reset()两个方法, flushPendingEntries()方法负责把所有暂存区的内容刷新到缓存中。而reset()方法则负责把本地暂存区清空,  同时把clearOnCommit 置为false。

public class TransactionalCache implements Cache:
private final Cache delegate;//指向缓存区(链条式的Cache实现类)
private boolean clearOnCommit;//执行更新后clearOnCommit将变为true
private final Map entriesToAddOnCommit;//本地暂存
public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
}
private void flushPendingEntries() {
    for (Map.Entry entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
}
private void reset() {
    clearOnCommit = false;
    entriesToAddOnCommit.clear();
    entriesMissedInCache.clear();
}

4.更新

在缓存执行器调用更新操作的时候, 会调用flushCacheIfRequired(), 这个方法中会先判断ms.isFlushCacheRequired(), 为true并且二级缓存存在就会执行事务缓存执行器的clear()方法, 而isFlushCacheRequired()就是从标签里面取到的flushCache的值。而增删改操作的flushCache属性默认为true。所以进行更新的时候, 也会调用事务缓存管理器的clear方法。

public class CachingExecutor implements Executor:
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();  
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {
            tcm.clear(cache);
    }
}

在TransactionalCacheManager 的clear方法中。依然是先获取暂存区, 并调用暂存区的clear()方法。

TransactionalCacheManager:
private final Map transactionalCaches = new HashMap<>();
public void clear(Cache cache) {
  getTransactionalCache(cache).clear();
}

TransactionalCache的clear()方法中, clearOnCommit属性被置为了true, 并清空了暂存区。清空暂存区不难理解, 因为如果存在更新操作, 则暂存区中暂存起来的数据则有可能不再准确了。并且缓存区也定然出现了不一致的情况, 所以在TransactionalCache的commit方法中, 会去判断clearOnCommit是否为true(即是否进行过更新操作),  如果是, 缓存区的数据也会被clear()掉。而在清除执行完成后, reset()方法中会把clearOnCommit重新置为false。

NO.4 |总结

Mybatis使用了装饰者+责任链(变种)的模式构建了二级缓存的组件, 每一个功能都有相应的Cache实现类来完成, 同时这些实现类也会调用自己持有的Cache实现类, 完成责任链。最终被调用的类是PerpetualCache ,它就是最终负责数据存储的类。

而为了解决二级缓存跨会话使用可能引起的脏读问题, mybatis引入了事务缓存管理器, 每一个会话持有一个事务缓存管理器, 每个事务缓存管理器维护着多个缓存区(每个namespace都有对应的缓存区)对应的暂存区, 暂存区中维护本地暂存数据, 并指向它所属的缓存区。

通过事务缓存管理器查询的时候, 直接去查缓存区, 但是如果没有命中, 重新查询出的数据仅放入暂存区, 直到进行提交, 才把数据刷新到缓存区。这是为了防止其他会话查到当前会话中的事务未提交的数据。而在执行更新操作的时候, 会先清空对应的暂存区数据, 在提交事务的时候, 也会把对应的缓存区数据清空。

结合我们之前讲的两篇文章, 所有关于mybatis多级缓存的事情就交代清楚了。3.[源码]mybatis二级缓存源码分析(二)----二级缓存是如何实现的_第5张图片

这里除了编程知识分享,同时也是我成长脚印的记录, 期待与您一起学习和进步, 长按下方二维码关注公众号: 程序媛swag。如果您觉得这篇文章帮助到了您, 请帮忙点个在看 !

3.[源码]mybatis二级缓存源码分析(二)----二级缓存是如何实现的_第6张图片

你可能感兴趣的:(mybatis,mybatis)