Mybatis 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的实现类和缓存管理器

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

  • PerpetualCache mybatis 提供的缓存默认实现,基于hashMap

  • TransactionalCache,事务缓存,持有以下字段

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

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

四丶CachingExcutor源码学习

1.查询

public <E> List<E> 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 映射文件中添加一行: <cache/> or使用注解
//先查CacheKey,查不到再委托给实际的执行器去查
if (cache != null) {
//如果需要刷新缓存 对应flushCache的配置
flushCacheIfRequired(ms);
// 如果配置了使用 二级缓存 useCach配置
//<select ... flushCache="false" useCache="true"/>
if (ms.isUseCache() && resultHandler == null) {
//确定是存储过程的时候没有out类型的参数 如果有抛出异常
//二级缓存不支持有输出的存储过程
ensureNoOutParams(ms, parameterObject, boundSql);
//根据暂存区的cache从缓存区取值
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//没有那么调用被装饰着去查询 还会走到BaseExecutor的一级缓存
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//存到暂存区
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//没有使用二级缓存 那么被装饰者执行
return delegate.<E> 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<Object, Object> 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<? extends Cache> implementation;
//装饰器,通过配置 可以选择使用那些进行装饰增强
private List<Class<? extends Cache>> 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<? extends Cache> 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);
}
}

Mybatis源码3 CachingExecutor, 二级缓存,缓存的实现的更多相关文章

  1. mybatis源码分析之05一级缓存

    首先需要明白,mybatis的一级缓存就是指SqlSession缓存,Map缓存! 通过前面的源码分析知道mybatis框架默认使用的是DefaultSqlSession,它是由DefaultSqlS ...

  2. Mybatis源码分析之Cache一级缓存原理(四)

    之前的文章我已经基本讲解到了SqlSessionFactory.SqlSession.Excutor以及Mpper执行SQL过程,下面我来了解下myabtis的缓存, 它的缓存分为一级缓存和二级缓存, ...

  3. Mybatis 源码分析之一二级缓存

    一级缓存 其实关于 Mybatis 的一级缓存是比较抽象的,并没有什么特别的配置,都是在代码中体现出来的. 当调用 Configuration 的 newExecutor 方法来创建 executor ...

  4. mybatis源码分析(7)-----缓存Cache(一级缓存,二级缓存)

    写在前面  MyBatis 提供查询缓存,用于减轻数据库压力,提高数据库性能. MyBatis缓存分为一级缓存和二级缓存. 通过对于Executor 的设计.也可以发现MyBatis的缓存机制(采用模 ...

  5. Mybatis源码解析优秀博文

    最近阅读了许久的mybatis源码,小有所悟.同时也发现网上有许多优秀的mybatis源码讲解博文.本人打算把自己阅读过的.觉得不错的一些博文列出来.以此进一步加深对mybatis框架的理解.其实还有 ...

  6. Mybatis源码分析之Cache二级缓存原理 (五)

    一:Cache类的介绍 讲解缓存之前我们需要先了解一下Cache接口以及实现MyBatis定义了一个org.apache.ibatis.cache.Cache接口作为其Cache提供者的SPI(Ser ...

  7. mybatis源码分析之06二级缓存

    上一篇整合redis框架作为mybatis的二级缓存, 该篇从源码角度去分析mybatis是如何做到的. 通过上一篇文章知道,整合redis时需要在FemaleMapper.xml中添加如下配置 &l ...

  8. mybatis源码学习:一级缓存和二级缓存分析

    目录 零.一级缓存和二级缓存的流程 一级缓存总结 二级缓存总结 一.缓存接口Cache及其实现类 二.cache标签解析源码 三.CacheKey缓存项的key 四.二级缓存TransactionCa ...

  9. myBatis源码解析-二级缓存的实现方式

    1. 前言 前面近一个月去写自己的mybatis框架了,对mybatis源码分析止步不前,此文继续前面的文章.开始分析mybatis一,二级缓存的实现.附上自己的项目github地址:https:// ...

  10. 【MyBatis源码解析】MyBatis一二级缓存

    MyBatis缓存 我们知道,频繁的数据库操作是非常耗费性能的(主要是因为对于DB而言,数据是持久化在磁盘中的,因此查询操作需要通过IO,IO操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相 ...

随机推荐

  1. 其它——MySQL主从搭建基于docker

    文章目录 10分钟搭建MySQL主从同步(基于docker) 一 主从配置原理 二 操作步骤 2.1我们准备两台装好mysql的服务器(我在此用docker模拟了两台机器) 2.2 远程连接入主库和从 ...

  2. Oracle中的substr()函数和INSTR()函数和mysql中substring_index函数字符截取函数用法:计算BOM系数用量拼接字符串*计算值方法

    最近一直在研究计算产品BOM的成本系数,将拼接的元件用量拼接后拆分计算是个问题,后来受到大佬在mysql中截取字符串的启发在oracle中以substr和instr实现了  1.以下是我在mysql中 ...

  3. 教育法学第六章单元测试MOOC

    第六章单元测试 返回 本次得分为:100.00/100.00, 本次测试的提交时间为:2020-09-06, 如果你认为本次测试成绩不理想,你可以选择 再做一次 . 1 单选(5分) "学习 ...

  4. 嵌入式BI的精解与探索

    摘要:本文由葡萄城技术团队原创并首发.转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 前言 1996年,商业智能(BI)的概念首次浮现,随后的20多年间,商 ...

  5. Java虚拟机(JVM):第四幕:自动内存管理 - 经典垃圾收集器

    前言:如果说收集算法是内存回收的方法论,那么垃圾收集器则是内存回收的实践者.整哥Java堆 :Full GC. 1.Serial收集器:最基础.历史最悠久的收集器,这是一个单线程工作的收集器. 2.P ...

  6. 【京东开源项目】微前端框架MicroApp 1.0正式发布

    介绍 MicroApp是由京东前端团队推出的一款微前端框架,它从组件化的思维,基于类WebComponent进行微前端的渲染,旨在降低上手难度.提升工作效率.MicroApp无关技术栈,也不和业务绑定 ...

  7. 分布式应用开发的核心技术系列之——基于TCP/IP的原始消息设计

    本文由葡萄城技术团队原创并首发.转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 前言 本文的内容主要围绕以下几个部分: TCP/IP的简单介绍. 消息的介绍 ...

  8. 【Vue3响应式入门#01】Reactivity

    专栏分享:vue2源码专栏,vue3源码专栏,vue router源码专栏,玩具项目专栏,硬核推荐 欢迎各位ITer关注点赞收藏 背景 以下是柏成根据Vue3官方课程整理的响应式书面文档 - 第一节, ...

  9. .net core中你的MD5用对了吗?

    本文的项目环境为 .net 6.0 (.net 5.0 以上都支持) 在 .net 中获取字符串的 MD5 相信是非常容易的事情吧, 但是随便在网上搜一搜发现流传的版本还不少呢,比如: StringB ...

  10. 在不知带头节点地址的情况下删除和插入一个p指针指向的节点总结

    在不知带头节点地址的情况下删除和插入一个p指针指向的节点总结 (p指向的不是第一个,也不是最后一个)A->B->C *p->B 插入(在p结点之前插入q) 解析: 直接往p前插入q, ...