您好,欢迎来到三六零分类信息网!老站,搜索引擎当天收录,欢迎发信息

【MyBatis源码解析】MyBatis一二级缓存

2024/12/16 23:56:17发布16次查看
mybatis缓存
我们知道,频繁的数据库操作是非常耗费性能的(主要是因为对于db而言,数据是持久化在磁盘中的,因此查询操作需要通过io,io操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相同的查询语句,完全可以把查询结果存储起来,下次查询同样的内容的时候直接从内存中获取数据即可,这样在某些场景下可以大大提升查询效率。
mybatis的缓存分为两种:
一级缓存,一级缓存是sqlsession级别的缓存,对于相同的查询,会从缓存中返回结果而不是查询数据库
二级缓存,二级缓存是mapper级别的缓存,定义在mapper文件的<cache>标签中并需要开启此缓存,多个mapper文件可以共用一个缓存,依赖<cache-ref>标签配置
下面来详细看一下mybatis的一二级缓存。
mybatis一级缓存工作流程
接着看一下mybatis一级缓存工作流程。前面说了,mybatis的一级缓存是sqlsession级别的缓存,当opensession()的方法运行完毕或者主动调用了sqlsession的close方法,sqlsession就被回收了,一级缓存与此同时也一起被回收掉了。前面的文章有说过,在mybatis中,无论selectone还是selectlist方法,最终都被转换为了selectlist方法来执行,那么看一下sqlsession的selectlist方法的实现:
 1 public <e> list<e> selectlist(string statement, object parameter, rowbounds rowbounds) { 2     try { 3       mappedstatement ms = configuration.getmappedstatement(statement); 4       return executor.query(ms, wrapcollection(parameter), rowbounds, executor.no_result_handler); 5     } catch (exception e) { 6       throw exceptionfactory.wrapexception(error querying database.  cause:  + e, e); 7     } finally { 8       errorcontext.instance().reset(); 9     }10 }

继续跟踪第4行的代码,到baseexeccutor的query方法:
1 public <e> list<e> query(mappedstatement ms, object parameter, rowbounds rowbounds, resulthandler resulthandler) throws sqlexception {2     boundsql boundsql = ms.getboundsql(parameter);3     cachekey key = createcachekey(ms, parameter, rowbounds, boundsql);4     return query(ms, parameter, rowbounds, resulthandler, key, boundsql);5 }
第3行构建缓存条件cachekey,这里涉及到怎么样条件算是和上一次查询是同一个条件的一个问题,因为同一个条件就可以返回上一次的结果回去,这部分代码留在下一部分分析。
接着看第4行的query方法的实现,代码位于cachingexecutor中:
 1 public <e> list<e> query(mappedstatement ms, object parameterobject, rowbounds rowbounds, resulthandler resulthandler, cachekey key, boundsql boundsql) 2       throws sqlexception { 3     cache cache = ms.getcache(); 4     if (cache != null) { 5       flushcacheifrequired(ms); 6       if (ms.isusecache() && resulthandler == null) { 7         ensurenooutparams(ms, parameterobject, boundsql); 8         @suppresswarnings(unchecked) 9         list<e> list = (list<e>) tcm.getobject(cache, key);10         if (list == null) {11           list = delegate.<e> query(ms, parameterobject, rowbounds, resulthandler, key, boundsql);12           tcm.putobject(cache, key, list); // issue #578 and #11613         }14         return list;15       }16     }17     return delegate.<e> query(ms, parameterobject, rowbounds, resulthandler, key, boundsql);18 }

第3行~第16行的代码先不管,继续跟第17行的query方法,代码位于baseexecutor中:
 1 public <e> list<e> query(mappedstatement ms, object parameter, rowbounds rowbounds, resulthandler resulthandler, cachekey key, boundsql boundsql) throws sqlexception { 2     errorcontext.instance().resource(ms.getresource()).activity(executing a query).object(ms.getid()); 3     if (closed) { 4       throw new executorexception(executor was closed.); 5     } 6     if (querystack == 0 && ms.isflushcacherequired()) { 7       clearlocalcache(); 8     } 9     list<e> list;10     try {11       querystack++;12       list = resulthandler == null ? (list<e>) localcache.getobject(key) : null;13       if (list != null) {14         handlelocallycachedoutputparameters(ms, key, parameter, boundsql);15       } else {16         list = queryfromdatabase(ms, parameter, rowbounds, resulthandler, key, boundsql);17       }18     } finally {19       querystack--;20     }21     ...22 }
看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也行,将<select>标签中的flushcache属性设置为true即可,这意味着每次查询的时候都会清理一遍perpetualcache,perpetualcache中没数据,自然只能走db
从mybatis一级缓存来看,它以单纯的hashmap做缓存,没有容量控制,而一次sqlsession中通常来说并不会有大量的查询操作,因此只适用于一次sqlsession,如果用到二级缓存的mapper级别的场景,有可能缓存数据不断碰到而导致内存溢出。
还有一点,差点忘了写了,<insert>、<delete>、<update>最终都会转换为update方法,看一下baseexecutor的update方法:
1 public int update(mappedstatement ms, object parameter) throws sqlexception {2     errorcontext.instance().resource(ms.getresource()).activity(executing an update).object(ms.getid());3     if (closed) {4       throw new executorexception(executor was closed.);5     }6     clearlocalcache();7     return doupdate(ms, parameter);8 }
第6行clearlocalcache()方法,这意味着所有的增、删、改都会清空本地缓存,这和是否配置了flushcache=true是无关的。
这很好理解,因为增、删、改这三种操作都可能会导致查询出来的结果并不是原来的结果,如果增、删、改不清理缓存,那么可能导致读取出来的数据是脏数据。
一级缓存的cachekey
接着我们看下一个问题:怎么样的查询条件算和上一次查询是一样的查询,从而返回同样的结果回去?这个问题,得从cachekey说起。
我们先看一下cachekey的数据结构:
 1 public class cachekey implements cloneable, serializable { 2  3   private static final long serialversionuid = 1146682552656046210l; 4  5   public static final cachekey null_cache_key = new nullcachekey(); 6  7   private static final int default_multiplyer = 37; 8   private static final int default_hashcode = 17; 9 10   private int multiplier;11   private int hashcode;12   private long checksum;13   private int count;14   private list<object> updatelist;15   ...16 }
其中最重要的是第14行的updatelist这个两个属性,为什么这么说,因为hashmap的key是cachekey,而hashmap的get方法是先判断hashcode,在hashcode冲突的情况下再进行equals判断,因此最终无论如何都会进行一次equals的判断,看下equals方法的实现:
 1 public boolean equals(object object) { 2     if (this == object) { 3       return true; 4     } 5     if (!(object instanceof cachekey)) { 6       return false; 7     } 8  9     final cachekey cachekey = (cachekey) object;10 11     if (hashcode != cachekey.hashcode) {12       return false;13     }14     if (checksum != cachekey.checksum) {15       return false;16     }17     if (count != cachekey.count) {18       return false;19     }20 21     for (int i = 0; i < updatelist.size(); i++) {22       object thisobject = updatelist.get(i);23       object thatobject = cachekey.updatelist.get(i);24       if (thisobject == null) {25         if (thatobject != null) {26           return false;27         }28       } else {29         if (!thisobject.equals(thatobject)) {30           return false;31         }32       }33     }34     return true;35 }
看到整个方法的流程都是围绕着updatelist中的每个属性进行逐一比较,因此再进一步的,我们要看一下updatelist中到底存储了什么。
关于updatelist里面存储的数据我们可以看下哪里使用了updatelist的add方法,然后一步一步反推回去即可。updatelist中数据的添加是在doupdate方法中:
 1 private void doupdate(object object) { 2     int basehashcode = object == null ? 1 : object.hashcode(); 3  4     count++; 5     checksum += basehashcode; 6     basehashcode *= count; 7  8     hashcode = multiplier * hashcode + basehashcode; 9 10     updatelist.add(object);11 }
它的调用方为update方法:
 1 public void update(object object) { 2     if (object != null && object.getclass().isarray()) { 3       int length = array.getlength(object); 4       for (int i = 0; i < length; i++) { 5         object element = array.get(object, i); 6         doupdate(element); 7       } 8     } else { 9       doupdate(object);10     }11 }
这里主要是对输入参数是数组类型进行了一次判断,是数组就遍历逐一做doupdate,否则就直接做doupdate。再看update方法的调用方,其实update方法的调用方有挺多处,但是这里我们要看的是executor中的,看一下baseexecutor中的createcachekey方法实现:
1 ...2 cachekey cachekey = new cachekey();3 cachekey.update(ms.getid());4 cachekey.update(rowbounds.getoffset());5 cachekey.update(rowbounds.getlimit());6 cachekey.update(boundsql.getsql());7 ...
到了这里应当一目了然了,mybastis从三组共四个条件判断两次查询是相同的:
<select>标签所在的mapper的namespace+<select>标签的id属性
rowbounds的offset和limit属性,rowbounds是mybatis用于处理分页的一个类,offset默认为0,limit默认为integer.max_value
<select>标签中定义的sql语句
即只要两次查询满足以上三个条件且没有定义flushcache=true,那么第二次查询会直接从mybatis一级缓存perpetualcache中返回数据,而不会走db。
mybatis二级缓存
上面说完了mybatis,接着看一下mybatis二级缓存,还是从二级缓存工作流程开始。还是从defaultsqlsession的selectlist方法进去:
 1 public <e> list<e> selectlist(string statement, object parameter, rowbounds rowbounds) { 2     try { 3       mappedstatement ms = configuration.getmappedstatement(statement); 4       return executor.query(ms, wrapcollection(parameter), rowbounds, executor.no_result_handler); 5     } catch (exception e) { 6       throw exceptionfactory.wrapexception(error querying database.  cause:  + e, e); 7     } finally { 8       errorcontext.instance().reset(); 9     }10 }

执行query方法,方法位于cachingexecutor中:
1 public <e> list<e> query(mappedstatement ms, object parameterobject, rowbounds rowbounds, resulthandler resulthandler) throws sqlexception {2     boundsql boundsql = ms.getboundsql(parameterobject);3     cachekey key = createcachekey(ms, parameterobject, rowbounds, boundsql);4     return query(ms, parameterobject, rowbounds, resulthandler, key, boundsql);5 }
继续跟第4行的query方法,同样位于cachingexecutor中:
 1 public <e> list<e> query(mappedstatement ms, object parameterobject, rowbounds rowbounds, resulthandler resulthandler, cachekey key, boundsql boundsql) 2       throws sqlexception { 3     cache cache = ms.getcache(); 4     if (cache != null) { 5       flushcacheifrequired(ms); 6       if (ms.isusecache() && resulthandler == null) { 7         ensurenooutparams(ms, parameterobject, boundsql); 8         @suppresswarnings(unchecked) 9         list<e> list = (list<e>) tcm.getobject(cache, key);10         if (list == null) {11           list = delegate.<e> query(ms, parameterobject, rowbounds, resulthandler, key, boundsql);12           tcm.putobject(cache, key, list); // issue #578 and #11613         }14         return list;15       }16     }17     return delegate.<e> query(ms, parameterobject, rowbounds, resulthandler, key, boundsql);18 }

从这里看到,执行第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>创建的cache实例
第10行~第12行,如果没有从mybatis二级缓存中拿到数据,那么就会查一次数据库,然后放到mybatis二级缓存中去
至于如何判定上次查询和这次查询是一次查询?由于这里的cachekey和mybatis一级缓存使用的是同一个cachekey,因此它的判定条件和前文写过的mybatis一级缓存三个维度的判定条件是一致的。
最后再来谈一点,cache cache = ms.getcache()这句代码十分重要,这意味着cache是从mappedstatement中获取到的,而mappedstatement又和每一个<insert>、<delete>、<update>、<select>绑定并在mybatis启动的时候存入configuration中:
protected final map<string, mappedstatement> mappedstatements = new strictmap<mappedstatement>(mapped statements collection);
因此mybatis二级缓存的生命周期即整个应用的生命周期,应用不结束,定义的二级缓存都会存在在内存中。
从这个角度考虑,为了避免mybatis二级缓存中数据量过大导致内存溢出,mybatis在配置文件中给我们增加了很多配置例如size(缓存大小)、flushinterval(缓存清理时间间隔)、eviction(数据淘汰算法)来保证缓存中存储的数据不至于太过庞大。
mybatis二级缓存实例化过程
接着看一下mybatis二级缓存<cache>实例化的过程,代码位于xmlmapperbuilder的cacheelement方法中:
 1 private void cacheelement(xnode context) throws exception { 2     if (context != null) { 3       string type = context.getstringattribute(type, perpetual); 4       class<? extends cache> typeclass = typealiasregistry.resolvealias(type); 5       string eviction = context.getstringattribute(eviction, lru); 6       class<? extends cache> evictionclass = typealiasregistry.resolvealias(eviction); 7       long flushinterval = context.getlongattribute(flushinterval); 8       integer size = context.getintattribute(size); 9       boolean readwrite = !context.getbooleanattribute(readonly, false);10       boolean blocking = context.getbooleanattribute(blocking, false);11       properties props = context.getchildrenasproperties();12       builderassistant.usenewcache(typeclass, evictionclass, flushinterval, size, readwrite, blocking, props);13     }14 }
这里分别取<cache>中配置的各个属性,关注一下两个默认值:
type表示缓存实现,默认是perpetual,根据typealiasregistry中注册的,perpetual实际对应perpetualcache,这和mybatis一级缓存是一致的
eviction表示淘汰算法,默认是lru算法
第3行~第11行拿到了所有属性,那么调用12行的usenewcache方法创建缓存:
 1 public cache usenewcache(class<? extends cache> typeclass, 2       class<? extends cache> evictionclass, 3       long flushinterval, 4       integer size, 5       boolean readwrite, 6       boolean blocking, 7       properties props) { 8     cache cache = new cachebuilder(currentnamespace) 9         .implementation(valueordefault(typeclass, perpetualcache.class))10         .adddecorator(valueordefault(evictionclass, lrucache.class))11         .clearinterval(flushinterval)12         .size(size)13         .readwrite(readwrite)14         .blocking(blocking)15         .properties(props)16         .build();17     configuration.addcache(cache);18     currentcache = cache;19     return cache;20 }
这里又使用了建造者模式,跟一下第16行的build()方法,在此之前该传入的参数都已经传入了cachebuilder:
 1 public cache build() { 2     setdefaultimplementations(); 3     cache cache = newbasecacheinstance(implementation, id); 4     setcacheproperties(cache); 5     // issue #352, do not apply decorators to custom caches 6     if (perpetualcache.class.equals(cache.getclass())) { 7       for (class<? extends cache> decorator : decorators) { 8         cache = newcachedecoratorinstance(decorator, cache); 9         setcacheproperties(cache);10       }11       cache = setstandarddecorators(cache);12     } else if (!loggingcache.class.isassignablefrom(cache.getclass())) {13       cache = new loggingcache(cache);14     }15     return cache;16 }
第3行的代码,构建基础的缓存,implementation指的是type配置的值,这里是默认的perpetualcache。
第6行的代码,如果是perpetualcache,那么继续装饰(又是装饰器模式,可以数数这几篇mybatis源码解析的文章里面出现了多少次装饰器模式了),这里的装饰是根据eviction进行装饰,到这一步,给perpetualcache加上了lru的功能。
第11行的代码,继续装饰,这次mybatis将它命名为标准装饰,setstandarddecorators方法实现为:
 1 private cache setstandarddecorators(cache cache) { 2     try { 3       metaobject metacache = systemmetaobject.forobject(cache); 4       if (size != null && metacache.hassetter(size)) { 5         metacache.setvalue(size, size); 6       } 7       if (clearinterval != null) { 8         cache = new scheduledcache(cache); 9         ((scheduledcache) cache).setclearinterval(clearinterval);10       }11       if (readwrite) {12         cache = new serializedcache(cache);13       }14       cache = new loggingcache(cache);15       cache = new synchronizedcache(cache);16       if (blocking) {17         cache = new blockingcache(cache);18       }19       return cache;20     } catch (exception e) {21       throw new cacheexception(error building standard cache decorators.  cause:  + e, e);22     }23 }
这次是根据其它的配置参数来:
如果配置了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本身的装饰。
mybatis二级缓存带来的问题
补充一个内容,mybatis二级缓存使用的在某些场景下会出问题,来看一下为什么这么说。
假设我有一条select语句(开启了二级缓存):
select a.col1, a.col2, a.col3, b.col1, b.col2, b.col3 from tablea a, tableb b where a.id = b.id;
对于tablea与tableb的操作定义在两个mapper中,分别叫做mappera与mapperb,即它们属于两个命名空间,如果此时启用缓存:
mappera中执行上述sql语句查询这6个字段
tableb更新了col1与col2两个字段
mappera再次执行上述sql语句查询这6个字段(前提是没有执行过任何insert、delete、update操作)
此时问题就来了,即使第(2)步tableb更新了col1与col2两个字段,第(3)步mappera走二级缓存查询到的这6个字段依然是原来的这6个字段的值,因为我们从cachekey的3组条件来看:
<select>标签所在的mapper的namespace+<select>标签的id属性
rowbounds的offset和limit属性,rowbounds是mybatis用于处理分页的一个类,offset默认为0,limit默认为integer.max_value
<select>标签中定义的sql语句
对于mappera来说,其中的任何一个条件都没有变化,自然会将原结果返回。
这个问题对于mybatis的二级缓存来说是一个无解的问题,因此使用mybatis二级缓存有一个前提:必须保证所有的增删改查都在同一个命名空间下才行。
以上就是【mybatis源码解析】mybatis一二级缓存的详细内容。
该用户其它信息

VIP推荐

免费发布信息,免费发布B2B信息网站平台 - 三六零分类信息网 沪ICP备09012988号-2
企业名录 Product