缓存
MyBatis共有两级缓存
- 一级缓存:在Base Executor实现
- 二级缓存:在Caching Executor实现
一级缓存命中条件
一级缓存不能打开,存储结构为Key-Value形式,底层为一个HashMap,当一级缓存命中时,Sql只会被编译处理一次
作用:当执行完一句查SQL的时候,再调用时,产生的两个对象是一致的,而且SQL只被编译和使用一次,因为第二次调用的时候,取得对象是从一级缓存中取得
命中相关条件
命中条件
- Sql和参数相同
- statementID要一样
- 方法名不一样,接口名不一样,也就是方法的全限定名称不一样都会导致StatementID不一样
- 不同的会话,执行相同的Sql的StateMentID会不一样(一级缓存被称为Session缓存)
- 使用不同的RowBounds进行分页,也会导致StatementID不一样
- 查询后清空缓存也不会命中缓存
- 方法加上注解flushCache=true
- 调用session的clearCache方法
- 当主动去调用mapper进行增删改操作的时候(这个增删改的判定是根据注解和配置形式来决定的,而不是sql决定,比如@Delete(select * …)也会被视作删除操作从而清空缓存),也会自动清空一次缓存(否则会产生数据一致性问题)
- 关闭一级缓存,修改localCacheScope改成STATEMENT(但此时不是真正关闭,嵌套查询依然会走缓存)
- 子查询不会清空缓存,因为子查询是要依赖一级缓存的
所以总的来说
运行时参数
- 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,从而导致会话是不可能同一的
怎么让其恢复,不失效?
使用手动开启事务,让其在一个会话里面
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加一层装饰),LoggingCache同样做自己的处理,然后调用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 <E> List<E> 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<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list);
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
TransactionalCache的getObject方法
走二级缓存的实现主要就是在TransactionalCache的getObject方法里面,而且这段代码也是很简洁的
@Override
public Object getObject(Object key) {
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
if (clearOnCommit) {
return null;
} else {
return object;
}
}
TransactionalCache的putObject方法
putObject方法其实就是将二级缓存中没有,将Executor查询的结果放进去暂存区中,等事务提交后再将缓存区中的东西刷新进二级缓存中
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
TransactionalCache的commit方法
commit也就是相当于事务提交了,那么就要将entriesMissedInCache和entriesToAddOnCommit的数据刷新进二级缓存中去了
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> 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();
}
|