Mybatis源码3 CachingExecutor, 二级缓存,缓存的实现

Mybatis CachingExecutor, 二级缓存,缓存的实现

一丶二级缓存概述

Mybatis源码3 CachingExecutor, 二级缓存,缓存的实现

上一章节,我们知道mybaits在构造SqlSession的时候,需要让SqlSession持有一个执行器,如果配置了缓存开启,那么在Configuration.newExecutor的时候,会使用CachingExecutor对执行器进行装饰(被装饰可能是SimpleExecutor,ReuseExecutor,BatchExecutor)

  //如果要求缓存,生成另一种CachingExecutor(默认就是有缓存),装饰者模式,所以默认都是返回CachingExecutor
  if (cacheEnabled) {
  //上面根据配置的ExecutorType 生成了executor==> SimpleExecutor or ReuseExecutor or BatchExecutor
      //这里的开启不意味着一定开启二级缓存,而是说通过CachingExecutor装饰,可能用到二级缓存,为什么这么说,见  CachingExecutor 代码学习
    executor = new CachingExecutor(executor);
  }

二丶为什么需要二级缓存

一级缓存是SqlSession级别的(SqlSession持有一个Excutor,一个Excutor对于一个一级缓存)

所以SqlSesion的生命周期等于一级缓存的生命周期,且不同SqlSession的一级缓存不共用

不同session进行相同SQL查询的时候,是查询两次数据库的。显然这是一种浪费,既然SQL查询相同,就没有必要再次查库了,直接利用缓存数据即可,这种思想就是MyBatis二级缓存的初衷。如果开启二级缓存,关闭sqlsession后,会把该sqlsession一级缓存中的数据添加到mapper namespace的二级缓存中。这样,缓存在sqlsession关闭之后依然存在。

三丶Cache的实现类和缓存管理器

Mybatis源码3 CachingExecutor, 二级缓存,缓存的实现

其实还有一个BlockingCache,但是我的idea画图有点问题

  • PerpetualCache mybatis 提供的缓存默认实现,基于hashMap
  • TransactionalCache,事务缓存,持有以下字段
//装饰器模式,真正的缓存操作将交由此执行
private Cache delegate;
//是否因为commit而清空了缓存 避免脏读
private boolean clearOnCommit;
//commit时要添加的元素  二级缓存变更的数据都将存储在 entriesToAddOnCommit中,后续同步到真正的缓存空间 //有点类似于git的本地代码 commit之后经过push才提交到云端服务器,只不过mybatis的push是commit操作
private Map entriesToAddOnCommit;
//缓存未命中的key 避免缓存穿透
private Set entriesMissedInCache;
  • TransactionalCacheManager 事务缓存管理器,没有实现cache接口,持有以下字段
//管理了许多TransactionalCache
//二级缓存是和nameSpace相关的,map的key对应一个mapperStatement
// map的value 是上面讲到的事务缓存,事务缓存持有 Cache delegate指向真正的二级缓存数据存储区域
  private Map transactionalCaches = new HashMap();

每一个Sqlsession对应一个CachingExecutor,每一个CachingExcutor对应一个事务缓存管理器,每一个事务缓存管理器有多个value,value中的TransactionalCache 中的delegate是真正的二级缓存缓存区,entriesToAddOnCommit是其暂存区

Mybatis源码3 CachingExecutor, 二级缓存,缓存的实现

四丶CachingExcutor源码学习

1.查询

public  List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
    //二级缓存适合MappedStatement 绑定的 MappedStatement是和mapper的nameSpace一对一的
    //这里的cache是属于这个mapper的暂存区
  Cache cache = ms.getCache();
  //默认情况下是没有开启缓存的(二级缓存).要开启二级缓存,
  // 你需要在你的 SQL 映射文件中添加一行:  or使用注解
  //先查CacheKey,查不到再委托给实际的执行器去查
  if (cache != null) {
      //如果需要刷新缓存 对应flushCache的配置
    flushCacheIfRequired(ms);
      // 如果配置了使用 二级缓存 useCach配置
      //
    if (ms.isUseCache() && resultHandler == null) {
        //确定是存储过程的时候没有out类型的参数 如果有抛出异常
        //二级缓存不支持有输出的存储过程
      ensureNoOutParams(ms, parameterObject, boundSql);
        //根据暂存区的cache从缓存区取值
      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;
    }
  }
    //没有使用二级缓存 那么被装饰者执行
  return delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

