mybatis缓存由于性能、脏数据等原因一般不用(那还看毛线),但我们也需要了解它(至少知道如何关闭它),并从中学习一些缓存的设计。Mybatis缓存有Session级(一级缓存)和Mapper级(二级缓存),一级缓存不能被Session共享,二级缓存可以。下面详细介绍下(和缓存无关的用蓝色字体标注)
Session缓存(一级缓存),使用hashmap存储,Session不会共享缓存
配置文件添加settings,添加如下setting。value为SESSION(启用)或者STATEMENT(禁用)
<setting name="localCacheScope" value="SESSION" />
Mybatis会读取这个设置的位置并保存到configuration的localCacheScope中,默认是SESSION。
Properties settings = settingsAsProperties(root.evalNode("settings"));
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
在settingsAsProperties方法中有代码片段:
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
通过类名(元数据类)猜测它保存了传入Class的类信息(方法,字段,方法参数),这里关注的是缓存,mark下,以后研究
还记得主流程吗? Session->Executor, 一级缓存主要就是BaseExecutor,query流程为:根据查询条件生成CacheKey,从缓存查,查不出在则查DB并放入缓存,这里的缓存用的是PerpetualCache,内部就是hashmap。
CacheKey如何生成?内部保存了count(调用update次数),checksum(基础hash值之和),updateList(传入的object)
其它的我也不知道是啥,不建议深究。
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
CacheKey比较方法:
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof CacheKey)) {
return false;
}
final CacheKey cacheKey = (CacheKey) object;
if (hashcode != cacheKey.hashcode) {
return false;
}
if (checksum != cacheKey.checksum) {
return false;
}
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;
}
CacheKey就这么多,为什么这么设计,有什么好处,不清楚呀!!!
继续debug,终于找到你->localCacheScope:
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
Session缓存比较简单,若localCacheScope为Statement,则每次都会把缓存clear掉,不同Session不共享原因是:
Session->Executor->PerpetualCache->HashMap。
Session缓存问题???(先思考下哦):
脏数据(Session1缓存了id为1的city,Session2更新了改city)
内存问题(hashmap一直put后??? 等着oom吧)
清除一刀切(clearLocalCache直接clear掉map,缓存了city但更新world后,不好意思,clear everything)
Mapper缓存(二级缓存),相比Session缓存,控制的更精细,不同Session可共享,可对缓存进行一些装饰,但脏数据也是致命的(分布式服务),下面看下二级缓存:
开启二级缓存如下,然并卵,cacheEnabled默认true
<setting name="cacheEnabled" value="true"/>
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.hhg.jerry.dao.CityDao">
<cache blocking="true" eviction="LRU"
flushInterval="500" readOnly="false" size="100" type="perpetual"/>
<select id="getById"
resultType="com.hhg.jerry.model.City"
parameterType="java.lang.Long"
useCache="true" flushCache="false"
>
SELECT * FROM city where id = #{id}
</select>
</mapper>
先大概说明下,一个Mapper只能配置一个cache,最简单的写法<cache/>就ok了,blocking:阻塞,eviction:最近使用(LRU),flushInterval(刷新间隔),readOnly(只读?),size(大小),type(缓存类),<select>中多了userCache和flushCache,对于select而言不写他们也一样,会有默认值,而<update><delete>的默认值和select相反
cacheEnabled时SimpleExecutor会被CachedExecutor装饰,CachedExecutor的query先查看MappedStatement.getCache方法,不为空才会走二级缓存,否则走一级。MappedStatement?先mark下,等下,Cache对象从MappedStatement获取的,还是得看下MappedStatement:
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);
}
}
上面的方法解析了配置文件(没配则会给默认值),最后调用了builderAssistant.userNewCache来构建Cache。
注意到typeAliasRegistry,通过string参数返回Class对象,比如传入PERPETUAL则返回PERPETUAL的类对象,mark下。Class<? extends Cache>是啥?,LRU又是什么?LruCache里面用了LinkedHashMap,且重写了map的removeEldestEntry方法?(不知道的问问阿杜吧)
继续看代码:
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
用CacheBuilder构建了cache,加到configuration,设置(MapperBuildAssistant的)currentCache为cache
再看CacheBuilder的build方法:
public Cache build() {
setDefaultImplementations();
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// issue #352, do not apply decorators to custom caches
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache(cache);
}
return cache;
}
注意issue #352,可能是bug?上面的build就是把cache进行装饰,装饰逻辑:
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
cache = new SerializedCache(cache);
}
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
if (blocking) {
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
MetaObject metaCache = SystemMetaObject.forObject(cache); MetaObject?代表着对象吧?mark下
到此,完美,就是有个小问题,我在哪?
速度定下位:在构建 configuration->mapper->buildAssistant->cache,哦,在构建mapper时用了助手(buildAssistant),把构建的cache放到助手的currentCache中了,Cache构建完了,和MappedStatement还没有关系唉,继续看:
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
继续跟发现还是助手的addStatement方法添加了MappedStatement,并把currentCache放置到MappedStatement中去,当然MappedStatement还设置了flushCache和userCache(对select而言默认flush为false,usercache为true,其他相反)
回头接着看CachingExecutor的query方法:
@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, 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;
}
}
return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
resultHander这里为空,先不管它,剩下的除了tcm都好理解,看看tcm是啥:
public class TransactionalCacheManager {
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
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 txCache = transactionalCaches.get(cache);
if (txCache == null) {
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
}
哦,事务,当CachingExecutor放入一个缓存时,先放到了TransactionalCache中,带提交后才会把缓存的对象放到MappedStatement所对应的cache对象中去,而在rollback时直接clear,那就顺带看一眼Session的commit和rollback,会调用tcm对应方法的。
思考下:
1、在Session中查id为1的city,改Session未commit,再次查询会从缓存读取吗?
2、二级缓存为什么能对不同的Session可见?
3、二级缓存何时被clear?
思考完就该实践下验证缓存了,缓存的装饰类大概10个吧,建议都去了解下。
上面思考1,不会,接着去DB拿数据 2,Mapper缓存在哪里?MappedStatement中啊,不同的Session用的是同一个MappedStatement,3、update时呗,准确的说取决于MappedStatement的flushCacheRequired,rollback时呢?当然不会,缓存还没放到Cache对象中,当然会了,tcm.clear直接把这个Cache对象原来的缓存清掉了
cache-ref好像没有说到,举个栗子:CityMap.xml配置cache-ref为CoutryMap.xml,那么对于CityMap而言,buildAssistant的currentCache引用的就是CountryMap.xml对于的currentCache
总结:缓存所有部分都分析完了,其它部分难度和缓存差不多,当然就不在话下了,期间mark了MetaClass、MetaObject、typeAliasRegister,接下来看看这几个mark?还是先看看Maper接口吧,直接传入statement也太。。。
City city = sqlSession.selectOne("com.hhg.jerry.dao.CityDao.getById",1L);