深入理解MyBatis-一级缓存
序言
在上一篇中我们介绍了Executor的整体设计与实现,其中涉及到的概念包括SqlSession、Executor以及StatementHandler。但是在上一篇的章节中,主角Executor的戏份并不算多,我把重点放在了讲解Executor的整体设计与实现。这会让我们误以为Executor的功能其实很简单,其实并不然,除了Executor设计理念之外,其还涉及了其他三个重要部分,分别是一二级缓存、延迟加载以及事务管理,这三者的都是算得上是围绕着Executor的重点。其次,在查询中,还有一个比较重要的功能实现,那就是嵌套子查询,而嵌套子查询的实现又与一级缓存、延迟加载及Executor后续核心组件的实现息息相关。所以,我将在接下来的两篇章节中,分析介绍MyBatis的一级缓存及二级缓存的设计与实现。之后,在介绍完StatementHandler、MyBatis映射体系等核心概念之后,再回过头来,分析MyBatis的嵌套子查询、懒加载以及延迟加载是如何实现的。相信到时候,读者不仅会对嵌套查询的理解更为深刻,也能更全面的理解MyBatis的核心组件间是如何协同合作与分工的。
1、一级缓存
1、概述
在谈及MyBatis的缓存时,相信不少Java从业者会不自觉的联想到Redis。这本身并没有错,因为现阶段绝大多数企业选择Redis来作为MyBatis的外部缓存中间件。但这会使大多数Java初级工程师或者初学者,如我自己本身,一开始会误以为MyBatis的缓存就是依赖于Redis来实现的(虽然是我学艺不精导致的,但不排除有其他的同学会存在着相同的误解)。这其中存在着两个误区,一是:使用Redis作为MyBatis的外部缓存其实是MyBatis二级缓存的扩展,二是:外部缓存的实现并不是MyBatis缓存的全部,在MyBatis中存在着一级缓存及二级缓存,而一级缓存本身并不依赖于任何的其他外部软件。
在介绍一级缓存的开头,我觉得有必要声明,MyBatis的一级缓存是线程级的缓存,换个大众点的说法,一级缓存是查询缓存。
为什么这么说呢?还记得在上一篇中,我们提及SqlSession是一个作用域为会话级别的对象,是线程不安全的。在每次通过SqlSessionFactory获取一个SqlSession时,我们拿到的都是一个新的SqlSession对象。而每个SqlSession与Executor是一对一的对象,Executor也是线程不安全的。一级缓存的实现逻辑又都在Executor中,所以说,一级缓存是一个线程的缓存。又因为在MyBatis中,只有查询时,才会使用到一级缓存,所以又称一级缓存为查询缓存。
也就是说,一级缓存只在一次会话查询中有效。只有在一次会话查询中,我们执行了多次条件及Sql完全相同的情况下,一级缓存才能发挥它的作用,以提高查询性能。其大概的执行过程如下图所示。
2、一级缓存的命中条件与场景
在MyBatis中,一级缓存时默认启用的,虽然可以通过将localCacheScop的级别配置为statement来关闭一级缓存,但是这样子做并不能完全使一级缓存失效。为了验证我说的话,我们再来看看上个章节中的BaseExecutor的源码。注意,我在下方代码中删除了所有与一级缓存无关的代码。
public abstract class BaseExecutor implements Executor {
//本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询(一级缓存)
protected PerpetualCache localCache;
protected Configuration configuration;
//查询堆栈
protected int queryStack = 0;
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
// ...
// 每当执行update操作时,清除本地缓存
clearLocalCache();
return doUpdate(ms, parameter);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//得到绑定sql
BoundSql boundSql = ms.getBoundSql(parameter);
//创建缓存Key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
//查询
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {utorException("Executor was closed.");
}
// 当子查询栈为0时,且配置了flushCache为true时,清空一级缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
//加一,这样递归调用到上面的时候就不会再清局部缓存了
queryStack++;
//先根据cachekey从localCache去查
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
//若查到localCache缓存,处理localOutputParameterCache
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();
}
//清空延迟加载队列
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// 当localCacheScop级别配置为Statement时,每次执行完查询后,都会清除本地缓存。
clearLocalCache();
}
}
return list;
}
@Override
public boolean isCached(MappedStatement ms, CacheKey key) {
return localCache.getObject(key) != null;
}
@Override
public void commit(boolean required) throws SQLException {
// ...
clearLocalCache();
// ...
}
@Override
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
// 回滚时,清除一级缓存
clearLocalCache();
flushStatements(true);
} finally {
if (required) {
transaction.rollback();
}
}
}
}
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
//从数据库查
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
//先向缓存中放入占位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 最后删除占位符
localCache.removeObject(key);
}
// 加入缓存
localCache.putObject(key, list);
return list;
}
}
在大概的浏览完上面带有注释的代码后,看着下方几个要点,再对着代码过一遍。
- 在BaseExecutor中,维护了一个PerpetualCache来缓存查询结果。PerpetualCache属于MyBatis中Cache体系里,最终实现结果存储的实现。这个在下一章节中会讲。在PerpetualCache类中,维护了一个HashMap来存储查询数据。
- 在执行最终query方法之前(参数最多的query方法),会调用createCacheKey方法,来构建一个CacheKey对象,用该对象来作为一次一级缓存的唯一key。createCacheKey在上方源代码中已经被我去除,留在下面详细介绍。
- 加倍注意所有调用了clearLocalCache的地方,其中包括update一处、query两处、commit以及rollback各一处,也就是说,每当执行update、commit和rollback操作时,均会无条件清空一级存储。而在query方法中,清除本地缓存是有条件的。第一处是MappedStatement对象配置了flushCache等于true时,才会清空一级缓存,第二处的条件是localCacheScop级别配置为statement时。此外,这两个条件还有一个共同的条件就是需要嵌套子查询栈长度为0时才会执行。至于为什么这么做,我们留着以后再讲。
接下来,我们来分析createCacheKey,看看一级缓存的唯一键与哪些条件相关。
//创建缓存Key
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
//MyBatis 对于其 Key 的生成采取规则为:[mappedStementId + offset + limit + SQL + queryParams + environment]生成一个哈希码
cacheKey.update(ms.getId());
cacheKey.update(Integer.valueOf(rowBounds.getOffset()));
cacheKey.update(Integer.valueOf(rowBounds.getLimit()));
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
//模仿DefaultParameterHandler的逻辑,不再重复,请参考DefaultParameterHandler
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 逐个参数值加入cacheKey
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
可以看到,CacheKey与MappedStatement、参数、RowBounds、Sql和Environment相关。而在CacheKey的doUpdate源码中,主要是通过这些相关条件参数来生成hashCode,也就是说,只要其中任何一个条件不满足,都会导致无法命中一级缓存。
总结
对于以上提及的一级缓存命中条件,大致可以分为以下两类:
-
运行时参数相关
- 同一个会话
- Sql语句相同
- 参数相同
- 相同的MappedStatement,即StatementID得相同
- RowBounds(返回行范围)得相同
- Environment(运行时环境)得相同】
-
操作与配置相关
- 未手动清空缓存(事务提交与回滚)
- 未配置flushCache等于true
- 未执行update操作
- 缓存作用域不是Statement
下图时关于与一级缓存相关的执行流程