Mybatis提供一级和二级缓存。
一级缓存只存在session级别,底层用一个Map存储。默认开启。
二级缓存与namespace(一个Mapper接口)对应,必须显示commit才生效。默认不开启。
上面这幅图很好的描述了MyBatis缓存的执行流程。
一次查询,先从二级缓存找,然后一级缓存最后数据库。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
//1正常会走到这里
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
//2 对上面的excutor进行保装 返回CachingExecutor。
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
上面2处用到了装饰器模式,对原始类SimpleExecutor包了一层。当下次调用时,就使用了委托模式进行调用。
下面我们看看一次查询的流程。最终会走到
CachingExecutor
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
{//1这里拿到二级缓存对象
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
//2查寻之前,先到二级缓存里找一下
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//3查寻数据库
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//4 结果放进缓存
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//6一级缓存直接走这里,跳过上面if
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
上面的逻辑很简单,但要我们自己设计一个缓存系统该怎么设计呢?
我们先看下MyBatis缓存的设计
MyBatis采用了委托机制和装饰者模式来设计的。
基础缓存:
为缓存数据最终存储的处理类,默认为 PerpetualCache,基于Map存储;可自定义存储处理,如基于EhCache、Memcached等;
排除算法缓存:
当缓存数量达到一定大小后,将通过算法对缓存数据进行清除。默认 Lru ,提供有 fifo 等
装饰器缓存:
缓存put/get处理前后的装饰器,如使用 LoggingCache 输出缓存命中日志信息、使用 SerializedCache 对 Cache的数据 put或get 进行序列化及反序列化处理、当设置flushInterval(默认1/h)后,则使用 ScheduledCache 对缓存数据进行定时刷新等
一般缓存框架的数据结构基本上都是 Key-Value 方式存储,MyBatis 对于其 Key 的生成采取规则为:
[hash:checksum:mappedStementId:offset:limit:execSql:qryParams]。
对于并发 Read/Write 时缓存数据的同步问题,MyBatis 默认基于 JDK/concurrent中的ReadWriteLock
那么,怎么把上面的组建给结合起来呢?
加载配置文件时,configuration中有这样一段代码来解析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);
}
}
看看useNewCache是怎么实例化的。
public Cache useNewCache(Class typeClass,
Class evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
Properties props) {
typeClass = valueOrDefault(typeClass, PerpetualCache.class);
evictionClass = valueOrDefault(evictionClass, LruCache.class);
// 这里构建 Cache 实例采用 Builder 模式,每一个 Namespace 生成一个 Cache 实例
Cache cache = new CacheBuilder(currentNamespace)
// Builder 前设置一些从XML中解析过来的参数
.implementation(typeClass)
.addDecorator(evictionClass)
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.properties(props)
// 再看下面的 build 方法实现
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
public Cache build() {
setDefaultImplementations();
// 创建基础缓存实例
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// 缓存排除算法初始化,并将其委托至基础缓存中
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
// 标准装饰器缓存设置,如LoggingCache之类,同样将其委托至基础缓存中
cache = setStandardDecorators(cache);
// 返回最终缓存的责任链对象
return cache;
}
这里我们看到来装饰器模式的威力。相当于把一个对象放到下一个要构造对象的构造方法中,但这样 是不是强耦合呢?
可见,所有构建的缓存实例已经通过责任链方式将其串连在一起,各 Cache 各负其责、依次调用,直到缓存数据被 Put 至 基础缓存实例中存储。