这个章节中的源码在第二章和第三章部分出现过,最好先看一遍。
二级缓存
总所周知的,在进行数据库查询的时候,首先要先查询缓存。查询缓存的操作首先发生在执行器CachingExecutor。
Executor默认外层的CachingExecutor嵌套这内层的SimpleExecutor。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
//SIMPLE 就是普通的执行器;
//REUSE 执行器会重用预处理语句(PreparedStatement);
//BATCH 执行器不仅重用语句还会执行批量更新。
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);
}
//cacheEnabled表示全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。可以在settings中配置
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
那么要开启二级缓存,cacheEnabled要为true。这样才能生成CachingExecutor执行缓存操作。在进行查询操作时,首先会去获取缓存,注意MappedStatement.getCache()这个方法。MappedStatement是用来存储我们的sql语句信息,也是在解析mapper.xml是生成的。
//CachingExecutor.query
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//这个首先获取二级缓存,默认是不开起的,需要在手动在mapper.xml配置
Cache cache = ms.getCache();
//没有配置就是null
if (cache != null) {
//判断缓存是否需要刷新,flushCache是select的一个参数,如果为true,那么每次都会清空缓存
flushCacheIfRequired(ms);
//判断是否使用缓存,如select就能配置useCache属性,而insert,update,delete就没有
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//尝试取缓存中查找,如果没有,调用查找方法
//tcm并不是直接的缓存,可以把它认为是事务缓存的管理器,用来记录当前cache命中的缓存和没有命中的缓存。
//通过它间接获取cache的值
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
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);
}
如果这个ms.getCache()返回空,if里面的语句就都不执行了,就不会使用二级缓存,那这个cache什么时候为空呢?既然MappedStatement是在解析配置文件的阶段就生成的,那么就要回到解析时候的源码。
二级缓存的创建
//XMLMapperBuilder.configurationElement
private void configurationElement(XNode context) {
try {
//mapper的缓存信息,命名空间等会被临时保存到MapperBuilderAssistant中,最后把这些公用的信息在存到MappedStatement中
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//MapperBuilderAssistant该类用来协助处理mapper.xml,并保存一些中间信息
//解析每个mapper.xml都会创建一个自己的builderAssistant
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"));
//在解析增删改查节点时,每个节点都会生成一个mapperStatement对象并保存到configuration类中.
//mapperStatement保存这这个节点的全部信息,如id,fetchSize,timeout
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
在创建cacheElement命名空间的的缓存时,就换创建一个Cache,这个cache属于这个mapper.xml,并保存在builderAssistant中
private void cacheElement(XNode context) {
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);
}
}
public Cache useNewCache(Class<? extends Cache> typeClass,Class<? extends Cache> evictionClass,Long flushInterval,Integer size,boolean readWrite,boolean blocking,Properties props) {
//创建缓存
//根据cache中的eviction会创建不同类型的缓存
//LRU(默认,LruCache) – 最近最少使用:移除最长时间不被使用的对象。
//FIFO(FifoCache) – 先进先出:按对象进入缓存的顺序来移除它们。
//SOFT(SoftCache) – 软引用:基于垃圾回收器状态和软引用规则移除对象。
//WEAK(WeakCache) – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
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);
//给builderAssistant中的参数currentCache赋值
currentCache = cache;
return cache;
}
生成mappedStatement代码,会直接获取属于该mapper的cache而不是去创建新的。
//XMLStatementBuilder.parseStatementNode
public void parseStatementNode() {
//解析一堆select上的标签,代码略,详细在第二章查看
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
//记录标信息,查询之前是否刷新缓存
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
//记录标信息,是否使用缓存
.useCache(valueOrDefault(useCache, isSelect))
//获取builderAssistant中缓存
.cache(currentCache);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
public Builder cache(Cache cache) {
mappedStatement.cache = cache;
return this;
}
回到之前的查询代码,小节一下,要使用二级缓存需满足以下几个条件:
- settings中的配置cacheEnabled要为true(默认true),这样才能生成CacheExecutor,执行二级缓存相关代码。
- 在mapper.xml中需要配置cache标签,才能创建属于这个mapper的Cache对象,每个mapper都有一个自己的缓存。根据配置的参数会选择不同的缓存。
- select中的useCache标签要为true(默认值:对 select 元素为 true),这样才能在通过后续的逻辑判断。
二级缓存的结构
通过上文可知,我们可以选4种类型的缓存。那么如果防止缓存数据过多导致内存溢出呢?由于默认选择的缓存机制是LruCache(最近最少使用),那么就用这个举例。
缓存的接口,每个缓存都要实现这个接口,封装了基础的获取清除元素等方法。
public interface Cache {
String getId();
void putObject(Object key, Object value);
Object getObject(Object key);
Object removeObject(Object key);
void clear();
int getSize();
default ReadWriteLock getReadWriteLock() {
return null;
}
LruCache也是要实现这个接口,但是可以发现,它除了自己本身维护了一个keyMap用来缓存数据,还有一个类型为delegate的Cache。也许更应该将LruCache看成一个缓存的装饰器。
public class LruCache implements Cache {
private final Cache delegate;
private Map<Object, Object> keyMap;
private Object eldestKey;
//...方法略
}
那么内部的这个Cache是什么类型的缓存呢?从刚才的创建缓存的代码可以找到答案。
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))
//...
.build();
//...
}
内层的Cache是PerpetualCache,这个缓存可以看作是缓存的最基本实现,一级缓存就是使用它作为缓存的容器。而LruCache是在其上的进一步包装。
public class PerpetualCache implements Cache {
private final String id;
private Map<Object, Object> cache = new HashMap<>();
//...方法略
}
而在LruCache之上,根据配置文件还可能包装多层的缓存。
//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;
}
private Cache setStandardDecorators(Cache cache) {
try {
//设置重新缓存的大小(初始化1024)
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
//flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
//readOnly(只读)默认属性false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。
if (readWrite) {
cache = new SerializedCache(cache);
}
//固定嵌套的两层,日志缓存和同步缓存
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
//blocking,数据块将逐块计算,使得存储器访问是一个具有高内存局部性的小邻域。
if (blocking) {
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
就这样一层一层的嵌套,最后的缓存就变成了多层的缓存结构。
二级缓存数据的清理
还是用LruCache举例。
public LruCache(Cache delegate) {
this.delegate = delegate;
setSize(1024);
}
在其构造方法中可以看出,这里规定了最大容量为1024
public void setSize(final int size) {
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
//如果当前的map大小大于1024
boolean tooBig = size() > size;
if (tooBig) {
//记录最老的键值对
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
它选择了Map类型的容器作为缓存,并重写了removeEldestEntry方法。该方法通过给定一定的条件,如果返回true,那么就移除最老的键值对。
public void putObject(Object key, Object value) {
delegate.putObject(key, value);
cycleKeyList(key);
}
private void cycleKeyList(Object key) {
//keyMap超过容量会自行移除
keyMap.put(key, key);
if (eldestKey != null) {
//delegate是PerpetualCache,需要手动移除
delegate.removeObject(eldestKey);
eldestKey = null;
}
}
每当往缓存中添加新数据时,都会判断最老的键值对是否存在(如果容量达到1024,再往里添加就存在了),那么就将老的键值对移除,这样就能保证缓存数量最大为1024.
一级缓存
BaseExecutor是所有Executor的父类,当执行query方法时会用到一级缓存。也叫做localCache本地缓存。这个缓存在BaseExector创建的时候一起创建,并在一次会话结束后随之一起销毁。
//BaseExecutor构造方法
//一级缓存使用的是PerpetualCache,该类的结构在上文
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
在进行查询之前,首先会在localCache中进行查询,这个没有用到配置文件的信息,所以一级缓存是不能关闭的。
//BaseExecutor.query
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}