一级缓存与二级缓存
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框架中的缓存与缓存的使用改进。