上篇文章介绍了Mybatis执行Mapper的流程,其中涉及到Executor部分的代码,我们以insert()方法为例进行了讲解,本篇文章我们以Executor的query()方法为切入口,探究Mybatis中的一二级缓存相关实现。
一级缓存
前面文章提到Executor的通用逻辑由BaseExecutor实现,接下来介绍BaseExecutor的query()方法。看看一级缓存的实现思路。
可以看到在query()中,先从缓存localCache中获取结果,获取不到再从数据库中进行查询。接下来看看localCache具体的数据结构。
如图所示,PerpetualCache实现了Cache接口,通过一个HashMap实例存放缓存,这是Mybatis缓存中最简单基础的实现类,其余实现Cache接口的还有如下类:
这些类都是对PerpetualCache的一些增强类。内部持有Cache的delegte,其作用各不相同,例如BlockingCache是阻塞版本的缓存装饰器,能够保证同一时间只有一个线程到缓存中查找指定的Key对应的数据。
FifoCache是阻塞版本的缓存装饰器,FifoCache内部有一个维护具有长度限制的Key键值链表(LinkedList实例)和一个被装饰的缓存对象,Key值链表主要是维护Key的FIFO顺序,而缓存存储和获取则交给被装饰的缓存对象来完成。
接下来回到BaseExecutor中,PerpetualCache的初始化是由BaseExecutor的构造函数完成。
可以在BaseExecutor的query()方法中看到,获取缓存时的key为CacheKey对象。那么CacheKey由哪些因素组成?我们看看CacheKey的构建。在BaseExecutor的query()重载方法中构造了CacheKey。
其CreateCacheKey()的代码如下:
可以看到CacheKey和Mapper的Id,即Mapper命名空间与<select|update|insert|delete>标签的Id组成的全局限定名、查询结果的偏移量及查询的条数、具体的SQL语句及SQL语句中需要传递的所有参数、MyBatis主配置文件中,通过environment标签配置的环境信息对应的Id属性值这些信息有关。换句话说,只有两次查询时上面的这些信息完全相同,才认为两次查询的是同一条sql。缓存才会生效。
上述逻辑的一个漏洞在于两次sql只要所有信息一样就会认为是同一条sql,会走缓存查询,那么在两次Sql间执行了update操作影响到了sql的查询结果,这时候再从缓存取岂不是取出一个已经过时的数据。
MyBatis的思路是在每次执行update操作之前都会清理掉缓存,如下所示:
其clearLocalCache()代码如下,每次执行update前都会将缓存清空。
。
继续回到BaseExecutor的query()方法中,可以看到该方法最后有一个清理缓存的操作。
而清缓存的前提是configuration中的localCacheScope为STATEMENT。localCacheScope有两种取值,SESSION和STATEMENT。
也就是说,只有一级缓存的取值为SESSION时,才对前一次执行的query()有缓存,否则缓存只对当前Sql有效。 localCacheScope的默认值为SESSION,具体参考配置_MyBatis中文网
在单机环境下,该配置并无不妥,但是当在分布式环境下,要将其设置为STATEMENT,避免其他应用节点执行SQL更新语句后,本节点缓存得不到刷新而导致的数据一致性问题。
可以从Mybatis的源码中清晰看出一级缓存无法关闭,只能选择缓存的Scope。并且经常大家说Mybatis的一级缓存是SqlSession级别的,从前面系列文章及一级缓存源码分析可以看出,实际上是针对Executor的缓存。
二级缓存
前面文章提到Executor有一个实现类CachingExecutor,该类是其他几个实现类的装饰类。在CachingExecutor中持有Executor的delagete完成基本功能,TransactionManager用来管理二级缓存。
TransactionManager的具体实现如下:
其内部持有一个HashMap的实例,key是Cache类型,value是TransactionCache。
接下来回到CachingExecutor的quey()方法,看看二级缓存的存取。
如图所示,先从tmc中获取对应的缓存,如果没有则添加。观察tmc的getObject()方法中的参数Cache,可以看到该对象是从MappedStatement中获取的,那么是Cache是什么时候初始化的尼。 XMLMapperBuilder在解析Mapper配置时会调用cacheElement()方法解析cache标签,cacheElement()方法代码如下:
useNewCache()的具体实现为
接下来看看二级缓存的刷新,可以看到CachingExecutor的update()方法中flushCacheIfRequired()方法,如下所示
要根据MappedStatement对象的flushCacheRequired属性决定要不要清理缓存。而flushCacheRequired来源于Mybatis的Sql中的flushCache。
其中select的sql默认flushCache为false,其他属性sql的flashCache默认为true。
Mybatis的二级缓存由主配置文件中的cacheEnabled属性开启。开启后在Configuration中实例化Executor对象时创建对应的CachingExecutor对象。如下所示:
总结
MyBatis一级缓存是SqlSession级别的缓存,默认就是开启的,而且无法关闭;二级缓存需要在MyBatis主配置文件中通过设置cacheEnabled参数值来开启。
一级缓存是在Executor中实现的。MyBatis的Executor组件有3种不同的实现,分别为SimpleExecutor、ReuseExecutor和BatchExecutor。这些类都继承自BaseExecutor,在BaseExecutor类的query()方法中,首先从缓存中获取查询结果,如果获取不到,则从数据库中查询结果,然后将查询结果缓存起来。
MyBatis的二级缓存则是通过装饰器模式实现的,当通过cacheEnabled参数开启了二级缓存,MyBatis框架会使用CachingExecutor对SimpleExecutor、ReuseExecutor或者BatchExecutor进行装饰,当执行查询操作时,对查询结果进行缓存,执行更新操作时则更新二级缓存。