详解Java的MyBatis框架中的缓存与缓存的使用改进
一级缓存与二级缓存
mybatis将数据缓存设计成两级结构,分为一级缓存、二级缓存:
一级缓存是session会话级别的缓存,位于表示一次数据库会话的sqlsession对象之中,又被称之为本地缓存。一级缓存是mybatis内部实现的一个特性,用户不能配置,默认情况下自动支持的缓存,用户没有定制它的权利(不过这也不是绝对的,可以通过开发插件对它进行修改);
二级缓存是application应用级别的缓存,它的是生命周期很长,跟application的声明周期一样,也就是说它的作用范围是整个application应用。
mybatis中一级缓存和二级缓存的组织如下图所示:
一级缓存的工作机制:
一级缓存是session会话级别的,一般而言,一个sqlsession对象会使用一个executor对象来完成会话操作,executor对象会维护一个cache缓存,以提高查询性能。
二级缓存的工作机制:
如上所言,一个sqlsession对象会使用一个executor对象来完成会话操作,mybatis的二级缓存机制的关键就是对这个executor对象做文章。如果用户配置了"cacheenabled=true",那么mybatis在为sqlsession对象创建executor对象时,会对executor对象加上一个装饰者:cachingexecutor,这时sqlsession使用cachingexecutor对象来完成操作请求。cachingexecutor对于查询请求,会先判断该查询请求在application级别的二级缓存中是否有缓存结果,如果有查询结果,则直接返回缓存结果;如果缓存中没有,再交给真正的executor对象来完成查询操作,之后cachingexecutor会将真正executor返回的查询结果放置到缓存中,然后在返回给用户。
mybatis的二级缓存设计得比较灵活,你可以使用mybatis自己定义的二级缓存实现;你也可以通过实现org.apache.ibatis.cache.cache接口自定义缓存;也可以使用第三方内存缓存库,如memcached等。
缓存的改造
问题:
最容易出现的问题是开启cache后,分页查询时无论查询哪一页都返回第一页的数据。另外,使用sql自动生成插件生成get方法的sql时,传入的参数不起作用,无论传入的参数是多少,都返回第一个参数的查询结果。
为什么出现这些问题:
在之前讲解mybatis的执行流程的时候提到,在开启cache的前提下,mybatis的executor会先从缓存里读取数据,读取不到才去数据库查询。问题就出在这里,sql自动生成插件和分页插件执行的时机是在statementhandler里,而statementhandler是在executor之后执行的,无论sql自动生成插件和分页插件都是通过改写sql来实现的,executor在生成读取cache的key(key由sql以及对应的参数值构成)时使用都是原始的sql,这样当然就出问题了。
解决问题:
找到问题的原因后,解决起来就方便了。只要通过拦截器改写executor里生成key的方法,在生成可以时使用自动生成的sql(对应sql自动生成插件)或加入分页信息(对应分页插件)就可以了。
拦截器签名:
@intercepts({@signature(type = executor.class, method = "query", args = {mappedstatement.class, object.class, rowbounds.class, resulthandler.class})}) public class cacheinterceptor implements interceptor { ... }
从签名里可以看出,要拦截的目标类型是executor(注意:type只能配置成接口类型),拦截的方法是名称为query的方法。
intercept的实现:
public object intercept(invocation invocation) throws throwable { executor executorproxy = (executor) invocation.gettarget(); metaobject metaexecutor = metaobject.forobject(executorproxy, default_object_factory, default_object_wrapper_factory); // 分离代理对象链 while (metaexecutor.hasgetter("h")) { object object = metaexecutor.getvalue("h"); metaexecutor = metaobject.forobject(object, default_object_factory, default_object_wrapper_factory); } // 分离最后一个代理对象的目标类 while (metaexecutor.hasgetter("target")) { object object = metaexecutor.getvalue("target"); metaexecutor = metaobject.forobject(object, default_object_factory, default_object_wrapper_factory); } object[] args = invocation.getargs(); return this.query(metaexecutor, args); } public <e> list<e> query(metaobject metaexecutor, object[] args) throws sqlexception { mappedstatement ms = (mappedstatement) args[0]; object parameterobject = args[1]; rowbounds rowbounds = (rowbounds) args[2]; resulthandler resulthandler = (resulthandler) args[3]; boundsql boundsql = ms.getboundsql(parameterobject); // 改写key的生成 cachekey cachekey = createcachekey(ms, parameterobject, rowbounds, boundsql); executor executor = (executor) metaexecutor.getoriginalobject(); return executor.query(ms, parameterobject, rowbounds, resulthandler, cachekey, boundsql); } private cachekey createcachekey(mappedstatement ms, object parameterobject, rowbounds rowbounds, boundsql boundsql) { configuration configuration = ms.getconfiguration(); pagesqlid = configuration.getvariables().getproperty("pagesqlid"); if (null == pagesqlid || "".equals(pagesqlid)) { logger.warn("property pagesqlid is not setted,use default '.*page$' "); pagesqlid = defaultpagesqlid; } cachekey cachekey = new cachekey(); cachekey.update(ms.getid()); cachekey.update(rowbounds.getoffset()); cachekey.update(rowbounds.getlimit()); list<parametermapping> parametermappings = boundsql.getparametermappings(); // 解决自动生成sql,sql语句为空导致key生成错误的bug if (null == boundsql.getsql() || "".equals(boundsql.getsql())) { string id = ms.getid(); id = id.substring(id.lastindexof(".") + 1); string newsql = null; try { if ("select".equals(id)) { newsql = sqlbuilder.buildselectsql(parameterobject); } sqlsource sqlsource = buildsqlsource(configuration, newsql, parameterobject.getclass()); parametermappings = sqlsource.getboundsql(parameterobject).getparametermappings(); cachekey.update(newsql); } catch (exception e) { logger.error("update cachekey error.", e); } } else { cachekey.update(boundsql.getsql()); } metaobject metaobject = metaobject.forobject(parameterobject, default_object_factory, default_object_wrapper_factory); if (parametermappings.size() > 0 && parameterobject != null) { typehandlerregistry typehandlerregistry = ms.getconfiguration().gettypehandlerregistry(); if (typehandlerregistry.hastypehandler(parameterobject.getclass())) { cachekey.update(parameterobject); } else { for (parametermapping parametermapping : parametermappings) { string propertyname = parametermapping.getproperty(); if (metaobject.hasgetter(propertyname)) { cachekey.update(metaobject.getvalue(propertyname)); } else if (boundsql.hasadditionalparameter(propertyname)) { cachekey.update(boundsql.getadditionalparameter(propertyname)); } } } } // 当需要分页查询时,将page参数里的当前页和每页数加到cachekey里 if (ms.getid().matches(pagesqlid) && metaobject.hasgetter("page")) { pageparameter page = (pageparameter) metaobject.getvalue("page"); if (null != page) { cachekey.update(page.getcurrentpage()); cachekey.update(page.getpagesize()); } } return cachekey; }
plugin的实现:
public object plugin(object target) { // 当目标类是cachingexecutor类型时,才包装目标类,否者直接返回目标本身,减少目标被代理的 // 次数 if (target instanceof cachingexecutor) { return plugin.wrap(target, this); } else { return target; } }
推荐阅读
-
详解Java的MyBatis框架中的事务处理
-
详解Java的MyBatis框架与Spring框架整合中的映射器注入
-
Java的MyBatis框架中Mapper映射配置的使用及原理解析
-
Java的MyBatis框架中XML映射缓存的使用教程
-
Java环境中MyBatis与Spring或Spring MVC框架的集成方法
-
详解Java的MyBatis框架中的缓存与缓存的使用改进
-
Java的MyBatis框架中关键的XML字段映射的配置参数详解
-
Java的MyBatis框架中关键的XML字段映射的配置参数详解
-
Java的MyBatis框架中Mapper映射配置的使用及原理解析
-
Java的MyBatis框架中XML映射缓存的使用教程