所以总的来说
运行时参数
-
sql和参数相同
-
相同的statementID,即方法的全限定名称一样
-
sqlSession一样(会话级缓存)
-
RowBounds分页也一样
操作与配置
-
不清空缓存(提交、回滚操作都会清空缓存,还有手动清空)
-
不配置flushCache=true
-
不执行update语句,即不执行增删改语句
-
启用一级缓存,也就是缓存的作用域不改为STATEMENT
一级缓存源码解析
首先认识MyBatis执行SQl的一套流程
-
动态代理Mapper
-
创建SqlSession
-
SqlSession中调用Executor
-
Executor调用StatementHandler去处理SQL
-
在数据库执行SQL
而一级缓存(LocalCache)的调用,就在Executor中,具体来说是在BaseExecutor中,(BaseExecutor执行query方法时,会先查看是否存在一级缓存,不存在会执行doQuery方法(这个方法的具体实现是在调用的BaseExecutor实例中,将SQL放入数据库里面执行后,然后填充缓存),存在则使用PerpeturalCache,一级缓存的实现是在PerpetualCache中的,不需要继续走数据库执行SQL
一级缓存结构
一级缓存具体的结构其实是一个HashMap
-
Key:由SQL、Session、分页条件、参数等一系列东西组成的
-
Value:就是缓存的查出来的结果
一级缓存总结
-
与会话相关
-
参数条件相关
-
提交、修改会清空缓存
一级缓存失效情况
Spring集成MyBatis一级缓存失效?
这是因为Spring集成MyBatis会导致每次执行SQL都是一次会话(没有手动去配置事务),无论SQL是否一样,也就是违反了第一条,规定到底是每一个执行的SQL使用的执行器不一样,每次都会新建一个执行器Executor,从而导致会话是不可能同一的
怎么让其恢复,不失效?
使用手动开启事务,让其在一个会话里面
在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查询的结果放进去暂存区中,等事务提交后再将缓存区中的东西刷新进二级缓存中