二级缓存源码分析
Mybatis二级缓存的工作流程和前文提到的一级缓存类似,只是在一级缓存处理前,用CachingExecutor装饰了BaseExecutor的子类,实现了缓存的查询和写入功能,所以二级缓存直接从源码开始分析。
源码分析
源码分析从CachingExecutor的query方法展开,源代码走读过程中涉及到的知识点较多,不能一一详细讲解,可以在文后留言,我会在交流环节更详细的表示出来。
CachingExecutor的query方法,首先会从MappedStatement中获得在配置初始化时赋予的cache。
Cache cache = ms.getCache();
本质上是装饰器模式的使用,具体的执行链是
SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache。
以下是具体这些Cache实现类的介绍,他们的组合为Cache赋予了不同的能力。
SynchronizedCache: 同步Cache,实现比较简单,直接使用synchronized修饰方法。 LoggingCache: 日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。 SerializedCache: 序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。 LruCache: 采用了Lru算法的Cache实现,移除最近最少使用的key/value。 PerpetualCache: 作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap。
然后是判断是否需要刷新缓存,代码如下所示。
flushCacheIfRequired(ms);
在默认的设置中SELECT语句不会刷新缓存,insert/update/delte会刷新缓存。进入该方法。代码如下所示。
private void flushCacheIfRequired(MappedStatement ms) { Cache cache = ms.getCache(); if (cache != null && ms.isFlushCacheRequired()) { tcm.clear(cache); } }
Mybatis的CachingExecutor持有了TransactionalCacheManager,即上述代码中的tcm。
TransactionalCacheManager中持有了一个Map,代码如下所示。
private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
这个Map保存了Cache和用TransactionalCache包装后的Cache的映射关系。
TransactionalCache实现了Cache接口,CachingExecutor会默认使用他包装初始生成的Cache,作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。
在TransactionalCache的clear,有以下两句。清空了需要在提交时加入缓存的列表,同时设定提交时清空缓存,代码如下所示。
@Override public void clear() { clearOnCommit = true; entriesToAddOnCommit.clear(); }
CachingExecutor继续往下走,ensureNoOutParams主要是用来处理存储过程的,暂时不用考虑。
if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, parameterObject, boundSql);
之后会尝试从tcm中获取缓存的列表。
List<E> list = (List<E>) tcm.getObject(cache, key);
在getObject方法中,会把获取值的职责一路向后传,最终到PerpetualCache。如果没有查到,会把key加入Miss集合,这个主要是为了统计命中率。
Object object = delegate.getObject(key); if (object == null) { entriesMissedInCache.add(key); }
CachingExecutor继续往下走,如果查询到数据,则调用tcm.putObject方法,往缓存中放入值。
f (list == null) { list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 }
tcm的put方法也不是直接操作缓存,只是在把这次的数据和key放入待提交的Map中。
@Override public void putObject(Object key, Object object) { entriesToAddOnCommit.put(key, object); }
从以上的代码分析中,我们可以明白,如果不调用commit方法的话,由于TranscationalCache的作用,并不会对二级缓存造成直接的影响。因此我们看看Sqlsession的commit方法中做了什么。代码如下所示。
@Override public void commit(boolean force) { try { executor.commit(isCommitOrRollbackRequired(force));
因为我们使用了CachingExecutor,首先会进入CachingExecutor实现的commit方法。
@Override public void commit(boolean required) throws SQLException { delegate.commit(required); tcm.commit(); }
会把具体commit的职责委托给包装的Executor。主要是看下tcm.commit(),tcm最终又会调用到TrancationalCache。
public void commit() { if (clearOnCommit) { delegate.clear(); } flushPendingEntries(); reset(); }
看到这里的clearOnCommit就想起刚才TrancationalCache的clear方法设置的标志位,真正的清理Cache是放到这里来进行的。具体清理的职责委托给了包装的Cache类。之后进入flushPendingEntries方法。代码如下所示。
private void flushPendingEntries() { for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) { delegate.putObject(entry.getKey(), entry.getValue()); } ................ }
在flushPendingEntries中,就把待提交的Map循环后,委托给包装的Cache类,进行putObject的操作。 后续的查询操作会重复执行这套流程。如果是insert|update|delete的话,会统一进入CachingExecutor的update方法,其中调用了这个函数,代码如下所示,因此不再赘述。
private void flushCacheIfRequired(MappedStatement ms)
总结
Mybatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到Mapper级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。 Mybatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用的条件比较苛刻。 在分布式环境下,由于默认的Mybatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将Mybatis的Cache接口实现,有一定的开发成本,不如直接用Redis,Memcache实现业务上的缓存就好了。
全文总结
本文介绍了Mybatis的基础概念,Mybatis一二级缓存的使用及源码分析,并对于一二级缓存进行了一定程度上的总结。 最终的结论是Mybatis的缓存机制设计的不是很完善,在使用上容易引起脏数据问题,个人建议不要使用Mybatis缓存,在业务层面上使用其他机制实现需要的缓存功能,让Mybatis老老实实做它的ORM框架就好了哈哈。
|