Mybaits 源码解析 (九)----- 全网最详细,没有之一:一级缓存和二级缓存源码分析
像mybatis、hibernate这样的orm框架,封装了jdbc的大部分操作,极大的简化了我们对数据库的操作。
在实际项目中,我们发现在一个事务中查询同样的语句两次的时候,第二次没有进行数据库查询,直接返回了结果,实际这种情况我们就可以称为缓存。
mybatis的缓存级别
一级缓存
- mybatis的一级查询缓存(也叫作本地缓存)是基于org.apache.ibatis.cache.impl.perpetualcache 类的 hashmap本地缓存,其作用域是sqlsession,mybatis 默认一级查询缓存是开启状态,且不能关闭。
- 在同一个sqlsession中两次执行相同的 sql查询语句,第一次执行完毕后,会将查询结果写入到缓存中,第二次会从缓存中直接获取数据,而不再到数据库中进行查询,这样就减少了数据库的访问,从而提高查询效率。
- 基于perpetualcache 的 hashmap本地缓存,其存储作用域为 session,perpetualcache 对象是在sqlsession中的executor的localcache属性当中存放,当 session flush 或 close 之后,该session中的所有 cache 就将清空。
二级缓存
- 二级缓存与一级缓存其机制相同,默认也是采用 perpetualcache,hashmap存储,不同在于其存储作用域为 mapper(namespace),每个mapper中有一个cache对象,存放在configration中,并且将其放进当前mapper的所有mappedstatement当中,并且可自定义存储源,如 ehcache。
- mapper级别缓存,定义在mapper文件的<cache>标签并需要开启此缓存
用下面这张图描述一级缓存和二级缓存的关系。
cachekey
在 mybatis 中,引入缓存的目的是为提高查询效率,降低数据库压力。既然 mybatis 引入了缓存,那么大家思考过缓存中的 key 和 value 的值分别是什么吗?大家可能很容易能回答出 value 的内容,不就是 sql 的查询结果吗。那 key 是什么呢?是字符串,还是其他什么对象?如果是字符串的话,那么大家首先能想到的是用 sql 语句作为 key。但这是不对的,比如:
select * from user where id > ?
id > 1 和 id > 10 查出来的结果可能是不同的,所以我们不能简单的使用 sql 语句作为 key。从这里可以看出来,运行时参数将会影响查询结果,因此我们的 key 应该涵盖运行时参数。除此之外呢,如果进行分页查询也会导致查询结果不同,因此 key 也应该涵盖分页参数。综上,我们不能使用简单的 sql 语句作为 key。应该考虑使用一种复合对象,能涵盖可影响查询结果的因子。在 mybatis 中,这种复合对象就是 cachekey。下面来看一下它的定义。
public class cachekey implements cloneable, serializable { private static final int default_multiplyer = 37; private static final int default_hashcode = 17; // 乘子,默认为37 private final int multiplier; // cachekey 的 hashcode,综合了各种影响因子 private int hashcode; // 校验和 private long checksum; // 影响因子个数 private int count; // 影响因子集合 private list<object> updatelist; public cachekey() { this.hashcode = default_hashcode; this.multiplier = default_multiplyer; this.count = 0; this.updatelist = new arraylist<object>(); } /** 每当执行更新操作时,表示有新的影响因子参与计算 * 当不断有新的影响因子参与计算时,hashcode 和 checksum 将会变得愈发复杂和随机。这样可降低冲突率,使 cachekey 可在缓存中更均匀的分布。 */ public void update(object object) { int basehashcode = object == null ? 1 : arrayutil.hashcode(object); // 自增 count count++; // 计算校验和 checksum += basehashcode; // 更新 basehashcode basehashcode *= count; // 计算 hashcode hashcode = multiplier * hashcode + basehashcode; // 保存影响因子 updatelist.add(object); } /** * cachekey 最终要作为键存入 hashmap,因此它需要覆盖 equals 和 hashcode 方法 */ public boolean equals(object object) { // 检测是否为同一个对象 if (this == object) { return true; } // 检测 object 是否为 cachekey if (!(object instanceof cachekey)) { return false; } final cachekey cachekey = (cachekey) object; // 检测 hashcode 是否相等 if (hashcode != cachekey.hashcode) { return false; } // 检测校验和是否相同 if (checksum != cachekey.checksum) { return false; } // 检测 coutn 是否相同 if (count != cachekey.count) { return false; } // 如果上面的检测都通过了,下面分别对每个影响因子进行比较 for (int i = 0; i < updatelist.size(); i++) { object thisobject = updatelist.get(i); object thatobject = cachekey.updatelist.get(i); if (!arrayutil.equals(thisobject, thatobject)) { return false; } } return true; } public int hashcode() { // 返回 hashcode 变量 return hashcode; } }
当不断有新的影响因子参与计算时,hashcode 和 checksum 将会变得愈发复杂和随机。这样可降低冲突率,使 cachekey 可在缓存中更均匀的分布。cachekey 最终要作为键存入 hashmap,因此它需要覆盖 equals 和 hashcode 方法。
一级缓存源码解析
一级缓存的测试
同一个session查询
public static void main(string[] args) { sqlsession session = sqlsessionfactory.opensession(); try { blog blog = (blog)session.selectone("querybyid",1); blog blog2 = (blog)session.selectone("querybyid",1); } finally { session.close(); } }
结论:只有一个db查询
两个session分别查询
public static void main(string[] args) { sqlsession session = sqlsessionfactory.opensession(); sqlsession session1 = sqlsessionfactory.opensession(); try { blog blog = (blog)session.selectone("querybyid",17); blog blog2 = (blog)session1.selectone("querybyid",17); } finally { session.close(); } }
结论:进行了两次db查询
同一个session,进行update之后再次查询
public static void main(string[] args) { sqlsession session = sqlsessionfactory.opensession(); try { blog blog = (blog)session.selectone("querybyid",17); blog.setname("llll"); session.update("updateblog",blog); blog blog2 = (blog)session.selectone("querybyid",17); } finally { session.close(); } }
结论:进行了两次db查询
总结:在一级缓存中,同一个sqlsession下,查询语句相同的sql会被缓存,如果执行增删改操作之后,该缓存就会被删除
创建缓存对象perpetualcache
我们来回顾一下创建sqlsession的过程
sqlsession session = sessionfactory.opensession(); public sqlsession opensession() { return this.opensessionfromdatasource(this.configuration.getdefaultexecutortype(), (transactionisolationlevel)null, false); } private sqlsession opensessionfromdatasource(executortype exectype, transactionisolationlevel level, boolean autocommit) { transaction tx = null; defaultsqlsession var8; try { environment environment = this.configuration.getenvironment(); transactionfactory transactionfactory = this.gettransactionfactoryfromenvironment(environment); tx = transactionfactory.newtransaction(environment.getdatasource(), level, autocommit); //创建sql执行器 executor executor = this.configuration.newexecutor(tx, exectype); var8 = new defaultsqlsession(this.configuration, executor, autocommit); } catch (exception var12) { this.closetransaction(tx); throw exceptionfactory.wrapexception("error opening session. cause: " + var12, var12); } finally { errorcontext.instance().reset(); } return var8; } public executor newexecutor(transaction transaction, executortype executortype) { executortype = executortype == null ? this.defaultexecutortype : executortype; executortype = executortype == null ? executortype.simple : executortype; object executor; if (executortype.batch == executortype) { executor = new batchexecutor(this, transaction); } else if (executortype.reuse == executortype) { executor = new reuseexecutor(this, transaction); } else { //默认创建simpleexecutor executor = new simpleexecutor(this, transaction); } if (this.cacheenabled) { //开启二级缓存就会用cachingexecutor装饰simpleexecutor executor = new cachingexecutor((executor)executor); } executor executor = (executor)this.interceptorchain.pluginall(executor); return executor; } public simpleexecutor(configuration configuration, transaction transaction) { super(configuration, transaction); } protected baseexecutor(configuration configuration, transaction transaction) { this.transaction = transaction; this.deferredloads = new concurrentlinkedqueue(); //创建一个缓存对象,perpetualcache并不是线程安全的 //但sqlsession和executor对象在通常情况下只能有一个线程访问,而且访问完成之后马上销毁。也就是session.close(); this.localcache = new perpetualcache("localcache"); this.localoutputparametercache = new perpetualcache("localoutputparametercache"); this.closed = false; this.configuration = configuration; this.wrapper = this; }
我只是简单的贴了代码,大家可以看我之前的博客,我们可以看到defaultsqlsession中有simpleexecutor对象,simpleexecutor对象中有一个perpetualcache,一级缓存的数据就是存储在perpetualcache对象中,sqlsession关闭的时候会清空perpetualcache
一级缓存实现
再来看baseexecutor中的query方法是怎么实现一级缓存的,executor默认实现为cachingexecutor
cachingexecutor
public <e> list<e> query(mappedstatement ms, object parameter, rowbounds rowbounds, resulthandler resulthandler) throws sqlexception { boundsql boundsql = ms.getboundsql(parameter); //利用sql和执行的参数生成一个key,如果同一sql不同的执行参数的话,将会生成不同的key cachekey key = createcachekey(ms, parameter, rowbounds, boundsql); return query(ms, parameter, rowbounds, resulthandler, key, boundsql); } @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, parameterobject, 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; } } // 直接来到这里 // 实现为baseexecutor.query() return delegate.<e> query(ms, parameterobject, rowbounds, resulthandler, key, boundsql); }
如上,在访问一级缓存之前,mybatis 首先会调用 createcachekey 方法创建 cachekey。下面我们来看一下 createcachekey 方法的逻辑:
public cachekey createcachekey(mappedstatement ms, object parameterobject, rowbounds rowbounds, boundsql boundsql) { if (closed) { throw new executorexception("executor was closed."); } // 创建 cachekey 对象 cachekey cachekey = new cachekey(); // 将 mappedstatement 的 id 作为影响因子进行计算 cachekey.update(ms.getid()); // rowbounds 用于分页查询,下面将它的两个字段作为影响因子进行计算 cachekey.update(rowbounds.getoffset()); cachekey.update(rowbounds.getlimit()); // 获取 sql 语句,并进行计算 cachekey.update(boundsql.getsql()); list<parametermapping> parametermappings = boundsql.getparametermappings(); typehandlerregistry typehandlerregistry = ms.getconfiguration().gettypehandlerregistry(); for (parametermapping parametermapping : parametermappings) { if (parametermapping.getmode() != parametermode.out) { // 运行时参数 object value; // 当前大段代码用于获取 sql 中的占位符 #{xxx} 对应的运行时参数, // 前文有类似分析,这里忽略了 string propertyname = parametermapping.getproperty(); if (boundsql.hasadditionalparameter(propertyname)) { value = boundsql.getadditionalparameter(propertyname); } else if (parameterobject == null) { value = null; } else if (typehandlerregistry.hastypehandler(parameterobject.getclass())) { value = parameterobject; } else { metaobject metaobject = configuration.newmetaobject(parameterobject); value = metaobject.getvalue(propertyname); } // 让运行时参数参与计算 cachekey.update(value); } } if (configuration.getenvironment() != null) { // 获取 environment id 遍历,并让其参与计算 cachekey.update(configuration.getenvironment().getid()); } return cachekey; }
如上,在计算 cachekey 的过程中,有很多影响因子参与了计算。比如 mappedstatement 的 id 字段,sql 语句,分页参数,运行时变量,environment 的 id 字段等。通过让这些影响因子参与计算,可以很好的区分不同查询请求。所以,我们可以简单的把 cachekey 看做是一个查询请求的 id。有了 cachekey,我们就可以使用它读写缓存了。
simpleexecutor(baseexecutor)
@suppresswarnings("unchecked") @override public <e> list<e> query(mappedstatement ms, object parameter, rowbounds rowbounds, resulthandler resulthandler, cachekey key, boundsql boundsql) throws sqlexception { errorcontext.instance().resource(ms.getresource()).activity("executing a query").object(ms.getid()); if (closed) { throw new executorexception("executor was closed."); } if (querystack == 0 && ms.isflushcacherequired()) { clearlocalcache(); } list<e> list; try { querystack++; // 看这里,先从localcache中获取对应cachekey的结果值 list = resulthandler == null ? (list<e>) localcache.getobject(key) : null; if (list != null) { handlelocallycachedoutputparameters(ms, key, parameter, boundsql); } else { // 如果缓存中没有值,则从db中查询 list = queryfromdatabase(ms, parameter, rowbounds, resulthandler, key, boundsql); } } finally { querystack--; } if (querystack == 0) { for (deferredload deferredload : deferredloads) { deferredload.load(); } deferredloads.clear(); if (configuration.getlocalcachescope() == localcachescope.statement) { clearlocalcache(); } } return list; }
baseexecutor.queryfromdatabase()
我们先来看下这种缓存中没有值的情况,看一下查询后的结果是如何被放置到缓存中的
private <e> list<e> queryfromdatabase(mappedstatement ms, object parameter, rowbounds rowbounds, resulthandler resulthandler, cachekey key, boundsql boundsql) throws sqlexception { list<e> list; localcache.putobject(key, execution_placeholder); try { // 1.执行查询,获取list list = doquery(ms, parameter, rowbounds, resulthandler, boundsql); } finally { localcache.removeobject(key); } // 2.将查询后的结果放置到localcache中,key就是我们刚才封装的cachekey,value就是从db中查询到的list localcache.putobject(key, list); if (ms.getstatementtype() == statementtype.callable) { localoutputparametercache.putobject(key, parameter); } return list; }
我们来看看 localcache.putobject(key, list);
perpetualcache
perpetualcache 是一级缓存使用的缓存类,内部使用了 hashmap 实现缓存功能。它的源码如下:
public class perpetualcache implements cache { private final string id; private map<object, object> cache = new hashmap<object, object>(); public perpetualcache(string id) { this.id = id; } @override public string getid() { return id; } @override public int getsize() { return cache.size(); } @override public void putobject(object key, object value) { // 存储键值对到 hashmap cache.put(key, value); } @override public object getobject(object key) { // 查找缓存项 return cache.get(key); } @override public object removeobject(object key) { // 移除缓存项 return cache.remove(key); } @override public void clear() { cache.clear(); } // 省略部分代码 }
总结:可以看到localcache本质上就是一个map,key为我们的cachekey,value为我们的结果值,是不是很简单,只是封装了一个map而已。
清除缓存
sqlsession.update()
当我们进行更新操作时,会执行如下代码
@override public int update(mappedstatement ms, object parameter) throws sqlexception { errorcontext.instance().resource(ms.getresource()).activity("executing an update").object(ms.getid()); if (closed) { throw new executorexception("executor was closed."); } //每次执行update/insert/delete语句时都会清除一级缓存。 clearlocalcache(); // 然后再进行更新操作 return doupdate(ms, parameter); } @override public void clearlocalcache() { if (!closed) { // 直接将map清空 localcache.clear(); localoutputparametercache.clear(); } }
session.close();
//defaultsqlsession public void close() { try { this.executor.close(this.iscommitorrollbackrequired(false)); this.closecursors(); this.dirty = false; } finally { errorcontext.instance().reset(); } } //baseexecutor public void close(boolean forcerollback) { try { try { this.rollback(forcerollback); } finally { if (this.transaction != null) { this.transaction.close(); } } } catch (sqlexception var11) { log.warn("unexpected exception on closing transaction. cause: " + var11); } finally { this.transaction = null; this.deferredloads = null; this.localcache = null; this.localoutputparametercache = null; this.closed = true; } } public void rollback(boolean required) throws sqlexception { if (!this.closed) { try { this.clearlocalcache(); this.flushstatements(true); } finally { if (required) { this.transaction.rollback(); } } } } public void clearlocalcache() { if (!this.closed) { // 直接将map清空 this.localcache.clear(); this.localoutputparametercache.clear(); } }
当关闭sqlsession时,也会清楚sqlsession中的一级缓存
总结
- 一级缓存只在同一个sqlsession*享数据
- 在同一个sqlsession对象执行相同的sql并参数也要相同,缓存才有效。
- 如果在sqlsession中执行update/insert/detete语句或者session.close();的话,sqlsession中的executor对象会将一级缓存清空。
二级缓存源码解析
二级缓存构建在一级缓存之上,在收到查询请求时,mybatis 首先会查询二级缓存。若二级缓存未命中,再去查询一级缓存。与一级缓存不同,二级缓存和具体的命名空间绑定,一个mapper中有一个cache,相同mapper中的mappedstatement公用一个cache,一级缓存则是和 sqlsession 绑定。一级缓存不存在并发问题二级缓存可在多个命名空间间共享,这种情况下,会存在并发问题,比喻多个不同的sqlsession 会同时执行相同的sql语句,参数也相同,那么cachekey是相同的,就会造成多个线程并发访问相同cachekey的值,下面首先来看一下访问二级缓存的逻辑。
二级缓存的测试
二级缓存需要在mapper.xml中配置<cache/>标签
<?xml version="1.0" encoding="utf-8"?> <!doctype mapper public "-//mybatis.org//dtd mapper 3.0//en" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="mybatis.blogmapper"> <select id="querybyid" parametertype="int" resulttype="jdbc.blog"> select * from blog where id = #{id} </select> <update id="updateblog" parametertype="jdbc.blog"> update blog set name = #{name},url = #{url} where id=#{id} </update> <!-- 开启blogmapper二级缓存 --> <cache/> </mapper>
不同的session进行相同的查询
public static void main(string[] args) { sqlsession session = sqlsessionfactory.opensession(); sqlsession session1 = sqlsessionfactory.opensession(); try { blog blog = (blog)session.selectone("querybyid",17); blog blog2 = (blog)session1.selectone("querybyid",17); } finally { session.close(); } }
结论:执行两次db查询
第一个session查询完成之后,手动提交,在执行第二个session查询
public static void main(string[] args) { sqlsession session = sqlsessionfactory.opensession(); sqlsession session1 = sqlsessionfactory.opensession(); try { blog blog = (blog)session.selectone("querybyid",17); session.commit(); blog blog2 = (blog)session1.selectone("querybyid",17); } finally { session.close(); } }
结论:执行一次db查询
第一个session查询完成之后,手动关闭,在执行第二个session查询
public static void main(string[] args) { sqlsession session = sqlsessionfactory.opensession(); sqlsession session1 = sqlsessionfactory.opensession(); try { blog blog = (blog)session.selectone("querybyid",17); session.close(); blog blog2 = (blog)session1.selectone("querybyid",17); } finally { session.close(); } }
结论:执行一次db查询
总结:二级缓存的生效必须在session提交或关闭之后才会生效
标签<cache/>的解析
按照之前的对mybatis的分析,对blog.xml的解析工作主要交给xmlconfigbuilder.parse()方法来实现的
1 // xmlconfigbuilder.parse() 2 public configuration parse() { 3 if (parsed) { 4 throw new builderexception("each xmlconfigbuilder can only be used once."); 5 } 6 parsed = true; 7 parseconfiguration(parser.evalnode("/configuration"));// 在这里 8 return configuration; 9 } 10 11 // parseconfiguration() 12 // 既然是在blog.xml中添加的,那么我们就直接看关于mappers标签的解析 13 private void parseconfiguration(xnode root) { 14 try { 15 properties settings = settingsaspropertiess(root.evalnode("settings")); 16 propertieselement(root.evalnode("properties")); 17 loadcustomvfs(settings); 18 typealiaseselement(root.evalnode("typealiases")); 19 pluginelement(root.evalnode("plugins")); 20 objectfactoryelement(root.evalnode("objectfactory")); 21 objectwrapperfactoryelement(root.evalnode("objectwrapperfactory")); 22 reflectionfactoryelement(root.evalnode("reflectionfactory")); 23 settingselement(settings); 24 // read it after objectfactory and objectwrapperfactory issue #631 25 environmentselement(root.evalnode("environments")); 26 databaseidproviderelement(root.evalnode("databaseidprovider")); 27 typehandlerelement(root.evalnode("typehandlers")); 28 // 就是这里 29 mapperelement(root.evalnode("mappers")); 30 } catch (exception e) { 31 throw new builderexception("error parsing sql mapper configuration. cause: " + e, e); 32 } 33 } 34 35 36 // mapperelement() 37 private void mapperelement(xnode parent) throws exception { 38 if (parent != null) { 39 for (xnode child : parent.getchildren()) { 40 if ("package".equals(child.getname())) { 41 string mapperpackage = child.getstringattribute("name"); 42 configuration.addmappers(mapperpackage); 43 } else { 44 string resource = child.getstringattribute("resource"); 45 string url = child.getstringattribute("url"); 46 string mapperclass = child.getstringattribute("class"); 47 // 按照我们本例的配置,则直接走该if判断 48 if (resource != null && url == null && mapperclass == null) { 49 errorcontext.instance().resource(resource); 50 inputstream inputstream = resources.getresourceasstream(resource); 51 xmlmapperbuilder mapperparser = new xmlmapperbuilder(inputstream, configuration, resource, configuration.getsqlfragments()); 52 // 生成xmlmapperbuilder,并执行其parse方法 53 mapperparser.parse(); 54 } else if (resource == null && url != null && mapperclass == null) { 55 errorcontext.instance().resource(url); 56 inputstream inputstream = resources.geturlasstream(url); 57 xmlmapperbuilder mapperparser = new xmlmapperbuilder(inputstream, configuration, url, configuration.getsqlfragments()); 58 mapperparser.parse(); 59 } else if (resource == null && url == null && mapperclass != null) { 60 class<?> mapperinterface = resources.classforname(mapperclass); 61 configuration.addmapper(mapperinterface); 62 } else { 63 throw new builderexception("a mapper element may only specify a url, resource or class, but not more than one."); 64 } 65 } 66 } 67 } 68 }
我们来看看解析mapper.xml
// xmlmapperbuilder.parse() public void parse() { if (!configuration.isresourceloaded(resource)) { // 解析mapper属性 configurationelement(parser.evalnode("/mapper")); configuration.addloadedresource(resource); bindmapperfornamespace(); } parsependingresultmaps(); parsependingchacherefs(); parsependingstatements(); } // configurationelement() private void configurationelement(xnode context) { try { string namespace = context.getstringattribute("namespace"); if (namespace == null || namespace.equals("")) { throw new builderexception("mapper's namespace cannot be empty"); } builderassistant.setcurrentnamespace(namespace); cacherefelement(context.evalnode("cache-ref")); // 最终在这里看到了关于cache属性的处理 cacheelement(context.evalnode("cache")); parametermapelement(context.evalnodes("/mapper/parametermap")); resultmapelements(context.evalnodes("/mapper/resultmap")); sqlelement(context.evalnodes("/mapper/sql")); // 这里会将生成的cache包装到对应的mappedstatement buildstatementfromcontext(context.evalnodes("select|insert|update|delete")); } catch (exception e) { throw new builderexception("error parsing mapper xml. cause: " + e, e); } } // cacheelement() private void cacheelement(xnode context) throws exception { if (context != null) { //解析<cache/>标签的type属性,这里我们可以自定义cache的实现类,比如rediscache,如果没有自定义,这里使用和一级缓存相同的perpetual string type = context.getstringattribute("type", "perpetual"); class<? extends cache> typeclass = typealiasregistry.resolvealias(type); string eviction = context.getstringattribute("eviction", "lru"); class<? extends cache> evictionclass = typealiasregistry.resolvealias(eviction); long flushinterval = context.getlongattribute("flushinterval"); integer size = context.getintattribute("size"); boolean readwrite = !context.getbooleanattribute("readonly", false); boolean blocking = context.getbooleanattribute("blocking", false); properties props = context.getchildrenasproperties(); // 构建cache对象 builderassistant.usenewcache(typeclass, evictionclass, flushinterval, size, readwrite, blocking, props); } }
先来看看是如何构建cache对象的
mapperbuilderassistant.usenewcache()
public cache usenewcache(class<? extends cache> typeclass, class<? extends cache> evictionclass, long flushinterval, integer size, boolean readwrite, boolean blocking, properties props) { // 1.生成cache对象 cache cache = new cachebuilder(currentnamespace) //这里如果我们定义了<cache/>中的type,就使用自定义的cache,否则使用和一级缓存相同的perpetualcache .implementation(valueordefault(typeclass, perpetualcache.class)) .adddecorator(valueordefault(evictionclass, lrucache.class)) .clearinterval(flushinterval) .size(size) .readwrite(readwrite) .blocking(blocking) .properties(props) .build(); // 2.添加到configuration中 configuration.addcache(cache); // 3.并将cache赋值给mapperbuilderassistant.currentcache currentcache = cache; return cache; }
我们看到一个mapper.xml只会解析一次<cache/>标签,也就是只创建一次cache对象,放进configuration中,并将cache赋值给mapperbuilderassistant.currentcache
buildstatementfromcontext(context.evalnodes("select|insert|update|delete"));将cache包装到mappedstatement
// buildstatementfromcontext() private void buildstatementfromcontext(list<xnode> list) { if (configuration.getdatabaseid() != null) { buildstatementfromcontext(list, configuration.getdatabaseid()); } buildstatementfromcontext(list, null); } //buildstatementfromcontext() private void buildstatementfromcontext(list<xnode> list, string requireddatabaseid) { for (xnode context : list) { final xmlstatementbuilder statementparser = new xmlstatementbuilder(configuration, builderassistant, context, requireddatabaseid); try { // 每一条执行语句转换成一个mappedstatement statementparser.parsestatementnode(); } catch (incompleteelementexception e) { configuration.addincompletestatement(statementparser); } } } // xmlstatementbuilder.parsestatementnode(); public void parsestatementnode() { string id = context.getstringattribute("id"); string databaseid = context.getstringattribute("databaseid"); ... integer fetchsize = context.getintattribute("fetchsize"); integer timeout = context.getintattribute("timeout"); string parametermap = context.getstringattribute("parametermap"); string parametertype = context.getstringattribute("parametertype"); class<?> parametertypeclass = resolveclass(parametertype); string resultmap = context.getstringattribute("resultmap"); string resulttype = context.getstringattribute("resulttype"); string lang = context.getstringattribute("lang"); languagedriver langdriver = getlanguagedriver(lang); ... // 创建mappedstatement对象 builderassistant.addmappedstatement(id, sqlsource, statementtype, sqlcommandtype, fetchsize, timeout, parametermap, parametertypeclass, resultmap, resulttypeclass, resultsettypeenum, flushcache, usecache, resultordered, keygenerator, keyproperty, keycolumn, databaseid, langdriver, resultsets); } // builderassistant.addmappedstatement() public mappedstatement addmappedstatement( string id, ...) { if (unresolvedcacheref) { throw new incompleteelementexception("cache-ref not yet resolved"); } id = applycurrentnamespace(id, false); boolean isselect = sqlcommandtype == sqlcommandtype.select; //创建mappedstatement对象 mappedstatement.builder statementbuilder = new mappedstatement.builder(configuration, id, sqlsource, sqlcommandtype) ... .flushcacherequired(valueordefault(flushcache, !isselect)) .usecache(valueordefault(usecache, isselect)) .cache(currentcache);// 在这里将之前生成的cache封装到mappedstatement parametermap statementparametermap = getstatementparametermap(parametermap, parametertype, id); if (statementparametermap != null) { statementbuilder.parametermap(statementparametermap); } mappedstatement statement = statementbuilder.build(); configuration.addmappedstatement(statement); return statement; }
我们看到将mapper中创建的cache对象,加入到了每个mappedstatement对象中,也就是同一个mapper中所有的mappedstatement 中的cache属性引用是同一个
有关于<cache/>标签的解析就到这了。
查询源码分析
cachingexecutor
// cachingexecutor public <e> list<e> query(mappedstatement ms, object parameterobject, rowbounds rowbounds, resulthandler resulthandler) throws sqlexception { boundsql boundsql = ms.getboundsql(parameterobject); // 创建 cachekey cachekey key = createcachekey(ms, parameterobject, rowbounds, boundsql); return query(ms, parameterobject, rowbounds, resulthandler, key, boundsql); } public <e> list<e> query(mappedstatement ms, object parameterobject, rowbounds rowbounds, resulthandler resulthandler, cachekey key, boundsql boundsql) throws sqlexception { // 从 mappedstatement 中获取 cache,注意这里的 cache 是从mappedstatement中获取的 // 也就是我们上面解析mapper中<cache/>标签中创建的,它保存在configration中 // 我们在上面解析blog.xml时分析过每一个mappedstatement都有一个cache对象,就是这里 cache cache = ms.getcache(); // 如果配置文件中没有配置 <cache>,则 cache 为空 if (cache != null) { //如果需要刷新缓存的话就刷新:flushcache="true" flushcacheifrequired(ms); if (ms.isusecache() && resulthandler == null) { ensurenooutparams(ms, boundsql); // 访问二级缓存 list<e> list = (list<e>) tcm.getobject(cache, key); // 缓存未命中 if (list == null) { // 如果没有值,则执行查询,这个查询实际也是先走一级缓存查询,一级缓存也没有的话,则进行db查询 list = delegate.<e>query(ms, parameterobject, rowbounds, resulthandler, key, boundsql); // 缓存查询结果 tcm.putobject(cache, key, list); } return list; } } return delegate.<e>query(ms, parameterobject, rowbounds, resulthandler, key, boundsql); }
如果设置了flushcache="true",则每次查询都会刷新缓存
<!-- 执行此语句清空缓存 --> <select id="getall" resulttype="entity.tdemo" usecache="true" flushcache="true" > select * from t_demo </select>
如上,注意二级缓存是从 mappedstatement 中获取的。由于 mappedstatement 存在于全局配置中,可以多个 cachingexecutor 获取到,这样就会出现线程安全问题。除此之外,若不加以控制,多个事务共用一个缓存实例,会导致脏读问题。至于脏读问题,需要借助其他类来处理,也就是上面代码中 tcm 变量对应的类型。下面分析一下。
transactionalcachemanager
/** 事务缓存管理器 */ public class transactionalcachemanager { // cache 与 transactionalcache 的映射关系表 private final map<cache, transactionalcache> transactionalcaches = new hashmap<cache, transactionalcache>(); public void clear(cache cache) { // 获取 transactionalcache 对象,并调用该对象的 clear 方法,下同 gettransactionalcache(cache).clear(); } public object getobject(cache cache, cachekey key) { // 直接从transactionalcache中获取缓存 return gettransactionalcache(cache).getobject(key); } public void putobject(cache cache, cachekey key, object value) { // 直接存入transactionalcache的缓存中 gettransactionalcache(cache).putobject(key, value); } public void commit() { for (transactionalcache txcache : transactionalcaches.values()) { txcache.commit(); } } public void rollback() { for (transactionalcache txcache : transactionalcaches.values()) { txcache.rollback(); } } private transactionalcache gettransactionalcache(cache cache) { // 从映射表中获取 transactionalcache transactionalcache txcache = transactionalcaches.get(cache); if (txcache == null) { // transactionalcache 也是一种装饰类,为 cache 增加事务功能 // 创建一个新的transactionalcache,并将真正的cache对象存进去 txcache = new transactionalcache(cache); transactionalcaches.put(cache, txcache); } return txcache; } }
transactionalcachemanager 内部维护了 cache 实例与 transactionalcache 实例间的映射关系,该类也仅负责维护两者的映射关系,真正做事的还是 transactionalcache。transactionalcache 是一种缓存装饰器,可以为 cache 实例增加事务功能。我在之前提到的脏读问题正是由该类进行处理的。下面分析一下该类的逻辑。
transactionalcache
public class transactionalcache implements cache { //真正的缓存对象,和上面的map<cache, transactionalcache>中的cache是同一个 private final cache delegate; private boolean clearoncommit; // 在事务被提交前,所有从数据库中查询的结果将缓存在此集合中 private final map<object, object> entriestoaddoncommit; // 在事务被提交前,当缓存未命中时,cachekey 将会被存储在此集合中 private final set<object> entriesmissedincache; @override public object getobject(object key) { // 查询的时候是直接从delegate中去查询的,也就是从真正的缓存对象中查询 object object = delegate.getobject(key); if (object == null) { // 缓存未命中,则将 key 存入到 entriesmissedincache 中 entriesmissedincache.add(key); } if (clearoncommit) { return null; } else { return object; } } @override public void putobject(object key, object object) { // 将键值对存入到 entriestoaddoncommit 这个map中中,而非真实的缓存对象 delegate 中 entriestoaddoncommit.put(key, object); } @override public object removeobject(object key) { return null; } @override public void clear() { clearoncommit = true; // 清空 entriestoaddoncommit,但不清空 delegate 缓存 entriestoaddoncommit.clear(); } public void commit() { // 根据 clearoncommit 的值决定是否清空 delegate if (clearoncommit) { delegate.clear(); } // 刷新未缓存的结果到 delegate 缓存中 flushpendingentries(); // 重置 entriestoaddoncommit 和 entriesmissedincache reset(); } public void rollback() { unlockmissedentries(); reset(); } private void reset() { clearoncommit = false; // 清空集合 entriestoaddoncommit.clear(); entriesmissedincache.clear(); } private void flushpendingentries() { for (map.entry<object, object> entry : entriestoaddoncommit.entryset()) { // 将 entriestoaddoncommit 中的内容转存到 delegate 中 delegate.putobject(entry.getkey(), entry.getvalue()); } for (object entry : entriesmissedincache) { if (!entriestoaddoncommit.containskey(entry)) { // 存入空值 delegate.putobject(entry, null); } } } private void unlockmissedentries() { for (object entry : entriesmissedincache) { try { // 调用 removeobject 进行解锁 delegate.removeobject(entry); } catch (exception e) { log.warn("..."); } } } }
存储二级缓存对象的时候是放到了transactionalcache.entriestoaddoncommit这个map中,但是每次查询的时候是直接从transactionalcache.delegate中去查询的,所以这个二级缓存查询数据库后,设置缓存值是没有立刻生效的,主要是因为直接存到 delegate 会导致脏数据问题。
为何只有sqlsession提交或关闭之后二级缓存才会生效?
那我们来看下sqlsession.commit()方法做了什么
sqlsession
@override public void commit(boolean force) { try { // 主要是这句 executor.commit(iscommitorrollbackrequired(force)); dirty = false; } catch (exception e) { throw exceptionfactory.wrapexception("error committing transaction. cause: " + e, e); } finally { errorcontext.instance().reset(); } } // cachingexecutor.commit() @override public void commit(boolean required) throws sqlexception { delegate.commit(required); tcm.commit();// 在这里 } // transactionalcachemanager.commit() public void commit() { for (transactionalcache txcache : transactionalcaches.values()) { txcache.commit();// 在这里 } } // transactionalcache.commit() public void commit() { if (clearoncommit) { delegate.clear(); } flushpendingentries();//这一句 reset(); } // transactionalcache.flushpendingentries() private void flushpendingentries() { for (map.entry<object, object> entry : entriestoaddoncommit.entryset()) { // 在这里真正的将entriestoaddoncommit的对象逐个添加到delegate中,只有这时,二级缓存才真正的生效 delegate.putobject(entry.getkey(), entry.getvalue()); } for (object entry : entriesmissedincache) { if (!entriestoaddoncommit.containskey(entry)) { delegate.putobject(entry, null); } } }
如果从数据库查询到的数据直接存到 delegate 会导致脏数据问题。下面通过一张图演示一下脏数据问题发生的过程,假设两个线程开启两个不同的事务,它们的执行过程如下:
如上图,时刻2,事务 a 对记录 a 进行了更新。时刻3,事务 a 从数据库查询记录 a,并将记录 a 写入缓存中。时刻4,事务 b 查询记录 a,由于缓存中存在记录 a,事务 b 直接从缓存中取数据。这个时候,脏数据问题就发生了。事务 b 在事务 a 未提交情况下,读取到了事务 a 所修改的记录。为了解决这个问题,我们可以为每个事务引入一个独立的缓存。查询数据时,仍从 delegate 缓存(以下统称为共享缓存)中查询。若缓存未命中,则查询数据库。存储查询结果时,并不直接存储查询结果到共享缓存中,而是先存储到事务缓存中,也就是 entriestoaddoncommit 集合。当事务提交时,再将事务缓存中的缓存项转存到共享缓存中。这样,事务 b 只能在事务 a 提交后,才能读取到事务 a 所做的修改,解决了脏读问题。
二级缓存的刷新
我们来看看sqlsession的更新操作
public int update(string statement, object parameter) { int var4; try { this.dirty = true; mappedstatement ms = this.configuration.getmappedstatement(statement); var4 = this.executor.update(ms, this.wrapcollection(parameter)); } catch (exception var8) { throw exceptionfactory.wrapexception("error updating database. cause: " + var8, var8); } finally { errorcontext.instance().reset(); } return var4; } public int update(mappedstatement ms, object parameterobject) throws sqlexception { this.flushcacheifrequired(ms); return this.delegate.update(ms, parameterobject); } private void flushcacheifrequired(mappedstatement ms) { //获取mappedstatement对应的cache,进行清空 cache cache = ms.getcache(); //sql需设置flushcache="true" 才会执行清空 if (cache != null && ms.isflushcacherequired()) { this.tcm.clear(cache); } }
mybatis二级缓存只适用于不常进行增、删、改的数据,比如国家行政区省市区街道数据。一但数据变更,mybatis会清空缓存。因此二级缓存不适用于经常进行更新的数据。
使用redis存储二级缓存
通过上面代码分析,我们知道二级缓存默认和一级缓存都是使用的perpetualcache存储结果,一级缓存只要sqlsession关闭就会清空,其内部使用hashmap实现,所以二级缓存无法实现分布式,并且服务器重启后就没有缓存了。此时就需要引入第三方缓存中间件,将缓存的值存到外部,如redis和ehcache
修改mapper.xml中的配置。
<?xml version="1.0" encoding="utf-8" ?> <!doctype mapper public "-//mybatis.org//dtd mapper 3.0//en" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.tyb.saas.common.dal.dao.areadefaultmapper"> <!-- flushinterval(清空缓存的时间间隔): 单位毫秒,可以被设置为任意的正整数。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。 size(引用数目): 可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的可用内存资源数目。默认值是1024。 readonly(只读):属性可以被设置为true或false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是false。 eviction(回收策略): 默认的是 lru: 1.lru – 最近最少使用的:移除最长时间不被使用的对象。 2.fifo – 先进先出:按对象进入缓存的顺序来移除它们。 3.soft – 软引用:移除基于垃圾回收器状态和软引用规则的对象。 4.weak – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。 blocking(是否使用阻塞缓存): 默认为false,当指定为true时将采用blockingcache进行封装,blocking,阻塞的意思, 使用blockingcache会在查询缓存时锁住对应的key,如果缓存命中了则会释放对应的锁,否则会在查询数据库以后再释放锁, 这样可以阻止并发情况下多个线程同时查询数据,详情可参考blockingcache的源码。 type(缓存类):可指定使用的缓存类,mybatis默认使用hashmap进行缓存,这里引用第三方中间件进行缓存 --> <cache type="org.mybatis.caches.redis.rediscache" blocking="false" flushinterval="0" readonly="true" size="1024" eviction="fifo"/> <!-- usecache(是否使用缓存):默认true使用缓存 --> <select id="find" parametertype="map" resulttype="com.chenhao.model.user" usecache="true"> select * from user </select> </mapper>
依然很简单, rediscache 在保存缓存数据和获取缓存数据时,使用了java的序列化和反序列化,因此需要保证被缓存的对象必须实现serializable接口。
也可以自己实现cache
实现自己的cache
package com.chenhao.mybatis.cache; import org.apache.ibatis.cache.cache; import org.springframework.data.redis.core.redistemplate; import org.springframework.data.redis.core.valueoperations; import java.util.concurrent.timeunit; import java.util.concurrent.locks.readwritelock; import java.util.concurrent.locks.reentrantreadwritelock; /** * @author chenhao * @date 2019/10/31. */ public class rediscache implements cache { private final string id; private static valueoperations<string, object> valueos; private static redistemplate<string, string> template; public static void setvalueos(valueoperations<string, object> valueos) { rediscache.valueos = valueos; } public static void settemplate(redistemplate<string, string> template) { rediscache.template = template; } private final readwritelock readwritelock = new reentrantreadwritelock(); public rediscache(string id) { if (id == null) { throw new illegalargumentexception("cache instances require an id"); } this.id = id; } @override public string getid() { return this.id; } @override public void putobject(object key, object value) { valueos.set(key.tostring(), value, 10, timeunit.minutes); } @override public object getobject(object key) { return valueos.get(key.tostring()); } @override public object removeobject(object key) { valueos.set(key.tostring(), "", 0, timeunit.minutes); return key; } @override public void clear() { template.getconnectionfactory().getconnection().flushdb(); } @override public int getsize() { return template.getconnectionfactory().getconnection().dbsize().intvalue(); } @override public readwritelock getreadwritelock() { return this.readwritelock; } }
mapper中配置自己实现的cache
<cache type="com.chenhao.mybatis.cache.rediscache"/>
上一篇: 郭子兴为什么会将女儿嫁给朱元璋 只因看中了他的一个优势
下一篇: PHP将数组转字符串
推荐阅读
-
Mybaits 源码解析 (十)----- 全网最详细,没有之一:Spring-Mybatis框架使用与源码解析
-
Mybaits 源码解析 (八)----- 全网最详细,没有之一:结果集 ResultSet 自动映射成实体类对象(上篇)
-
Mybaits 源码解析 (九)----- 全网最详细,没有之一:一级缓存和二级缓存源码分析
-
Mybaits 源码解析 (七)----- Select 语句的执行过程分析(下篇)全网最详细,没有之一
-
Mybaits 源码解析 (十)----- 全网最详细,没有之一:Spring-Mybatis框架使用与源码解析
-
Mybaits 源码解析 (八)----- 全网最详细,没有之一:结果集 ResultSet 自动映射成实体类对象(上篇)
-
Mybaits 源码解析 (九)----- 全网最详细,没有之一:一级缓存和二级缓存源码分析
-
Mybaits 源码解析 (七)----- Select 语句的执行过程分析(下篇)全网最详细,没有之一