mybatis的两个缓存:一级缓存与二级缓存
一级缓存
上篇我们已经提过一部分一级缓存的本质、使用、清除,这里简单提下失效原因
- 多次调用同一条查询之间有更新操作
- 一级缓存是SqlSession级别的, 不同 SqlSession 之间调用同一个查询用不了
- 同一个SqlSession, 但是非同一个SQL
- 手动执行缓存清除操作
二级缓存
二级缓存是多个SqlSession之间共享, 属于Configuration级别,而且先于一级缓存。
2.1 创建
通过前面的配置解析,这里我们直接看下 XmlConfigBuilder # configurationElement(XNode) 方法
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// here 1
cacheRefElement(context.evalNode("cache-ref"));
// here 2
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. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
缓存相关的就是这两个地方
<cache/>标签的子标签<cache-ref/>用于连接不同Mapper的命名空间
二级缓存的主要实现在<cache/>标签的子标签<cache/>
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);
}
}
- 前面一堆都是对cache标签属性的解析(包含默认值)
- 最后一个方法用于缓存的构建 MapperBuilderAssistant # useNewCache
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))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
这里构建了一个缓存 Cache, 存入了Configuration且赋值给了 currentCache,看下这个 currentCache 哪边用到了
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
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))
// here
// here
// here
.cache(currentCache);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
这里看到使用 MapperBuilderAssistant # addMappedStatement 将这个 currentCache 放入了 Configuration 的 mappedStatements 中。
2.2 使用
而二级缓存 CacheExecutor 的使用中 (CacheExecutor # query)
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// here
// here
// here
// 1.获取二级缓存
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
// 2.尝试从二级缓存取值
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 3.未能从二级缓存获取到值,调用原查询方法
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 4.查完反塞二级缓存
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 5.没有二级缓存,直接调用原查询方法
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
可以看到,二级缓存是从 MapperStatement 中取出来的
流程如下
- 存在二级缓存
1.尝试从二级缓存取值
2.未能从二级缓存获取到值,调用原查询方法
3.查完反塞二级缓存 - 不存在二级缓存,直接调用原查询方法
反塞与从缓存中取值是通过 TransactionalCacheManager 完成的
2.3 清除
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
2.4 二级缓存种类
- FifoCache:先进先出缓存淘汰策略的缓存
- LoggingCache:日志能力的缓存
- ScheduledCache:定时清空的缓存
- BlockingCache:阻塞式缓存
- SerializedCache:序列化能力的缓存
- SynchronizedCache:进行同步控制的缓存
2.4 实现之一 BlockCaching
public class BlockingCache implements Cache {
private long timeout;
private final Cache delegate;
private final ConcurrentHashMap<Object, ReentrantLock> locks;
public BlockingCache(Cache delegate) {
this.delegate = delegate;
this.locks = new ConcurrentHashMap<>();
}
}
- 持有一个Cache 的实现
- 维护了一把锁的Map
2.4.1获取锁 / 释放锁
private void acquireLock(Object key) {
Lock lock = getLockForKey(key);
if (timeout > 0) {
try {
boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
if (!acquired) {
throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());
}
} catch (InterruptedException e) {
throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
}
} else {
lock.lock();
}
}
private ReentrantLock getLockForKey(Object key) {
return locks.computeIfAbsent(key, k -> new ReentrantLock());
}
CurrentHashMap # computeIfAbsent 方法是线程安全的,可以确保多个线程进行put操作,得到的是同一个对象,也就是同一把锁
private void releaseLock(Object key) {
ReentrantLock lock = locks.get(key);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
2.4.2 获取对象 / 放入对象
@Override
public Object getObject(Object key) {
acquireLock(key);
Object value = delegate.getObject(key);
if (value != null) {
releaseLock(key);
}
return value;
}
如果没有获取到对象,则不释放锁,一直等待。这是一把可重入锁,每个此对象的获取线程会像链表一样链在此对象关联的监视器上,这是一种重量级锁。
那总不能一直等待,需要一个释放吧:
@Override
public void putObject(Object key, Object value) {
try {
delegate.putObject(key, value);
} finally {
releaseLock(key);
}
}
当有对象放入后,就执行释放锁的操作来唤醒等待线程。
还有一种写法是用 ReentrantLock 的 Condition,每个 Condition 就是一个 key
其他几种二级缓存的实现可以自行看下,我感觉这个阻塞用了一个重入锁比较好玩~~
2.5 失效原因
- 查询之间的更新操作会导致后面的查询用不到前面查询的二级缓存
- 前次SqlSession未提交或回滚导致后面的SqlSession的查询无法命中二级缓存
可以写几个SqlSession来实验一下,这里就不赘述了。
2.6 最后
这里还有一点实现,跟前面创建放一起可能会有点多,眼花缭乱的,看到这里还有兴趣就继续看下去吧
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 {
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);
}
if (readWrite) {
cache = new SerializedCache(cache);
}
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);
}
}
简直把装饰器模式使用的淋漓尽致啊,一次看个爽。
天地英雄气,千秋尚凛然。
网友评论