深入了解MyBatis二级缓存
Posted MyBatis
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入了解MyBatis二级缓存相关的知识,希望对你有一定的参考价值。
这是一篇发布于 2015 年的博客。
推荐使用可控的 Spring Cache 替代 MyBatis 二级缓存。
一、创建Cache的完整过程
我们从SqlSessionFactoryBuilder解析mybatis-config.xml配置文件开始:
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
然后是:
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
看parser.parse()方法:
parseConfiguration(parser.evalNode("/configuration"));
看处理Mapper.xml文件的位置:
mapperElement(root.evalNode("mappers"));
看处理Mapper.xml的XMLMapperBuilder:
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration,
resource, configuration.getSqlFragments());
mapperParser.parse();
继续看parse方法:
configurationElement(parser.evalNode("/mapper"));
到这里:
String namespace = context.getStringAttribute("namespace");
if (namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
从这里看到 namespace
就是xml中 <mapper>
元素的属性。然后下面是先后处理的 cache-ref
和 cache
,后面的 cache
会覆盖前面的 cache-ref
,但是如果一开始 cache-ref
没有找到引用的 cache
,他就不会被覆盖,会一直到最后处理完成为止,最后如果存在 cache
,反而会被 cache-ref
覆盖。这里是不是看着有点晕、有点乱?所以千万别同时配置这两个,实际上也很少有人会这么做。
看看MyBatis如何处理 <cache/>
:
private void cacheElement(XNode context) throws Exception {
if (context != null) {
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();
builderAssistant.useNewCache(typeClass, evictionClass,
flushInterval, size, readWrite, blocking, props);
}
}
从源码可以看到MyBatis读取了那些属性,而且很容易可以到这些属性的默认值。
创建Java的cache对象方法为 builderAssistant.useNewCache
,我们看看这段代码:
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
typeClass = valueOrDefault(typeClass, PerpetualCache.class);
evictionClass = valueOrDefault(evictionClass, LruCache.class);
Cache cache = new CacheBuilder(currentNamespace)
.implementation(typeClass)
.addDecorator(evictionClass)
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
从调用该方法的地方,我们可以看到并没有使用返回值cache,在后面的过程中创建MappedStatement的时候使用了 currentCache
。
二、使用Cache过程
在系统中,使用Cache的地方在 CachingExecutor
中:
@Override
public <E> List<E> query(
MappedStatement ms, Object parameterObject,
RowBounds rowBounds, ResultHandler resultHandler,
CacheKey key, BoundSql boundSql) throws SQLException {
Cache cache = ms.getCache();
获取cache后,先判断是否有二级缓存。 只有通过 <cache/>,<cache-ref/>
或 @CacheNamespace,@CacheNamespaceRef
标记使用缓存的Mapper.xml或Mapper接口(同一个namespace,不能同时使用)才会有二级缓存。
if (cache != null) {
如果cache存在,那么会根据sql配置( <insert>,<select>,<update>,<delete>
的 flushCache
属性来确定是否清空缓存。
flushCacheIfRequired(ms);
然后根据xml配置的属性 useCache
来判断是否使用缓存(resultHandler一般使用的默认值,很少会null)。
if (ms.isUseCache() && resultHandler == null) {
确保方法没有Out类型的参数,mybatis不支持存储过程的缓存,所以如果是存储过程,这里就会报错。
ensureNoOutParams(ms, parameterObject, boundSql);
没有问题后,就会从cache中根据key来取值:
@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;
}
}
没有缓存时,直接执行查询
return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
在上面的代码中 tcm.putObject(cache,key,list);
这句代码是缓存了结果。但是实际上直到sqlsession关闭,MyBatis才以序列化的形式保存到了一个Map(默认的缓存配置)中。
三、Cache使用时的注意事项
1. 只能在【只有单表操作】的表上使用缓存
不只是要保证这个表在整个系统中只有单表操作,而且和该表有关的全部操作必须全部在一个 namespace
下。
2. 在可以保证查询远远大于insert,update,delete操作的情况下使用缓存
这一点不需要多说,所有人都应该清楚。记住,这一点需要保证在1的前提下才可以!
四、避免使用二级缓存
可能会有很多人不理解这里,二级缓存带来的好处远远比不上他所隐藏的危害。
缓存是以
namespace
为单位的,不同namespace
下的操作互不影响。insert,update,delete操作会清空所在
namespace
下的全部缓存。通常使用MyBatis Generator生成的代码中,都是各个表独立的,每个表都有自己的
namespace
。
为什么避免使用二级缓存
在符合【Cache使用时的注意事项】的要求时,并没有什么危害。
其他情况就会有很多危害了。
针对一个表的某些操作不在他独立的 namespace
下进行。
例如在 UserMapper.xml
中有大多数针对 user
表的操作。但是在一个 XXXMapper.xml
中,还有针对 user
单表的操作。
这会导致 user
在两个命名空间下的数据不一致。如果在 UserMapper.xml
中做了刷新缓存的操作,在 XXXMapper.xml
中缓存仍然有效,如果有针对 user
的单表查询,使用缓存的结果可能会不正确。
更危险的情况是在 XXXMapper.xml
做了insert,update,delete操作时,会导致 UserMapper.xml
中的各种操作充满未知和风险。
有关这样单表的操作可能不常见。但是你也许想到了一种常见的情况。
多表操作一定不能使用缓存
为什么不能?
首先不管多表操作写到那个 namespace
下,都会存在某个表不在这个 namespace
下的情况。
例如两个表: role
和 user_role
,如果我想查询出某个用户的全部角色 role
,就一定会涉及到多表的操作。
<select id="selectUserRoles" resultType="UserRoleVO">
select * from user_role a,role b where a.roleid = b.roleid and a.userid = #{userid}
</select>
像上面这个查询,你会写到那个xml中呢??
不管是写到 RoleMapper.xml
还是 UserRoleMapper.xml
,或者是一个独立的 XxxMapper.xml
中。如果使用了二级缓存,都会导致上面这个查询结果可能不正确。
如果你正好修改了这个用户的角色,上面这个查询使用缓存的时候结果就是错的。
这点应该很容易理解。
在我看来,就以MyBatis目前的缓存方式来看是无解的。多表操作根本不能缓存。
如果你让他们都使用同一个 namespace
(通过 <cache-ref>
)来避免脏数据,那就失去了缓存的意义。
看到这里,实际上就是说,二级缓存不能用。整篇文章介绍这么多也没什么用了。
五、挽救二级缓存?
想更高效率的使用二级缓存是解决不了了。
但是解决多表操作避免脏数据还是有法解决的。解决思路就是通过拦截器判断执行的sql涉及到那些表(可以用jsqlparser解析),然后把相关表的缓存自动清空。但是这种方式对缓存的使用效率是很低的。
设计这样一个插件是相当复杂的,既然我没想着去实现,就不废话了。
最后还是建议,放弃二级缓存,在业务层使用可控制的缓存代替更好。
如果各位有更好的解决方法,欢迎留言~~~~~~~
以上是关于深入了解MyBatis二级缓存的主要内容,如果未能解决你的问题,请参考以下文章
《深入理解mybatis原理7》 MyBatis的二级缓存的设计原理