MyBatis共有两级缓存
一级缓存不能打开,存储结构为Key-Value形式,底层为一个HashMap,当一级缓存命中时,Sql只会被编译处理一次
作用:当执行完一句查SQL的时候,再调用时,产生的两个对象是一致的,而且SQL只被编译和使用一次,因为第二次调用的时候,取得对象是从一级缓存中取得
命中相关条件
命中条件
所以总的来说
运行时参数
操作与配置
首先认识MyBatis执行SQl的一套流程
而一级缓存(LocalCache)的调用,就在Executor中,具体来说是在BaseExecutor中,(BaseExecutor执行query方法时,会先查看是否存在一级缓存,不存在会执行doQuery方法(这个方法的具体实现是在调用的BaseExecutor实例中,将SQL放入数据库里面执行后,然后填充缓存),存在则使用PerpeturalCache,一级缓存的实现是在PerpetualCache中的,不需要继续走数据库执行SQL
一级缓存具体的结构其实是一个HashMap
这是因为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的形式,但由于一级缓存是会话级别的,清空重建的频率比较大,所以不会出现缓存撑爆的现象,而二级缓存是应用级缓存,整个应用的缓存是很大的,容易出现缓存空间不足的现象,所以需要考虑使用什么容器去存储二级缓存,甚至还要考虑使用什么算法来淘汰旧的缓存从而可以使用新的缓存
假如,缓存的数据年龄太大,数据库都已经更新了,二级缓存还继续存着,这是没有意义的,所以需要使用过期清理策略,给缓存设置一个过期时间,到时间就要进行删除
二级缓存是可以跨线程访问的,所以要保证线程安全!
也是因为二级缓存是可以跨线程使用的,假如两个线程同时去获取同一个缓存,那么这个缓存就不能是同个对象,否则会出现线程安全问题,所以在取出缓存后必须经过序列化去转化为不同的对象,才能给线程去使用
MyBatis设置二级缓存,实现的就是Cache接口,也就是说MyBatis只提供了Cache接口来让外界控制和访问二级缓存
现在问题就来了,只有一个Cache接口,那就是说对应上面的那些一系列需求(溢出淘汰、过期清理、序列化等)都是通过实现该接口的实现类去做的,按照平常的写法,我们可能会对应某个需求在实现类去写一个私有方法,让后在重写接口的方法上进行顺序调用,但这有一个不好的地方就是,这样的设计并不适合扩展,假如要去扩展一个新功能,实现类里面就要进行大改动
所以,MyBatis对于上诉需求的实现,采用两个设计模式,装饰器+责任链模式
每一个功能模块都实现了Cache接口(责任链模式的前提,规范化模块的责任,比如说,一系列模块都有自己的putObject的责任),当获取了SynchronizedCache去调用方法时,会先执行自己的逻辑,然后调用LoggingCache(装饰者模式,给LoggingCache加一层装饰),LoggingCache同样做自己的处理,然后调用LRUCache,继续往下,直到最后的PerpetualCache,相当于就是在进行责任传递,完成了自己的责任后交给下一个人去实现(使用接口规范属于同一种责任),并且使用装饰者模式进行对最上层调用进行解耦!即使后面增加了新节点,只需要在最底层进行添加调用即可让最上层的Cache包括了下面所有Cache的功能,完全屏蔽了底层的复杂性(责任链+装饰者的优点)!
与一级缓存的不同之处在于
这里要注意,当开启了缓存之后,使用注解开启的缓存,在Mapper配置文件里面的SQL是用不到的,还必须在Mapper配置里面加上引用缓存空间,即(即配置文件和注解是不能相互关联的!),而缓存空间则是Mapper接口的全限定名,当然,我们也可以引用其他Mapper的缓存空间,好处就是可以共同管理,当一个Mapper的缓存空间进行清空,另外一个缓存空间也会受影响
二级缓存因为支持跨线程访问,所以实现的复杂性会比一级缓存要难
二级缓存的架构分为三个部分
每个会话都有自己的唯一事务缓存管理器(存放进SqlSession里面的CachingExecutor里面),在事务缓存管理器会有对应的暂存区(暂存区决定于会话使用了多少个Mapper,一个Mapper就是一个暂存区),而对应的暂存区指向了对应的缓存区(对应的操作就是给Mapper指向了缓存空间,而且注意,只有会话提交后,事务管理器才会将暂存区里面的查询的结果转移到缓存区,会话结束了,事务缓存管理器也会注销掉,而缓存区则会保留)
下面是MyBatis执行SqlSession时候的流程,查询时会先走二级缓存CachingExecutor,再走一级缓存BaseExecutor,一级缓存中没有,BaseExecutor就会进行查询数据库,然后将数据填充到事务管理器的暂存区,提交后,暂存区的数据转移进缓存区
下面看看二级缓存执行具体执行流程
二级缓存的CRUD都是经过CachingExecutor去做的
事务管理器,用来存储暂存区,可以看到是使用HashMap来进行存储的!key为二级缓存,而value则为事务缓存
事务缓存其实是一个二级缓存的装饰对象!
从源码上可以看到,TransactionalCache装饰了Cache(二级缓存),前面提到过,事务没有提交的数据是没有进二级缓存的,那么保存在哪里呢?其实就是保存在事务缓存里面的entriesToAddOnCommit,也是一个HashMap对象,所以真正的暂存区其实是entriesToAddOnCommit
下面是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) { //调用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方法里面,而且这段代码也是很简洁的
@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; } }
putObject方法其实就是将二级缓存中没有,将Executor查询的结果放进去暂存区中,等事务提交后再将缓存区中的东西刷新进二级缓存中
public void putObject(Object key, Object object) { //从源码上看到,只是将key value存放进了entriesToAddOnCommit中 entriesToAddOnCommit.put(key, object); }
commit也就是相当于事务提交了,那么就要将entriesMissedInCache和entriesToAddOnCommit的数据刷新进二级缓存中去了
public void commit() { //如果二级缓存需要清空 if (clearOnCommit) { //清空二级缓存 delegate.clear(); } //刷新entriesMissedInCache和entriesToAddOnCommit的数据 flushPendingEntries(); //重置事务缓存 //其实就是将clearOnCommit、entriesMissedInCache和entriesToAddOnCommit清空重置 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)) { //存进去为null(可能再取的过程中,如果二级缓存中存在但为空,可能返回特殊值,来防止缓存穿透) delegate.putObject(entry, null); } } }
private void reset() { //进行清空重置而已 clearOnCommit = false; entriesToAddOnCommit.clear(); entriesMissedInCache.clear(); }