Mybatis源码阅读3 --- 缓存的一切

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);

猜你喜欢

转载自blog.csdn.net/lnlllnn/article/details/80707161