为什么说是提交到暂存区了

//TransactionalCacheManager的
  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }
// TransactionalCache 的 putObject
//其实是加到了 TransactionalCache的entriesToAddOnCommit中了 等commit的时候再加入到二级缓存,从而保证二级缓存中的数据是提交后的避免造成脏读
//假设发生了一个写操作,执行完成后另外一个请求查询到了该数据直接放置到二级缓存区域,但是此时这条数据执行了回滚操作,那么此时就会造成一个脏读!
@Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

2.更新

 public int update(MappedStatement ms, Object parameterObject) throws SQLException {
//刷新缓存完再update
     //这里的刷新缓存并不是立马将二级缓存中的数据进行清空
     //而是TransactionalCache中的clearOnCommit 置为了true
//如果更新的时候直接清空二级缓存 那么发生回滚的时候,二级缓存的数据回不来了,降低了二级缓存的命中率
   flushCacheIfRequired(ms);
   return delegate.update(ms, parameterObject);
 }

3.commit

@Override
public void  (boolean required) throws SQLException {
  delegate.commit(required);
  tcm.commit();
}

 //tcm.commit(); 多了commit方法,提供事务功能
  public void commit() {
      //提交是先清空缓存,避免被缓存和数据库不一致
    if (clearOnCommit) {
      delegate.clear();
    }
      //把待提交的数据 加入到二级缓存
    flushPendingEntries();
      //重置属性  clearOnCommit = false;entriesToAddOnCommit清空,entriesMissedInCache清空
    reset();
  }

  private void flushPendingEntries() {
      //待提交的数据 加入到二级缓存
    for (Map.Entry entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
      //没有命中的数据赋值为null  "这样可以避免缓存穿透么?"
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }

4.rollback

  @Override
  public void rollback(boolean required) throws SQLException {
    try {
      delegate.rollback(required);
    } finally {
      if (required) {
        tcm.rollback();
      }
    }
  }

//tcm.rollback
public void rollback() {
  unlockMissedEntries();
  reset();
}
private void unlockMissedEntries() {
    for (Object entry : entriesMissedInCache) {
      delegate.putObject(entry, null);
    }
  }

五丶Mybatis是如何把众多Cache的装饰器串联起来组成责任链模式的

mybatis构建一个二级缓存使用的是CacheBuilder(建造者模式)

//对应mapperStateMent的id
private String id;
//二级缓存的具体实现 也就是说可以通过配置进行自定义的二级缓存,甚至是基于第三方如redis的缓存
  private Class implementation;
//装饰器,通过配置 可以选择使用那些进行装饰增强
private List> decorators;
//缓存的大小
private Integer size;
//缓存自动清楚间隔
private Long clearInterval;
//读写锁
  private boolean readWrite;
//是否阻塞
private boolean blocking;
public Cache build() {
    //如果用户没有自定义的缓存 那么使用PerpetualCache 如果用户没有指定的淘汰策略 那么使用LruCache 最近最少使用的原则进行淘汰
  setDefaultImplementations();
  //先new一个base的cache(PerpetualCache)  没有指定 那么就是反射生成PerpetualCache
  Cache cache = newBaseCacheInstance(implementation, id);
  //设额外属性
  setCacheProperties(cache);
  // issue #352, do not apply decorators to custom caches
    //装饰器 只对默认的二级缓存PerpetualCache 生效,也就是说自定义的cache不被装饰,淘汰策略,锁等等要自己实现
  if (PerpetualCache.class.equals(cache.getClass())) {
    for (Class decorator : decorators) {
        //装饰者模式一个个包装cache 反射构造函数 把被装饰者作为入参
      cache = newCacheDecoratorInstance(decorator, cache);
      //又要来一遍设额外属性
      setCacheProperties(cache);
    }
    //最后附加上标准的装饰者
    cache = setStandardDecorators(cache);
  } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
      //如果是custom缓存,且没有日志装饰,要加日志
    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) {
      //刷新缓存间隔,怎么刷新呢,用ScheduledCache来刷,还是装饰者模式
      cache = new ScheduledCache(cache);
      ((ScheduledCache) cache).setClearInterval(clearInterval);
    }
    if (readWrite) {
        //如果readOnly=false,可读写的缓存 会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 false。
      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);
  }
}

