来来来,给俏如来扎起。感谢老铁们对俏如来的支持,2021一路有你,2022我们继续加油!你的肯定是我最大的动力
博主在参加博客之星评比,点击链接 , https://bbs.csdn.net/topics/603957267 疯狂打Call!五星好评 ⭐⭐⭐⭐⭐ 感谢
前言
对于Mybatis的缓存在上一章节《吃透Mybatis源码-Mybatis执行流程》我们有提到一部分,这篇文章我们对将详细分析一下Mybatis的一级缓存和二级缓存。
一级缓存
市面上流行的ORM框架都支持缓存,不管是Hibernate还是Mybatis都支持一级缓存和二级缓存,目的是把数据缓存到JVM内存中,减少和数据库的交互来提高查询速度。同时MyBatis还可以整合三方缓存技术。
Mybatis一级缓默认开启,是SqlSession级别的,也就是说需要同一个SqlSession执行同样的SQL和参数才有可能命中缓存。如:
同一个SqlSession执行同一个SQL,发现控制台日志只执行了一次SQL记录,说明第二次查询是走缓存了。但是要注意的是,当SqlSession执行了delete,update,insert语句后,缓存会被清除。
那么一级缓存在哪儿呢?下面给大家介绍一个类。
Mybatis中提供的缓存都是Cache的实现类,但是真正实现缓存的是
PerpetualCache
,其中维护了一个
Map<Object, Object> cache = new HashMap<Object, Object>()
结构来缓存数据。其他的缓存类采用了装饰模式对PerpetualCache做增强。比如:
LruCache
在PerpetualCache 的基础上增加了最近最少使用的缓存清楚策略,当缓存到达上限时候,删除最近最少使用的缓存 (Least Recently Use)。代码如下
publicclassLruCacheimplementsCache{//对 PerpetualCache 做装饰privatefinalCache delegate;
下面对其他的缓存类做一个介绍
PerpetualCache
: 基础缓存类- LruCache : LRU 策略的缓存 当缓存到达上限时候,删除最近最少使用的缓存 (Least Recently Use),eviction=“LRU”(默 认)
- FifoCache : FIFO 策略的缓存 当缓存到达上限时候,删除最先入队的缓存,配置eviction=“FIFO”
- SoftCache WeakCache :带清理策略的缓存 通过 JVM 的软引用和弱引用来实现缓存,当 JVM 内存不足时,会自动清理掉这些缓存,基于 SoftReference 和 WeakReference
- SynchronizedCache : 同步缓存 基于 synchronized 关键字实现,解决并发问题
- ScheduledCache : 定时调度的缓存,在进行 get/put/remove/getSize 等操作前,判断 缓存时间是否超过了设置的最长缓存时间(默认是 一小时),如果是则清空缓存–即每隔一段时间清 空一次缓存
- SerializedCache :支持序列化的缓存 将对象序列化以后存到缓存中,取出时反序列化
TransactionalCache
:事务缓存,在二级缓存中使用,可一次存入多个缓存,移除多个缓存 。通过TransactionalCacheManager 中用 Map 维护对应关系。
一级缓存到底存储在哪儿?
一级缓存在SimpleExecutor 的父类 BaseExecutor 执行器中,如下
publicabstractclassBaseExecutorimplementsExecutor{privatestaticfinalLog log =LogFactory.getLog(BaseExecutor.class);protectedTransaction transaction;protectedExecutor wrapper;protectedConcurrentLinkedQueue<DeferredLoad> deferredLoads;//一级缓存protectedPerpetualCache localCache;
PerpetualCache缓存类源码如下
publicclassPerpetualCacheimplementsCache{privatefinalString id;//缓存privateMap<Object,Object> cache =newHashMap<Object,Object>();
那么一级缓存在什么时候创建的?
在 BaseExecutor 中的构造器中创建了一级缓存,而执行器Executor 是保存在SqlSession中的,也就是说当创建SqlSession的时候,就会创建 SimpleExecutor,而在SimpleExecutor的构造器中会调用BaseExecutor的构造器来创建一级缓存。见:org.apache.ibatis.executor.SimpleExecutor#SimpleExecutor
publicclassSimpleExecutorextendsBaseExecutor{//执行器构造器publicSimpleExecutor(Configuration configuration,Transaction transaction){//调用父类构造器super(configuration, transaction);}
下面是 BaseExecutor 的执行器 org.apache.ibatis.executor.BaseExecutor#BaseExecutor
publicabstractclassBaseExecutorimplementsExecutor{privatestaticfinalLog log =LogFactory.getLog(BaseExecutor.class);protectedTransaction transaction;protectedExecutor wrapper;//一级缓存protectedPerpetualCache localCache;protectedPerpetualCache localOutputParameterCache;protectedConfiguration configuration;protectedBaseExecutor(Configuration configuration,Transaction transaction){this.transaction = transaction;this.deferredLoads =newConcurrentLinkedQueue<DeferredLoad>();//创建一级缓存this.localCache =newPerpetualCache("LocalCache");this.localOutputParameterCache =newPerpetualCache("LocalOutputParameterCache");this.closed =false;this.configuration = configuration;this.wrapper =this;}
一级缓存怎么存储的?
一级缓存是在执行查询的时候会先走二级缓存,二级缓存么有就会走一级缓存,以及缓存没有就会走数据库查询,然后放入一级缓存和二级缓存。我们来看一下源码流程 ,见:org.apache.ibatis.executor.CachingExecutor#query
@Overridepublic<E>List<E>query(MappedStatement ms,Object parameterObject,RowBounds rowBounds,ResultHandler resultHandler)throwsSQLException{BoundSql boundSql = ms.getBoundSql(parameterObject);//构建缓存的KeyCacheKey key =createCacheKey(ms, parameterObject, rowBounds, boundSql);//执行查询returnquery(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
这里在尝试构建Cachekey ,cachekey时由:MappedStatement的id(如:cn.xx.xx.xxMapper.selectByid) ,分页,Sql,参数值一起构建而成的,一级二级缓存都是如此。
@Overridepublic<E>List<E>query(MappedStatement ms,Object parameterObject,RowBounds rowBounds,ResultHandler resultHandler,CacheKey key,BoundSql boundSql)throwsSQLException{//开启了二级缓存才会存在Cache 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.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);//写入二级缓存
tcm.putObject(cache, key, list);// issue #578 and #116}return list;}}return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
这里我们看到,在执行org.apache.ibatis.executor.CachingExecutor#query 查询的时候会先走二级缓存,二级缓存没有会继续调用 org.apache.ibatis.executor.BaseExecutor#query 查询,而BaseExecutor#query会尝试先走一级缓存
public<E>List<E>query(MappedStatement ms,Object parameter,RowBounds rowBounds,ResultHandler resultHandler,CacheKey key,BoundSql boundSql)throwsSQLException{ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());if(closed){thrownewExecutorException("Executor was closed.");}if(queryStack ==0&& ms.isFlushCacheRequired()){clearLocalCache();}List<E> list;try{
queryStack++;//【重要】走一级缓存获取数据
list = resultHandler ==null?(List<E>) localCache.getObject(key):null;if(list !=null){handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);}else{//如果一级缓存中没有,走数据库查询数据
list =queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}}finally{
queryStack--;}if(queryStack ==0){for(DeferredLoad deferredLoad : deferredLoads){
deferredLoad.load();}// issue #601
deferredLoads.clear();if(configuration.getLocalCacheScope()==LocalCacheScope.STATEMENT){// issue #482clearLocalCache();}}return list;}
上面代码会先走一级缓存拿数据,如果一级缓存没有,就走数据库获取数据,然后加入一级缓存org.apache.ibatis.executor.BaseExecutor#queryFromDatabase
private<E>List<E>queryFromDatabase(MappedStatement ms,Object parameter,RowBounds rowBounds,ResultHandler resultHandler,CacheKey key,BoundSql boundSql)throwsSQLException{List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);try{//走数据库查询数据
list =doQuery(ms, parameter, rowBounds, resultHandler, boundSql);}finally{
localCache.removeObject(key);}//把数据写入一级缓存
localCache.putObject(key, list);if(ms.getStatementType()==StatementType.CALLABLE){
localOutputParameterCache.putObject(key, parameter);}return list;}
到这里我们就看到了一级缓存和二级缓存的执行流程,注意的是:先执行二级缓存再执行一级缓存。
这里画一个一级缓存的图
二级缓存
第一步:二级缓存需要在mybatis-config.xml 配置中开启,如下
<settingname="cacheEnabled"value="true"/>
当然其实该配置默认是开启的,也就是默认会使用 CachingExecutor 装饰基本的执行器。
第二步骤:需要在mapper.xml中配置 < cache/>如下
<mappernamespace="cn.whale.mapper.StudentMapper"><cachetype="org.apache.ibatis.cache.impl.PerpetualCache"size="1024"eviction="LRU"flushInterval="120000"readOnly="false"/>
...省略...
解释一下上面的配置,首先
<cache/>
是在某个mapper.xml中指定的,也就是说
二级缓存作用于当前的namespace
.
- type : 代表的是使用什么类型的缓存,只要是实现了 Cache 接口的实现类都可以
- size :缓存的个数,默认是1024 个对象
- eviction : 缓存剔除策略 ,LRU – 最近最少使用的:移除最长时间不被使用的对象(默认);FIFO – 先进先出:按对象进入缓存的顺序来移除它们 ;SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象;WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象
- flushInterval :定时自动清空缓存间隔 自动刷新时间,单位 ms,未配置时只有调用时刷新
- readOnly :缓存时候只读
- blocking :是否使用可重入锁实现 缓存的并发控制 true,会使用 BlockingCache 对 Cache 进行装饰 默认 false
Mapper.xml 配置了之后,select()会被缓存。update()、delete()、insert() 会刷新缓存,下面是测试案例
可以看到,这里使用了2个SqlSesion 2次执行了相同的SQL,参数相同,看控制台日志只执行了一次SQL,说明是命中的二级缓存。因为满足条件:同一个 namespace下的相同的SQL被执行,尽管使用的SqlSession不是同一个。
但是你可能注意到一个细节,就是
session.commit()
为什么要提交事务呢?这就要说到二级缓存的存储结构了,如果不执行commit是不会写入二级缓存的。在
CachingExecutor
中有一个属性
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
看名字肯能够看出二级缓存和事务有关系。结构如下
publicclassCachingExecutorimplementsExecutor{privatefinalExecutor delegate;//二级缓存,通过TransactionalCacheManager来管理privatefinalTransactionalCacheManager tcm =newTransactionalCacheManager();
TransactionalCacheManager 中维护了一个 HashMap<Cache, TransactionalCache>()
publicclassTransactionalCacheManager{//二级缓存的HashMapprivatefinalMap<Cache,TransactionalCache> transactionalCaches =newHashMap<Cache,TransactionalCache>();
在TransactionCache中维护了一个
Map<Object, Object> entriesToAddOnCommit;
publicclassTransactionalCacheimplementsCache{privatestaticfinalLog log =LogFactory.getLog(TransactionalCache.class);privatefinalCache delegate;privateboolean clearOnCommit;//二级缓存临时存储privatefinalMap<Object,Object> entriesToAddOnCommit;...省略...//写入二级缓存@OverridepublicvoidputObject(Object key,Object object){
entriesToAddOnCommit.put(key, object);}
当执行查询的时候,从数据库查询出来数据回写入TransactionalCache的entriesToAddOnCommit中,我们来看一下二级缓存写入的流程,见:org.apache.ibatis.executor.CachingExecutor#query
@Overridepublic<E>List<E>query(MappedStatement ms,Object parameterObject,RowBounds rowBounds,ResultHandler resultHandler,CacheKey key,BoundSql boundSql)throwsSQLException{//如果mapper.xml配置了 <cache/> 就会创建 CacheCache 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.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);//写入二级缓存
tcm.putObject(cache, key, list);// issue #578 and #116}return list;}}return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
如果mapper.xml配置了 就会创建 Cache,Cache不为null,才会走到二级缓存的流程,此时代码来到org.apache.ibatis.cache.TransactionalCacheManager#putObject
publicclassTransactionalCacheManager{//存储二级缓存privatefinalMap<Cache,TransactionalCache> transactionalCaches =newHashMap<Cache,TransactionalCache>();publicvoidputObject(Cache cache,CacheKey key,Object value){//通过cache为key拿到 TransactionalCache ,把数据put进去getTransactionalCache(cache).putObject(key, value);}
存储数据的是TransactionalCache ,见org.apache.ibatis.cache.decorators.TransactionalCache#putObject
publicclassTransactionalCacheimplementsCache{privatestaticfinalLog log =LogFactory.getLog(TransactionalCache.class);//正在的二级缓存存储位置privatefinalCache delegate;privateboolean clearOnCommit;//临时的二级缓存存储位置privatefinalMap<Object,Object> entriesToAddOnCommit;@OverridepublicvoidputObject(Object key,Object object){
entriesToAddOnCommit.put(key, object);}
我们看到,数据写到了 TransactionalCache#entriesToAddOnCommit 一个Map中。只有在执行commit的时候数据才会真正写入二级缓存。
我们来看下SqlSession.commit方法是如何触发二级缓存真正的写入的,见:org.apache.ibatis.session.defaults.DefaultSqlSession#commit()
@Overridepublicvoidcommit(){commit(false);}@Overridepublicvoidcommit(boolean force){try{//调用执行器提交事务
executor.commit(isCommitOrRollbackRequired(force));
dirty =false;}catch(Exception e){throwExceptionFactory.wrapException("Error committing transaction. Cause: "+ e, e);}finally{ErrorContext.instance().reset();}}
代码来到org.apache.ibatis.executor.CachingExecutor#commit
@Overridepublicvoidcommit(boolean required)throwsSQLException{//提交事务
delegate.commit(required);//调用org.apache.ibatis.cache.TransactionalCacheManager#commit提交事务
tcm.commit();}
代码来到org.apache.ibatis.cache.TransactionalCacheManager#commit
publicvoidcommit(){for(TransactionalCache txCache : transactionalCaches.values()){//调用 TransactionalCache#commit
txCache.commit();}}
代码来到org.apache.ibatis.cache.decorators.TransactionalCache#commit
publicclassTransactionalCacheimplementsCache{privatestaticfinalLog log =LogFactory.getLog(TransactionalCache.class);//真正的二级缓存存储位置,本质是一个 PerpetualCacheprivatefinalCache delegate;//临时存储二级缓存privatefinalMap<Object,Object> entriesToAddOnCommit;publicvoidcommit(){if(clearOnCommit){
delegate.clear();}//这里在写入缓存,保存到TransactionalCache中的delegate字段,本质是一个PerpetualCacheflushPendingEntries();//把entriesToAddOnCommit清除掉reset();}privatevoidflushPendingEntries(){for(Map.Entry<Object,Object> entry : entriesToAddOnCommit.entrySet()){//从entriesToAddOnCommit中拿到临时的缓存数据,写入缓存,最终会写入PerpetualCache#cache字段中
delegate.putObject(entry.getKey(), entry.getValue());}for(Object entry : entriesMissedInCache){if(!entriesToAddOnCommit.containsKey(entry)){
delegate.putObject(entry,null);}}}privatevoidreset(){
clearOnCommit =false;//清除entriesToAddOnCommit
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();}
所以我们总结一下二级缓存的写入流程,二级缓存通过
TransactionalCacheManager
中的一个
Map<Cache, TransactionalCache>
管理的,当执行query查询处数据的时候,会把数据写入
TransactionalCache
中的
Map<Object, Object> entriesToAddOnCommit
中临时存储。当执行commit的时候才会把entriesToAddOnCommit中的数据写入
TransactionalCache
中的
Cache delegate
,其本质和一级缓存一样,也是一个
PerpetualCache
。
当我们做第二次query的时候会尝试通过 TransactionalCacheManager#getObject 从二级缓存获取数据
publicclassTransactionalCacheManager{privatefinalMap<Cache,TransactionalCache> transactionalCaches =newHashMap<Cache,TransactionalCache>();//获取二级缓存publicObjectgetObject(Cache cache,CacheKey key){returngetTransactionalCache(cache).getObject(key);}
然后会从 TransactionalCache中的delegate中获取缓存
publicclassTransactionalCacheimplementsCache{privatestaticfinalLog log =LogFactory.getLog(TransactionalCache.class);//二级缓存privatefinalCache delegate;...省略...@OverridepublicObjectgetObject(Object key){// issue #116//从二级缓存获取数据Object object = delegate.getObject(key);if(object ==null){
entriesMissedInCache.add(key);}// issue #146if(clearOnCommit){returnnull;}else{return object;}}
所以记得,二级缓存一定要commit才会起作用。下面花了一个一级缓存和二级缓存的结构图
三方缓存框架
除了使用Mybatis自带的缓存,也可以使用第三方缓存方式,比如:比如 ehcache 和 redis 下面以Redis为例 ,首先导入mybatis整合redis的依赖
<dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-redis</artifactId><version>1.0.0-beta2</version></dependency>
第二步骤:在mapper.xml配置缓存
<cachetype="org.mybatis.caches.redis.RedisCache"eviction="FIFO"flushInterval="60000"size="512"readOnly="true"/>
这里type使用了RedisCache,RedisCache也是实现了Cache接口的,接着我们需要配置Redis的链接属性,默认RedisCache类会读取名字为 : redis.properties 的配置文件
host=127.0.0.1
password=123456
port=6379
connectionTimeout=5000
soTimeout=5000
database=0
再次执行测试代码,查看Redis效果如下
博主在参加博客之星评比,点击链接 , https://bbs.csdn.net/topics/603957267 疯狂打Call!五星好评 ⭐⭐⭐⭐⭐ 感谢
版权归原作者 墨家巨子@俏如来 所有, 如有侵权,请联系我们删除。