MyBatis一二级缓存
MyBatis缓存
我们知道,频繁的数据库操作是非常耗费性能的(主要是因为对于DB而言,数据是持久化在磁盘中的,因此查询操作需要通过IO,IO操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相同的查询语句,完全可以把查询结果存储起来,下次查询同样的内容的时候直接从内存中获取数据即可,这样在某些场景下可以大大提升查询效率。
MyBatis的缓存分为两种:
一级缓存,一级缓存是SqlSession级别的缓存,对于相同的查询,会从缓存中返回结果而不是查询数据库
二级缓存,二级缓存是Mapper级别的缓存,定义在Mapper文件的
下面来详细看一下MyBatis的一二级缓存。
MyBatis一级缓存工作流程
接着看一下MyBatis一级缓存工作流程。前面说了,MyBatis的一级缓存是SqlSession级别的缓存,当openSession()的方法运行完毕或者主动调用了SqlSession的close方法,SqlSession就被回收了,一级缓存与此同时也一起被回收掉了。前面的文章有说过,在MyBatis中,无论selectOne还是selectList方法,最终都被转换为了selectList方法来执行,那么看一下SqlSession的selectList方法的实现:
publicList selectList(Stringstatement,Objectparameter,RowBoundsrowBounds){ try{ MappedStatementms=configuration.getMappedStatement(statement); returnexecutor.query(ms,wrapCollection(parameter),rowBounds,Executor.NO_RESULT_HANDLER); }catch(Exceptione){ throwExceptionFactory.wrapException("Errorqueryingdatabase.Cause:"+e,e); }finally{ ErrorContext.instance().reset(); } }
继续跟踪第4行的代码,到BaseExeccutor的query方法:
publicList query(MappedStatementms,Objectparameter,RowBoundsrowBounds,ResultHandlerresultHandler)throwsSQLException{ BoundSqlboundSql=ms.getBoundSql(parameter); CacheKeykey=createCacheKey(ms,parameter,rowBounds,boundSql); returnquery(ms,parameter,rowBounds,resultHandler,key,boundSql); }
第3行构建缓存条件CacheKey,这里涉及到怎么样条件算是和上一次查询是同一个条件的一个问题,因为同一个条件就可以返回上一次的结果回去,这部分代码留在下一部分分析。
接着看第4行的query方法的实现,代码位于CachingExecutor中:
publicList query(MappedStatementms,ObjectparameterObject,RowBoundsrowBounds,ResultHandlerresultHandler,CacheKeykey,BoundSqlboundSql) throwsSQLException{ Cachecache=ms.getCache(); if(cache!=null){ flushCacheIfRequired(ms); if(ms.isUseCache()&&resultHandler==null){ ensureNoOutParams(ms,parameterObject,boundSql); @SuppressWarnings("unchecked") List list=(List )tcm.getObject(cache,key); if(list==null){ list=delegate. query(ms,parameterObject,rowBounds,resultHandler,key,boundSql); tcm.putObject(cache,key,list);//issue#578and#116 } returnlist; } } returndelegate. query(ms,parameterObject,rowBounds,resultHandler,key,boundSql); }
第3行~第16行的代码先不管,继续跟第17行的query方法,代码位于BaseExecutor中:
publicList query(MappedStatementms,Objectparameter,RowBoundsrowBounds,ResultHandlerresultHandler,CacheKeykey,BoundSqlboundSql)throwsSQLException{ ErrorContext.instance().resource(ms.getResource()).activity("executingaquery").object(ms.getId()); if(closed){ thrownewExecutorException("Executorwasclosed."); } if(queryStack==0&&ms.isFlushCacheRequired()){ clearLocalCache(); } List list; try{ queryStack++; list=resultHandler==null?(List )localCache.getObject(key):null; if(list!=null){ handleLocallyCachedOutputParameters(ms,key,parameter,boundSql); }else{ list=queryFromDatabase(ms,parameter,rowBounds,resultHandler,key,boundSql); } }finally{ queryStack--; } ... }
看12行,query的时候会尝试从localCache中去获取查询结果,如果获取到的查询结果为null,那么执行16行的代码从DB中捞数据,捞完之后会把CacheKey作为key,把查询结果作为value放到localCache中。
MyBatis一级缓存存储流程看完了,接着我们从这段代码中可以得到三个结论:
MyBatis的一级缓存是SqlSession级别的,但是它并不定义在SqlSessio接口的实现类DefaultSqlSession中,而是定义在DefaultSqlSession的成员变量Executor中,Executor是在openSession的时候被实例化出来的,它的默认实现为SimpleExecutor
MyBatis中的一级缓存,与有没有配置无关,只要SqlSession存在,MyBastis一级缓存就存在,localCache的类型是PerpetualCache,它其实很简单,一个id属性+一个HashMap属性而已,id是一个名为"localCache"的字符串,HashMap用于存储数据,Key为CacheKey,Value为查询结果
MyBatis的一级缓存查询的时候默认都是会先尝试从一级缓存中获取数据的,但是我们看第6行的代码做了一个判断,ms.isFlushCacheRequired(),即想每次查询都走DB也行,将
从MyBatis一级缓存来看,它以单纯的HashMap做缓存,没有容量控制,而一次SqlSession中通常来说并不会有大量的查询操作,因此只适用于一次SqlSession,如果用到二级缓存的Mapper级别的场景,有可能缓存数据不断碰到而导致内存溢出。
还有一点,差点忘了写了,
publicintupdate(MappedStatementms,Objectparameter)throwsSQLException{ ErrorContext.instance().resource(ms.getResource()).activity("executinganupdate").object(ms.getId()); if(closed){ thrownewExecutorException("Executorwasclosed."); } clearLocalCache(); returndoUpdate(ms,parameter); }
第6行clearLocalCache()方法,这意味着所有的增、删、改都会清空本地缓存,这和是否配置了flushCache=true是无关的。
这很好理解,因为增、删、改这三种操作都可能会导致查询出来的结果并不是原来的结果,如果增、删、改不清理缓存,那么可能导致读取出来的数据是脏数据。
一级缓存的CacheKey
接着我们看下一个问题:怎么样的查询条件算和上一次查询是一样的查询,从而返回同样的结果回去?这个问题,得从CacheKey说起。
我们先看一下CacheKey的数据结构:
publicclassCacheKeyimplementsCloneable,Serializable{ privatestaticfinallongserialVersionUID=1146682552656046210L; publicstaticfinalCacheKeyNULL_CACHE_KEY=newNullCacheKey(); privatestaticfinalintDEFAULT_MULTIPLYER=37; privatestaticfinalintDEFAULT_HASHCODE=17; privateintmultiplier; privateinthashcode; privatelongchecksum; privateintcount; privateList
其中最重要的是第14行的updateList这个两个属性,为什么这么说,因为HashMap的Key是CacheKey,而HashMap的get方法是先判断hashCode,在hashCode冲突的情况下再进行equals判断,因此最终无论如何都会进行一次equals的判断,看下equals方法的实现:
publicbooleanequals(Objectobject){ if(this==object){ returntrue; } if(!(objectinstanceofCacheKey)){ returnfalse; } finalCacheKeycacheKey=(CacheKey)object; if(hashcode!=cacheKey.hashcode){ returnfalse; } if(checksum!=cacheKey.checksum){ returnfalse; } if(count!=cacheKey.count){ returnfalse; } for(inti=0;i看到整个方法的流程都是围绕着updateList中的每个属性进行逐一比较,因此再进一步的,我们要看一下updateList中到底存储了什么。
关于updateList里面存储的数据我们可以看下哪里使用了updateList的add方法,然后一步一步反推回去即可。updateList中数据的添加是在doUpdate方法中:
privatevoiddoUpdate(Objectobject){ intbaseHashCode=object==null?1:object.hashCode(); count++; checksum+=baseHashCode; baseHashCode*=count; hashcode=multiplier*hashcode+baseHashCode; updateList.add(object); }它的调用方为update方法:
publicvoidupdate(Objectobject){ if(object!=null&&object.getClass().isArray()){ intlength=Array.getLength(object); for(inti=0;i这里主要是对输入参数是数组类型进行了一次判断,是数组就遍历逐一做doUpdate,否则就直接做doUpdate。再看update方法的调用方,其实update方法的调用方有挺多处,但是这里我们要看的是Executor中的,看一下BaseExecutor中的createCacheKey方法实现:
... CacheKeycacheKey=newCacheKey(); cacheKey.update(ms.getId()); cacheKey.update(rowBounds.getOffset()); cacheKey.update(rowBounds.getLimit()); cacheKey.update(boundSql.getSql()); ...到了这里应当一目了然了,MyBastis从三个维度判断两次查询是相同的:
RowBounds的offset和limit属性,RowBounds是MyBatis用于处理分页的一个类,offset默认为0,limit默认为Integer.MAX_VALUE
标签中定义的sql语句 即只要两次查询满足以上三个条件且没有定义flushCache="true",那么第二次查询会直接从MyBatis一级缓存PerpetualCache中返回数据,而不会走DB。
MyBatis二级缓存
上面说完了MyBatis,接着看一下MyBatis二级缓存,还是从二级缓存工作流程开始。还是从DefaultSqlSession的selectList方法进去:
publicList selectList(Stringstatement,Objectparameter,RowBoundsrowBounds){ try{ MappedStatementms=configuration.getMappedStatement(statement); returnexecutor.query(ms,wrapCollection(parameter),rowBounds,Executor.NO_RESULT_HANDLER); }catch(Exceptione){ throwExceptionFactory.wrapException("Errorqueryingdatabase.Cause:"+e,e); }finally{ ErrorContext.instance().reset(); } } 执行query方法,方法位于CachingExecutor中:
publicList query(MappedStatementms,ObjectparameterObject,RowBoundsrowBounds,ResultHandlerresultHandler)throwsSQLException{ BoundSqlboundSql=ms.getBoundSql(parameterObject); CacheKeykey=createCacheKey(ms,parameterObject,rowBounds,boundSql); returnquery(ms,parameterObject,rowBounds,resultHandler,key,boundSql); } 继续跟第4行的query方法,同样位于CachingExecutor中:
publicList query(MappedStatementms,ObjectparameterObject,RowBoundsrowBounds,ResultHandlerresultHandler,CacheKeykey,BoundSqlboundSql) throwsSQLException{ Cachecache=ms.getCache(); if(cache!=null){ flushCacheIfRequired(ms); if(ms.isUseCache()&&resultHandler==null){ ensureNoOutParams(ms,parameterObject,boundSql); @SuppressWarnings("unchecked") List list=(List )tcm.getObject(cache,key); if(list==null){ list=delegate. query(ms,parameterObject,rowBounds,resultHandler,key,boundSql); tcm.putObject(cache,key,list);//issue#578and#116 } returnlist; } } returndelegate. query(ms,parameterObject,rowBounds,resultHandler,key,boundSql); } 从这里看到,执行第17行的BaseExecutor的query方法之前,会先拿Mybatis二级缓存,而BaseExecutor的query方法会优先读取MyBatis一级缓存,由此可以得出一个重要结论:假如定义了MyBatis二级缓存,那么MyBatis二级缓存读取优先级高于MyBatis一级缓存。
而第3行~第16行的逻辑:
第5行的方法很好理解,根据flushCache=true或者flushCache=false判断是否要清理二级缓存
第7行的方法是保证MyBatis二级缓存不会存储存储过程的结果
第9行的方法先尝试从tcm中获取查询结果,这个tcm解释一下,这又是一个装饰器模式(数数MyBatis用到了多少装饰器模式了),创建一个事物缓存TranactionalCache,持有Cache接口,Cache接口的实现类就是根据我们在Mapper文件中配置的
创建的Cache实例
第10行~第12行,如果没有从MyBatis二级缓存中拿到数据,那么就会查一次数据库,然后放到MyBatis二级缓存中去
至于如何判定上次查询和这次查询是一次查询?由于这里的CacheKey和MyBatis一级缓存使用的是同一个CacheKey,因此它的判定条件和前文写过的MyBatis一级缓存三个维度的判定条件是一致的。最后再来谈一点,"Cachecache=ms.getCache()"这句代码十分重要,这意味着Cache是从MappedStatement中获取到的,而MappedStatement又和每一个
、 、 、 绑定并在MyBatis启动的时候存入Configuration中: protectedfinalMapmappedStatements=newStrictMap ("MappedStatementscollection"); 因此MyBatis二级缓存的生命周期即整个应用的生命周期,应用不结束,定义的二级缓存都会存在在内存中。
从这个角度考虑,为了避免MyBatis二级缓存中数据量过大导致内存溢出,MyBatis在配置文件中给我们增加了很多配置例如size(缓存大小)、flushInterval(缓存清理时间间隔)、eviction(数据淘汰算法)来保证缓存中存储的数据不至于太过庞大。
MyBatis二级缓存实例化过程
接着看一下MyBatis二级缓存
实例化的过程,代码位于XmlMapperBuilder的cacheElement方法中: privatevoidcacheElement(XNodecontext)throwsException{ if(context!=null){ Stringtype=context.getStringAttribute("type","PERPETUAL"); ClasstypeClass=typeAliasRegistry.resolveAlias(type); Stringeviction=context.getStringAttribute("eviction","LRU"); ClassevictionClass=typeAliasRegistry.resolveAlias(eviction); LongflushInterval=context.getLongAttribute("flushInterval"); Integersize=context.getIntAttribute("size"); booleanreadWrite=!context.getBooleanAttribute("readOnly",false); booleanblocking=context.getBooleanAttribute("blocking",false); Propertiesprops=context.getChildrenAsProperties(); builderAssistant.useNewCache(typeClass,evictionClass,flushInterval,size,readWrite,blocking,props); } }这里分别取
中配置的各个属性,关注一下两个默认值: type表示缓存实现,默认是PERPETUAL,根据typeAliasRegistry中注册的,PERPETUAL实际对应PerpetualCache,这和MyBatis一级缓存是一致的
eviction表示淘汰算法,默认是LRU算法
第3行~第11行拿到了所有属性,那么调用12行的useNewCache方法创建缓存:
publicCacheuseNewCache(ClasstypeClass, ClassevictionClass, LongflushInterval, Integersize, booleanreadWrite, booleanblocking, Propertiesprops){ Cachecache=newCacheBuilder(currentNamespace) .implementation(valueOrDefault(typeClass,PerpetualCache.class)) .addDecorator(valueOrDefault(evictionClass,LruCache.class)) .clearInterval(flushInterval) .size(size) .readWrite(readWrite) .blocking(blocking) .properties(props) .build(); configuration.addCache(cache); currentCache=cache; returncache; }这里又使用了建造者模式,跟一下第16行的build()方法,在此之前该传入的参数都已经传入了CacheBuilder:
publicCachebuild(){ setDefaultImplementations(); Cachecache=newBaseCacheInstance(implementation,id); setCacheProperties(cache); //issue#352,donotapplydecoratorstocustomcaches if(PerpetualCache.class.equals(cache.getClass())){ for(Classdecorator:decorators){ cache=newCacheDecoratorInstance(decorator,cache); setCacheProperties(cache); } cache=setStandardDecorators(cache); }elseif(!LoggingCache.class.isAssignableFrom(cache.getClass())){ cache=newLoggingCache(cache); } returncache; }第3行的代码,构建基础的缓存,implementation指的是type配置的值,这里是默认的PerpetualCache。
第6行的代码,如果是PerpetualCache,那么继续装饰(又是装饰器模式,可以数数这几篇MyBatis源码解析的文章里面出现了多少次装饰器模式了),这里的装饰是根据eviction进行装饰,到这一步,给PerpetualCache加上了LRU的功能。
第11行的代码,继续装饰,这次MyBatis将它命名为标准装饰,setStandardDecorators方法实现为:
privateCachesetStandardDecorators(Cachecache){ try{ MetaObjectmetaCache=SystemMetaObject.forObject(cache); if(size!=null&&metaCache.hasSetter("size")){ metaCache.setValue("size",size); } if(clearInterval!=null){ cache=newScheduledCache(cache); ((ScheduledCache)cache).setClearInterval(clearInterval); } if(readWrite){ cache=newSerializedCache(cache); } cache=newLoggingCache(cache); cache=newSynchronizedCache(cache); if(blocking){ cache=newBlockingCache(cache); } returncache; }catch(Exceptione){ thrownewCacheException("Errorbuildingstandardcachedecorators.Cause:"+e,e); } }这次是根据其它的配置参数来:
- 如果配置了flushInterval,那么继续装饰为ScheduledCache,这意味着在调用Cache的getSize、putObject、getObject、removeObject四个方法的时候都会进行一次时间判断,如果到了指定的清理缓存时间间隔,那么就会将当前缓存清空
- 如果readWrite=true,那么继续装饰为SerializedCache,这意味着缓存中所有存储的内存都必须实现Serializable接口
- 跟配置无关,将之前装饰好的Cache继续装饰为LoggingCache与SynchronizedCache,前者在getObject的时候会打印缓存命中率,后者将Cache接口中所有的方法都加了Synchronized关键字进行了同步处理
- 如果blocking=true,那么继续装饰为BlockingCache,这意味着针对同一个CacheKey,拿数据与放数据、删数据是互斥的,即拿数据的时候必须没有在放数据、删数据
Cache全部装饰完毕,返回,至此MyBatis二级缓存生成完毕。
最后说一下,MyBatis支持三种类型的二级缓存:
- MyBatis默认的缓存,type为空,Cache为PerpetualCache
- 自定义缓存
- 第三方缓存
从build()方法来看,后两种场景的Cache,MyBatis只会将其装饰为LoggingCache,理由很简单,这些缓存的定期清除功能、淘汰过期数据功能开发者自己或者第三方缓存都已经实现好了,根本不需要依赖MyBatis本身的装饰。