Original: https://www.cnblogs.com/cuzzz/p/16609744.html
Author: Cuzzz
Title: Mybatis源码3 CachingExecutor, 二级缓存,缓存的实现

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/609816/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 二分法查找案例

    public class BinarySearch { public static int binarySearch(int[] arr, int num) {int start …

    Java 2023年6月5日
    088
  • Java SPI 机制实现解耦

    SPI: Service Provider Interface Java 提供的一套用来被第三方实现或者扩展的 Api,它可以用来启动框架扩展和替换组件 架构 接口实现+策略模式+…

    Java 2023年5月29日
    086
  • 【转】京东评价系统海量数据存储设计

    概述 京东的商品评论目前已达到数十亿条,每天提供的服务调用也有数十亿次,而这些数据每年还在成倍增长,而数据存储是其中最重要的部分之一,接下来就介绍下京东评论系统的数据存储是如何设计…

    Java 2023年6月7日
    080
  • 服务监控 | 彻底搞懂Dropwizard Metrics一篇就够了

    Metrics是一个提供服务性能检测工具的Java类库,它提供了功能强大的性能指标工具库用于度量生产环境中的各关键组件性能。 度量类型 Metrics提供了以下几种基本的度量类型:…

    Java 2023年6月6日
    099
  • 我的第一个博客

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    Java 2023年6月9日
    077
  • Android开发java.lang.Class里面的native方法实现类源代码-记录一下备用

    Android SDK里面的java.lang.Class有一些native方法,比如public native T newInstance()等等,它们的实现类对应art/run…

    Java 2023年5月29日
    083
  • java——继承

    补充细节如下 访问修饰符号 ​ public        同类 同包 子类 不同包 ​ protected     同类 同包 子类 ​ 默认           同类 同包 ​…

    Java 2023年6月5日
    089
  • 前端练习(模仿菜鸟编程网站搭建)

    先看效果: 部分代码: 基本上实现了所有效果 ,目前还有一些小细节存在不足,正在改进中。。。 有需要源码的请留言,共同学习,共同进步。 Original: https://www….

    Java 2023年6月5日
    088
  • Ribbon

    Spring Cloud Ribbon是基于Netflix Ribbon实现的一套 客户端 负载均&#x8…

    Java 2023年6月8日
    073
  • 【主流技术】Mybatis Plus的理解与应用

    前言 mybatis plus是一个mybatis的增强工具,在其基础上只做增强不做改变。作为开发中常见的第三方组件,学习并应用在项目中可以节省开发时间,提高开发效率。 官方文档地…

    Java 2023年6月6日
    099
  • 深入浅出的分析 Set集合

    作者:炸鸡可乐原文出处:www.pzblog.cn 一、摘要 关于 Set 接口,在实际开发中,其实很少用到,但是如果你出去面试,它可能依然是一个绕不开的话题。 言归正传,废话咱们…

    Java 2023年6月9日
    097
  • 自己动手实现java数据结构(九) 跳表

    跳表介绍 在之前关于数据结构的博客中已经介绍过两种最基础的数据结构:基于连续内存空间的向量(线性表)和基于链式节点结构的链表。 有序的向量可以通过二分查找以logn对数复杂度完成随…

    Java 2023年6月8日
    089
  • SpringBoot整合参数校验的两种方式应用实践

    背景:SpringBoot秒杀小项目实现了两种参数校验方式1.原登录业务逻辑处理使用的正则表达式校验手机号格式输入(未使用Validation参数校验,只是简单实现)2.更改业务逻…

    Java 2023年6月8日
    078
  • 【Java学习】Java 初始化List的5种方式

    Java初始化List的5种方法 第一种 /** * 第一种方式 * 常规方式 */ @Test public void one(){ List languages = new A…

    Java 2023年5月29日
    099
  • 跟着 Guava、Spring 学习如何设计观察者模式

    文章首发在公众号(龙台的技术笔记),之后同步到掘金和个人网站:xiaomage.info 今天讲解一篇行为型设计模式,什么是行为型?行为型主要负责设计 类或对象之间的交互。工作中常…

    Java 2023年6月14日
    075
  • 动态代理大揭秘,带你彻底弄清楚动态代理!

    前言 代理模式是一种设计模式,能够使得在不修改源目标的前提下,额外扩展源目标的功能。即通过访问源目标的代理类,再由代理类去访问源目标。这样一来,要扩展功能,就无需修改源目标的代码了…

    Java 2023年6月8日
    0105
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球