MyBatis(二)缓存,kafka面试题1001kafka面试题

本文深入探讨了MyBatis的一级缓存与二级缓存,解析了源码,总结了一级缓存的结构、生效条件及失效情况,并详细介绍了二级缓存的定义、结构、策略以及在Spring集成后的失效问题。通过对一级缓存的执行流程分析,揭示了其基于会话的工作原理,而二级缓存作为应用级缓存,具备跨线程使用的特性,通过装饰器和责任链模式实现了高命中率。
摘要由CSDN通过智能技术生成

所以总的来说

运行时参数

  • sql和参数相同

  • 相同的statementID,即方法的全限定名称一样

  • sqlSession一样(会话级缓存)

  • RowBounds分页也一样

操作与配置

  • 不清空缓存(提交、回滚操作都会清空缓存,还有手动清空)

  • 不配置flushCache=true

  • 不执行update语句,即不执行增删改语句

  • 启用一级缓存,也就是缓存的作用域不改为STATEMENT

一级缓存源码解析

首先认识MyBatis执行SQl的一套流程

  1. 动态代理Mapper

  2. 创建SqlSession

  3. SqlSession中调用Executor

  4. Executor调用StatementHandler去处理SQL

  5. 在数据库执行SQL

而一级缓存(LocalCache)的调用,就在Executor中,具体来说是在BaseExecutor中,(BaseExecutor执行query方法时,会先查看是否存在一级缓存,不存在会执行doQuery方法(这个方法的具体实现是在调用的BaseExecutor实例中,将SQL放入数据库里面执行后,然后填充缓存),存在则使用PerpeturalCache,一级缓存的实现是在PerpetualCache中的,不需要继续走数据库执行SQL

一级缓存结构

一级缓存具体的结构其实是一个HashMap

  • Key:由SQL、Session、分页条件、参数等一系列东西组成的

  • Value:就是缓存的查出来的结果

一级缓存总结

  1. 与会话相关

  2. 参数条件相关

  3. 提交、修改会清空缓存

在这里插入图片描述

一级缓存失效情况
Spring集成MyBatis一级缓存失效?

这是因为Spring集成MyBatis会导致每次执行SQL都是一次会话(没有手动去配置事务),无论SQL是否一样,也就是违反了第一条,规定到底是每一个执行的SQL使用的执行器不一样,每次都会新建一个执行器Executor,从而导致会话是不可能同一的

怎么让其恢复,不失效?

使用手动开启事务,让其在一个会话里面

Mapper


在Spring里面,Mapper里面只有一个SQL,是怎样去执行的呢?

使用的是动态代理(进行拦截操作),从IOC容器里面获取的Mapper其实已经装配了SqlSessionTemplate,而其又动态代理装配了SqlSessionInterceptor、最终SqlSessionInterceptor调用SqlSessionFactory(SqlSessionFactory其实就是MyBatis)构造真正的SqlSession会话,使用会话执行Sql

这一套过程,从SqlSessionFactory开始才是关于MyBatis的,而前面的Mapper被SqiSessionTemplate动态代理的,此时创建了Mapper的实例才可以进行被调用里面的方法,而SqlSessionTemplate又被SqlSessionInterceptor动态代理了

在这里插入图片描述

二级缓存


一级缓存为会话级缓存,而二级缓存为应用级缓存

二级缓存有什么用?与一级缓存有什么不同?

二级缓存的定义

二级缓存为应用级缓存,针对不是一个会话了,而是整个项目而言,对于整个项目,每一次请求进来都是一个新线程,所以二级缓存是可以跨线程使用的,因此,二级缓存会拥有更高的命中率(因为一级缓存只要会话关闭了,就清空了,且不可以跨线程使用),适合缓存一些修改比较少的数据

二级缓存的结构

前面提到过,一级缓存的结构是一个HashMap去存储KeyValue的形式,但由于一级缓存是会话级别的,清空重建的频率比较大,所以不会出现缓存撑爆的现象,而二级缓存是应用级缓存,整个应用的缓存是很大的,容易出现缓存空间不足的现象,所以需要考虑使用什么容器去存储二级缓存,甚至还要考虑使用什么算法来淘汰旧的缓存从而可以使用新的缓存

溢出淘汰策略
  • FIFO:先进先出,即先淘汰出年龄大的数据

  • LRU:最近最少使用,即先淘汰出使用最少的数据(MyBatis默认的溢出淘汰策略就是LRU)

过期清理策略

假如,缓存的数据年龄太大,数据库都已经更新了,二级缓存还继续存着,这是没有意义的,所以需要使用过期清理策略,给缓存设置一个过期时间,到时间就要进行删除

线程安全

二级缓存是可以跨线程访问的,所以要保证线程安全!

序列化

也是因为二级缓存是可以跨线程使用的,假如两个线程同时去获取同一个缓存,那么这个缓存就不能是同个对象,否则会出现线程安全问题,所以在取出缓存后必须经过序列化去转化为不同的对象,才能给线程去使用

Cache接口

MyBatis设置二级缓存,实现的就是Cache接口,也就是说MyBatis只提供了Cache接口来让外界控制和访问二级缓存

现在问题就来了,只有一个Cache接口,那就是说对应上面的那些一系列需求(溢出淘汰、过期清理、序列化等)都是通过实现该接口的实现类去做的,按照平常的写法,我们可能会对应某个需求在实现类去写一个私有方法,让后在重写接口的方法上进行顺序调用,但这有一个不好的地方就是,这样的设计并不适合扩展,假如要去扩展一个新功能,实现类里面就要进行大改动

所以,MyBatis对于上诉需求的实现,采用两个设计模式,装饰器+责任链模式

在这里插入图片描述

每一个功能模块都实现了Cache接口(责任链模式的前提,规范化模块的责任,比如说,一系列模块都有自己的putObject的责任),当获取了SynchronizedCache去调用方法时,会先执行自己的逻辑,然后调用LoggingCache(装饰者模式,给LoggingCache加一层装饰),Logging

【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】

开源分享完整内容戳这里

Cache同样做自己的处理,然后调用LRUCache,继续往下,直到最后的PerpetualCache,相当于就是在进行责任传递,完成了自己的责任后交给下一个人去实现(使用接口规范属于同一种责任),并且使用装饰者模式进行对最上层调用进行解耦!即使后面增加了新节点,只需要在最底层进行添加调用即可让最上层的Cache包括了下面所有Cache的功能,完全屏蔽了底层的复杂性(责任链+装饰者的优点)!

二级缓存命中条件
  • 会话必须提交(手动提交、自动提交是不行的,因为二级缓存是跨线程使用的,假如事务没有提交被其他线程访问到,会出现脏读)

  • SQL语句、参数要相同

  • 分页条件相同

  • 相同的StatementID,也就是同一个Mapper里面的方法

与一级缓存的不同之处在于

  • 一级缓存还必须限制在同一个会话

  • 一级缓存不需要会话提交,反而会话提交后,一级缓存还会消失(新建的会话不能访问之前的一级缓存)

二级缓存的配置

在这里插入图片描述

这里要注意,当开启了缓存之后,使用注解开启的缓存,在Mapper配置文件里面的SQL是用不到的,还必须在Mapper配置里面加上引用缓存空间,即(即配置文件和注解是不能相互关联的!),而缓存空间则是Mapper接口的全限定名,当然,我们也可以引用其他Mapper的缓存空间,好处就是可以共同管理,当一个Mapper的缓存空间进行清空,另外一个缓存空间也会受影响

二级缓存架构

二级缓存因为支持跨线程访问,所以实现的复杂性会比一级缓存要难

二级缓存的架构分为三个部分

  • 会话

  • 事务缓存管理器

  • 缓存区

每个会话都有自己的唯一事务缓存管理器(存放进SqlSession里面的CachingExecutor里面),在事务缓存管理器会有对应的暂存区(暂存区决定于会话使用了多少个Mapper,一个Mapper就是一个暂存区),而对应的暂存区指向了对应的缓存区(对应的操作就是给Mapper指向了缓存空间,而且注意,只有会话提交后,事务管理器才会将暂存区里面的查询的结果转移到缓存区,会话结束了,事务缓存管理器也会注销掉,而缓存区则会保留

在这里插入图片描述

二级缓存执行流程

下面是MyBatis执行SqlSession时候的流程,查询时会先走二级缓存CachingExecutor,再走一级缓存BaseExecutor,一级缓存中没有,BaseExecutor就会进行查询数据库,然后将数据填充到事务管理器的暂存区,提交后,暂存区的数据转移进缓存区

在这里插入图片描述

下面看看二级缓存执行具体执行流程

在这里插入图片描述

  • 查询时,如果二级缓存找不到,后面经过一级缓存去找的时候,会将数据存进事务缓存管理器的暂存区,如果二级缓存找到,直接从二级缓存中取

  • 修改时,会先将会话的事务缓存管理器的缓存区给清理(修改的操作采用标记清空法,表明缓冲区被清空了,也就是说同一个会话的SQL, update语句后面的select经过自己的事务缓存管理器时得到的数据为Null, 此时交由Executor去查,再重新放入二级缓存,避免了)

  • 无论查询、修改操作,都只有在提交事务时,才会进行更新二级缓存

二级缓存源码

二级缓存的CRUD都是经过CachingExecutor去做的

在这里插入图片描述

  • delegate:装饰者模式,被装饰的Executor

  • tcm:事务缓存管理器,通过事务缓存管理器

TransactionalCache

在这里插入图片描述

事务管理器,用来存储暂存区,可以看到是使用HashMap来进行存储的!key为二级缓存,而value则为事务缓存

事务缓存其实是一个二级缓存的装饰对象!

在这里插入图片描述

从源码上可以看到,TransactionalCache装饰了Cache(二级缓存),前面提到过,事务没有提交的数据是没有进二级缓存的,那么保存在哪里呢?其实就是保存在事务缓存里面的entriesToAddOnCommit,也是一个HashMap对象,所以真正的暂存区其实是entriesToAddOnCommit

CachingExecutor的query方法

下面是query的源码

@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) {

//调用delegate,也就是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);

}

在这里插入图片描述

TransactionalCache的getObject方法

走二级缓存的实现主要就是在TransactionalCache的getObject方法里面,而且这段代码也是很简洁的

@Override

public Object getObject(Object key) {

// issue #116

//这一步就是调用二级缓存去获取

Object object = delegate.getObject(key);

//如果二级缓存中没有

if (object == null) {

//将未命中的缓存记录在entriesMissedInCache中

//防止缓存穿透(数据库、缓存都没有数据)

//entriesMissedInCache之所可以防止缓存穿透

//因为其里面的键值对也会被刷新进二级缓存中

entriesMissedInCache.add(key);

}

// issue #146

// clearOnCommit为true时,表示这个事务缓存的二级缓存需要被清空!

//也就是当前事务可能执行了修改动作,要对这个事务缓存的二级缓存进行清空

if (clearOnCommit) {

//因为缓存区要被清空,所以返回空!

//而且这里返回空只针对当前事务!

//因为对于其他事务来说,当前事务未提交所以是不可见的,所以不必对其他事务关闭二级缓存

//但对于当前自己事务来说,自己的修改是可见的,所以要关闭二级缓存

//让当前事务可以查数据库知道自己执行的动作(只有DB记录着当前事务的动作)

return null;

} else {

//如果二级缓存不清空,返回二级缓存中的值

//即使是空也返回

return object;

}

}

TransactionalCache的putObject方法

putObject方法其实就是将二级缓存中没有,将Executor查询的结果放进去暂存区中,等事务提交后再将缓存区中的东西刷新进二级缓存中

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值