本文共 13961 字,大约阅读时间需要 46 分钟。
最近在复习之前写过的博客,看到Mybatis缓存的部分,想起了Cache的设计用到了装饰者模式,那么刚好我们就来好好看看装饰者模式。
我们之前已经说过了,要想让Mybatis的二级缓存生效,需要在Mapper文件中加入如下配置。
对于这个配置的作用,我们直接引用mybatis官方文档。
这个简单语句的效果如下:
所有的这些属性都可以通过缓存元素的属性来修改。比如:
这个更高级的配置创建了一个 FIFO 缓存,并每隔 60 秒刷新,存数结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此在不同线程中的调用者之间修改它们会 导致冲突。
可用的收回策略有:
默认的是 LRU。
flushInterval(刷新间隔)可以被设置为任意的正整数,而且它们代表一个合理的毫秒 形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。
size(引用数目)可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。默认值是 1024。
readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存 会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 false。
在Mybatis中,对Mapper配置文件的解析由XMLMapperBuilder来完成。
configurationElement
private void configurationElement(XNode context) { try { 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")); parameterMapElement(context.evalNodes("/mapper/parameterMap")); resultMapElements(context.evalNodes("/mapper/resultMap")); sqlElement(context.evalNodes("/mapper/sql")); buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e); } }
我们先看使用默认配置情况下的解析
cacheElement(context.evalNode("cache"));
private void cacheElement(XNode context) throws Exception { if (context != null) { String type = context.getStringAttribute("type", "PERPETUAL"); Class typeClass = typeAliasRegistry.resolveAlias(type); String eviction = context.getStringAttribute("eviction", "LRU"); Class evictionClass = typeAliasRegistry.resolveAlias(eviction); Long flushInterval = context.getLongAttribute("flushInterval"); Integer size = context.getIntAttribute("size"); boolean readWrite = !context.getBooleanAttribute("readOnly", false); Properties props = context.getChildrenAsProperties(); builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props); } }
我们可以看到在cacheElement中,有对cache节点下的属性的值进行读取并作出相应的处理。
然后通过这些处理过的信息调用 builderAssistant.useNewCache方法来创建Cache实例并注册到Configuration中。
我们来看MapperBuilderAssistant中的useNewCache方法
public Cache useNewCache(Class typeClass, Class evictionClass, Long flushInterval, Integer size, boolean readWrite, Properties props) { //如果在配置文件中没有显式指定 就使用PerpetualCache.class typeClass = valueOrDefault(typeClass, PerpetualCache.class); //如果没有显式指定,LruCache.class evictionClass = valueOrDefault(evictionClass, LruCache.class); //开始创建一个使用了层层装饰者模式包裹的Cache实例 Cache cache = new CacheBuilder(currentNamespace) .implementation(typeClass) .addDecorator(evictionClass) .clearInterval(flushInterval) .size(size) .readWrite(readWrite) .properties(props) .build(); //将cache加入到configuration中 configuration.addCache(cache); currentCache = cache; return cache; }
方法不难理解,就是先创建一个使用了装饰者模式得到的Cache实例,然后将这个实例注入到configuration中。
所以我们主要看是怎么创建的。
CacheBuilder
//默认情况下是PerpetualCache.class,这个一般不会在配置文件中更改public CacheBuilder implementation(Class implementation) { this.implementation = implementation; return this; }//默认的是LruCache 对应的属性是eviction 即设置回收策略/*LRU – 最近最少使用的:移除最长时间不被使用的对象。FIFO – 先进先出:按对象进入缓存的顺序来移除它们。SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。*/ public CacheBuilder addDecorator(Class decorator) { if (decorator != null) { this.decorators.add(decorator); } return this; }//设置缓存失效时间,默认是不设置 ,以毫秒为单位 public CacheBuilder clearInterval(Long clearInterval) { this.clearInterval = clearInterval; return this; }//对应的size属性 默认是1024 public CacheBuilder size(Integer size) { this.size = size; return this; } //对应readOnly属性 设置缓存是否为只读 public CacheBuilder readWrite(boolean readWrite) { this.readWrite = readWrite; return this; }//设置相应的自定义配置,通过属性文件 public CacheBuilder properties(Properties properties) { this.properties = properties; return this; }public Cache build() { //使用单个String形参的构造方法来初始化PerpetualCache,构造方法的入参是namesapceName setDefaultImplementations(); Cache cache = newBaseCacheInstance(implementation, id); //如果有自定义的参数,带上 setCacheProperties(cache); //添加用于设置回收策略的Cache,这里开始了第一次封装Cache if (PerpetualCache.class.equals(cache.getClass())) { // issue #352, do not apply decorators to custom caches for (Class decorator : decorators) { cache = newCacheDecoratorInstance(decorator, cache); setCacheProperties(cache); } //这里实现了cache装饰类的层层封装 cache = setStandardDecorators(cache); } //最后还是封装成一个LoggingCache类型的Cache,这个Cache已经经过层层装饰了 else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) { cache = new LoggingCache(cache); } return cache; }
来稍微看一下具体的装饰步骤
private Cache newCacheDecoratorInstance(Class cacheClass, Cache base) { Constructor cacheConstructor = getCacheDecoratorConstructor(cacheClass); try { return cacheConstructor.newInstance(base); } catch (Exception e) { throw new CacheException("Could not instantiate cache decorator (" + cacheClass + "). Cause: " + e, e); } }
private Cache setStandardDecorators(Cache cache) { try { MetaObject metaCache = SystemMetaObject.forObject(cache); if (size != null && metaCache.hasSetter("size")) { metaCache.setValue("size", size); } //如果设置了缓存失效时间,加一个ScheduledCache装饰类 if (clearInterval != null) { cache = new ScheduledCache(cache); ((ScheduledCache) cache).setClearInterval(clearInterval); } //如果并不是只读,加一个SerializedCache装饰类 if (readWrite) { cache = new SerializedCache(cache); } //然后加上具有日志功能的装饰类 cache = new LoggingCache(cache); //然后加上具有同步功能的装饰类 cache = new SynchronizedCache(cache); return cache; } catch (Exception e) { throw new CacheException("Error building standard cache decorators. Cause: " + e, e); } }
通过这个分析,我们可以得到基本的Cache的装饰链
LoggingCache-> SynchronizedCache->SerializedCache->LoggingCache->ScheduledCache->LRUCache->PerpetualCache。
也就是说其实最后最终调用的还是PerpetualCache里的方法。
最后说一下:加入到configuration的过程。 configuration.addCache(cache);
public void addCache(Cache cache) { caches.put(cache.getId(), cache); }
cache.getId(),id是在初始化PerpetualCache这个Cache的时候通过构造方法传入的,id的值是这个Mapper的命名空间名。所以这样我们才能在MappedStatement中找对应的cache.
我们再回头看看我们曾经说过的Mybatis二级缓存。
我们直接看CacheExecutor的query方法。
publicList query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { //获取该Mapper命名空间下的缓存 Cache cache = ms.getCache(); //如果有,开始准备使用缓存了 if (cache != null) { //如果是select语句,默认情况下不清缓存,但是如果在Mapper文件中有在select配置中配置属性fluchCache为true的话,会执行缓存清理的动作。具体对这个是否需要请缓存配置的解析在XMLStatementBuilder类的parseStatementNode方法中。 flushCacheIfRequired(ms); //如果确定这个select语句使用了Cache并且方法参数中没有resultHandler类型的参数。就使用Cache了 if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, parameterObject, boundSql); @SuppressWarnings("unchecked") //从缓存中找 List list = (List ) tcm.getObject(cache, key); //如果没找到,去查询 if (list == null) { list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //将查询的结果放入Cache中 tcm.putObject(cache, key, list); // issue #578. Query must be not synchronized to prevent deadlocks } //返回结果 return list; } } return delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
方法的大概流程已经在代码块中解释了。
我们主要看下这个过程中对缓存的使用。
先看是否执行缓存清理的方法
flushCacheIfRequired
private void flushCacheIfRequired(MappedStatement ms) { Cache cache = ms.getCache(); //如果需要清理缓存,update的时候默认清理缓存 if (cache != null && ms.isFlushCacheRequired()) { tcm.clear(cache); } } public void clear(Cache cache) { getTransactionalCache(cache).clear(); } private TransactionalCache getTransactionalCache(Cache cache) { TransactionalCache txCache = transactionalCaches.get(cache); if (txCache == null) { txCache = new TransactionalCache(cache); transactionalCaches.put(cache, txCache); } return txCache; }
tcm对应的是一个TransactionalCacheManager对象,可以把他看做一个TransactionalCache池。在CachingExecutor中是这样初始化的。
private TransactionalCacheManager tcm = new TransactionalCacheManager();
所以在初始化Executor执行器的时候,tcm也就存在了。
TransactionalCacheManager中的getTransactionalCache方法,这个方法很明显是构造一下cache和TransactionalCache的关系,存入Map中,而且TransactionalCache将cache作为构造方法的入参。
所以这里,每一个cache就对应着一个包含cache的TransactionalCache对象。
TransactionalCache的clear方法
@Override public void clear() { reset(); clearOnCommit = true; } private void reset() { clearOnCommit = false; entriesToRemoveOnCommit.clear(); entriesToAddOnCommit.clear(); }
clear方法最后把clearOnCommit置为true.
并且把entriesToRemoveOnCommit和entriesToAddOnCommit这两个Map中的元素都清空了、。
tcm.putObject(cache, key, list);
将结果放入到缓存的方法。
public void putObject(Cache cache, CacheKey key, Object value) { getTransactionalCache(cache).putObject(key, value); }
调用的是TransactionalCache的putObject方法
@Override public void putObject(Object key, Object object) { //移除entriesToRemoveOnCommit中key为当前的CacheKey的元素 entriesToRemoveOnCommit.remove(key); //将当前的CacheKey为keey,并且构造一个包含cache,CacheKey和查询结果的AddEntry为Value存进entriesToAddCommit entriesToAddOnCommit.put(key, new AddEntry(delegate, key, object)); }
所以到这里呢,putObject方法呢,实际上只是把元素存在位于TranscationCache中的entriesToRemoveOnCommit这个Map中。
注意,此时并没有将查询结果放入到我们之前说的装饰器模式构造的cache中。
tcm.getObject(cache, key);
在缓存中查找是否有当前CacheKey对应的值。
调用的是ransactionalCache的getObject方法
@Override public Object getObject(Object key) { if (clearOnCommit) return null; // issue #146 return delegate.getObject(key); }
当clearOnCommit为true的时候就直接返回了,即代表着如果是udpate语句或者设置了flushCache属性为true的话,就甭想着在cache里面找,赶紧再跟数据库做交互呀。
如果不为true,那么就要在cache中去寻找当前CacheKey对应的值了。
而你会发现,之前的putObject并没有把值存入到cache中呀,所以这里根本不用分析了,肯定是找不到的。是的,不信你做个试验,你会发现第二次查询的时候依然是直接跟数据库做交互,不会在缓存中取。
事实上,如果你想要让Mybatis的二级缓存失效,你需要手动的在查询后执行session.commit操作。
public void commit() { commit(false); } public void commit(boolean force) { try { executor.commit(isCommitOrRollbackRequired(force)); dirty = false; } catch (Exception e) { throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }/*当调用无参的commit方法时,force指定是false使用DefaultSqlSessionFactory的无参openSession()方法时,autoCommit为false如果使用的是有参的openSession(boolean autoCommit)方法,那就取决于传入的参数了所以也就是说,如果没有显式的将dirty设置为true的时候方法返回false***注意 在调用update方法的时候,会把dirty设置为true,这时候方法就会返回true了。*/ private boolean isCommitOrRollbackRequired(boolean force) { return (!autoCommit && dirty) || force; }
我们可以看到,其实session.commit()实际上调用的是executor的commit方法。而具体传什么参数给executor的commit方法是由isCommitOrRollbackRequired方法决定的。
public void commit(boolean required) throws SQLException { delegate.commit(required); tcm.commit(); }//BaseExecutorpublic void commit(boolean required) throws SQLException { if (closed) throw new ExecutorException("Cannot commit, transaction is already closed"); //清理一级缓存 clearLocalCache(); //BatchExecutor有重写这个方法里的具体方法,目前没有用到,先不分析 flushStatements(); //这里就执行 connection.commit();操作 非查询数据的时候会用到 if (required) { transaction.commit(); } }
我们这里不去过多的分析executor的commit()方法。
而是要分析
tcm.commit();
public void commit() { for (TransactionalCache txCache : transactionalCaches.values()) { txCache.commit(); } }
调用这个会话中的TransactionalCache池里的所有TransactionalCache的commit()方法。
public void commit() { //如果clearOnCommit为true,那么就清理掉所有的放在cache里的数据 if (clearOnCommit) { delegate.clear(); } else { //将要从cache中要移除的元素移除了 for (RemoveEntry entry : entriesToRemoveOnCommit.values()) { entry.commit(); } } //将要添加到cache中的元素添加到cache中 for (AddEntry entry : entriesToAddOnCommit.values()) { entry.commit(); } reset(); }
AddEntry是TransactionalCache的一个静态内部类
private static class AddEntry { private Cache cache; private Object key; private Object value; public AddEntry(Cache cache, Object key, Object value) { this.cache = cache; this.key = key; this.value = value; } public void commit() { cache.putObject(key, value); } }
很明显他的commit方法就是在调用cache的putObject方法。
那么这个putObject方法就是我们要看的。
LoggingCache
@Override public void putObject(Object key, Object object) { delegate.putObject(key, object); }
我们一开始说了,我们得到的cache是一个LoggingCache实例。他的delegate属性是SynchronizedCache,他保证放入数据时候的线程安全。而如果没有设置readOnly为true的时候,会接着调用SerializedCache来做一个序列化的操作。如果设置了缓存过时时间,会调用ScheduledCache来做一个查看是否缓存失效的判断。接着就到了设置回收策略的Cache,我们这里默认是LRUCache,即最近最少使用,内部是使用LinkedHashMap实现的,并不复杂,回收策略是必须的,因为缓存不可能无限大,一定要设置一个max值。然后才到PerpetualCache,这才到了我们把CacheKey和查询结果存放起来的类。
private Map
很简单,就是把键值对放到cache这个Map中。
而对于delegate.getObject(key);的具体过程,当然是和delegate.putObject(key, object);相似的,就不细说了。
这里我们看到了每一个Cache都有自己的功能,而PerpetualCache只有最原始的存放数据和得到数据的功能。为了让Cache的功能更强大,我们使用了装饰者模式来一步步的丰富Cache的功能。
转载地址:http://idmvb.baihongyu.com/