在项目中曾经出现mybatis-ehcache做二级缓存导致数据脏读的问题。在读场景居多的情况下,这种方式可以提高了缓存命中率。但是在实际应用场景下,开发规范不可控,坑可能更多些。。。
<!-- ecache -->
<dependency>
<groupId>com.googlecode.ehcache-spring-annotations</groupId>
<artifactId>ehcache-spring-annotations</artifactId>
<version>1.1.2</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache -->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.0.3</version>
</dependency>
<!-- ecache -->
一般会配置如下的pom,然后配置好ehcache与Mapper文件就可以了。这里边会引用mybatis-ehcache。
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
Mybatis本身只支持一级缓存,提供二级缓存的接入能力。当有更新产生时,会清空一二级缓存,借个网上的图说明下缓存架构
这里有个关键点二级缓存是基于mapper映射文件的namespace。可以查看org.mybatis.caches.ehcache.EhcacheCache源码
public final class EhcacheCache implements Cache {
/**
* The cache manager reference.
*/
private static final CacheManager CACHE_MANAGER = CacheManager.create();
/**
* The cache id (namespace)
*/
private final String id;
/**
* The cache instance
*/
private final Ehcache cache;
/**
* Dummy lock that actually does not lock.
* As of 3.2.6 MyBatis does not call locking methods.
*/
private final ReadWriteLock readWriteLock = new DummyReadWriteLock();
EhcacheCache继承了Mybatis的org.apache.ibatis.cache.Cache接口。其中的cacheId即为Mapper文件的namespace。而mybatis 对cache的管理是通过cacheId来做的。
我们看下Mybatis创建cache,此部分在org.apache.ibatis.builder.MapperBuilderAssistant。
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
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)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
configuration.addCache(cache)是在org.apache.ibatis.session.Configuration中添加cache的
public void addCache(Cache cache) {
caches.put(cache.getId(), cache);
}
此处的cacheId即为org.mybatis.caches.ehcache.EhcacheCache中的id值。
在org.apache.ibatis.executor.CachingExecutor可以看到Mybatis的清除cache执行方法。
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
cache从属于MappedStatement,每个ms对应一个mapper文件的命名空间。Mybatis是在不同的namesapce下对各自的cache进行操作的。
从而mybatis在ehcache作为缓存情况下出现多表或者关联查询时,将很可能出现脏数据。那位同学就是因为在一个mapper中关联查询,关联表更新,但是对应这个mapper 中的cache还是原来的导致的脏读问题。
总的来看这种二级缓存方式弊要大于利。
1、这种方式基于mapper的namespace区分,要能确保开发人员在一个mapper中不引入其他的表。如果出现关联查询则更难处理。
2、使用场景非常有限,需要读远超过其他三种方式。
综上,建议还是在业务逻辑层加入缓存,而尽量谨慎选择Mybatis的二级缓存方式。