在mybatis中,一级缓存是默认开启的,无法关闭;二级缓存可以选择开启或者关闭,这篇博客主要介绍二级缓存的原理,具体的使用,不再介绍了
二级缓存在使用的时候,需要先进行解析,对二级缓存的处理,主要体现在几个方面
org.apache.ibatis.builder.xml.XMLMapperBuilder#cacheElement
这是解析节点的方法,至于怎么调用过来的,可以看下前面mybatis源码的博客,有介绍过
这里解析的是mapper.xml中的节点
private void cacheElement(XNode context) {
if (context != null) {
String type = context.getStringAttribute("type", "PERPETUAL");
//这里是根据type从存放别名的map中获取对应的class类
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.useNewCache()这个方法,把所有属性聚合到一个cache对象中
在useNewCache方法中,会通过
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();
这么一段代码,初始化一个cache对象,所以我们需要注重看的是这里的cache对象是怎么初始化的,我们来看下其build()方法
public Cache build() {
// 设置默认的缓存实现类
setDefaultImplementations();
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// issue #352, do not apply decorators to custom caches
// 判断是否是PerpetualCache
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
// 在这里,会开始包装二级缓存需要使用的cache
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache(cache);
}
return cache;
}
这个方法中,简单看下逻辑:先设置默认的缓存实现类,然后判断当前的implementation是否是PerpetualCache;如果是,核心逻辑在
cache = setStandardDecorators(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);
}
/**
* 在lruCache基础之上,包装了一层SerializedCache
*/
if (readWrite) {
cache = new SerializedCache(cache);
}
/**
* 在SerializedCache基础之上,包装了一层LoggingCache
*/
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);
}
}
可以看到,这里在初始化cache对象的时候,会一层套一层,各种cache对象,所以,我们可以理解为,最后我们返回的是BlockingCache(如果开启了blocking),但是这个对象的内部,会套了多层cache
为什么会说到这个点呢?因为我们知道,如果开启了二级缓存,会先调用org.apache.ibatis.executor.CachingExecutor#query()这个方法,如果二级缓存中,没有获取到结果,会调用到org.apache.ibatis.executor.BaseExecutor#query()方法,所以,我们可以认为CachingExecutor是二级缓存的处理逻辑
那为什么二级缓存开启了,会先调用CachingExecutor呢?就要看初始化executor的逻辑
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSession()
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource
org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
// 默认的executor是SimpleExecutor
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 {
executor = new SimpleExecutor(this, transaction);
}
//如果二级缓存开启,创建cachingExecutor,这里的executor会被包装在cachingExecutor中; 这里的cacheEnabled,是在解析全局配置文件时,解析节点时,赋值的,如果设置了cacheEnable为true,这里的属性值就会是true
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
/**
* 上面的逻辑是根据不同的executorType类型来初始化不同的执行器
* 下面是为executor生成动态代理对象
*/
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
这个方法,我们可以看到,在初始化executor对象时,如果开启了二级缓存,会在executor外面再包装一层CachingExecutor;所以,这也是 为什么在sql执行的时候,会先调用到CachingExecutor的query()方法
在sql执行时,会通过sqlSession的selectList/selectOne来完成,在内部,会调用executor的query()方法,所以对于开启了二级缓存的场景下,会先调用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) {
/**
* 这里的cache是mybatis的二级缓存。如果二级缓存不为null,并且二级缓存开启了,就从二级缓存中获取数据,否则,就从一级缓存中取数据,如果一级缓存中
* 也没有数据,就从数据库中获取,并将查询结果存入到一级缓存和二级缓存中
*
* flushCacheIfRequired():是指xml中,select、update、delete、insert节点的flushCache属性,默认:insert、delete、update是true,select是false(这里的默认值是在解析xml文件的时候,添加的)
*/
flushCacheIfRequired(ms);
/**
* 这里对应的是useCache属性;该属性值:如果是select标签,是true,非select标签为false
* mapper.xml文件中 标签的属性
*/
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
/**
* 下面是真正从二级缓存中查询数据,是通过责任链 + 装饰器模式实现的
* 类似于套娃的形式,一层套一层
*/
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
/**
* 调用一级缓存;
* 从一级缓存中查到数据之后,调用tcm的put方法,这里的put是把对应的对象,放到了一个中间变量map集合中,这样做,是为了防止脏数据
* 只有在事务提交的时候,才会真正的把数据写入到二级缓存
*/
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//调用一级缓存
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
这里的注释也比较明白,简单来说,就是先从二级缓存中查询,如果二级缓存可以查到,就返回,查不到,就去一级缓存中查询,所谓的一级缓存,是BaseExecutor来处理的,我们先看下从二级缓存获取的逻辑:
这里可以看到,就是上面解析节点时,包装的层数,现在在使用的时候,会一层一层的调用,最后实际是存储在PerpetualCache中