ORM - MyBatis一级缓存实现机制详解
一级缓存概念
Mybatis对缓存提供支持,但是在没有配置的默认情况下,它只开启一级缓存,一级缓存只是相对于同一个SqlSession而言。所以在参数和SQL完全一样的情况下,我们使用同一个SqlSession对象调用一个Mapper方法,往往只执行一次SQL,因为使用SelSession第一次查询后,MyBatis会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession都会取出当前缓存的数据,而不会再次发送SQL到数据库。
- 一级缓存(local cache),即本地缓存,作用域默认为SqlSession。当Session flush或close后,该session中的所有Cache将被清空。
- 本地缓存不能被关闭,但可以调用clearCache()来清空本地缓存,或者改变缓存的作用域。
- 在 MyBatis3.1 之后,可以配置本地缓存的作用域,在 MyBatis.xml 中配置。
localCacheScope :MyBatis利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 默认值为session,这种情况下会缓存一个会话中执行的所有语句。若设置为STATEMENT,本地会话仅用在语句执行上,对相同SqlSession的不同调用将不会共享数据。
- 一级缓存是 sqlsession 级别的缓存。一级缓存是一直开启的,sqlsession级别的一个 map,与数据库同一次会话期间查询到的数据会放在本地缓存中。 多个一级缓存中的数据不能共用。以后如果需要获取相同的数据,直接从缓存中获取,不必再去查询数据库。
- 一级缓存的工作机制,同一次会话期间只要查询过的数据都会保存在当前的 SqlSession 的一个Map中。
一级缓存失效的四种情况
一级缓存失效情况(没有使用到当前一级缓存的情况,效果就是,还需要再向数据发送SQL),如下:
- sqlSession 不同:使用不同的 sqlSession 数据库会话,不同的 SqlSession 对应不同的一级缓存;
- sqlSession 相同:但查询条件不同(当前一级缓存中还没有这个数据);
- 如果sqlSession相同:两次查询之间执行了增删改操作(这次增删改可能对当前数据有影响);
- 如果sqlSession相同,手动清除了一级缓存(把缓存内容清空)。
SqlSession级别的缓存就相当于一个Map。
一级缓存的生命周期
- MyBatis在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象。Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
- 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。
- 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。
- SqlSession中执行了任何一个update操作(
update()、delete()、insert()
) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用
一级缓存源码分析
MyBatis的一级缓存是SqlSession级别的缓存,SqlSession提供了面向用户的API,但是真正执行SQL操作的是Executor组件。Executor采用模板方法设计模式,BaseExecutor类用于处理一些通用的逻辑,其中一级缓存相关的逻辑就是在BaseExecutor类中完成的。
PerpetualCache
一级缓存使用PerpetualCache实例实现,在BaseExecutor类中维护了两个PerpetualCache属性。
public abstract class BaseExecutor implements Executor {
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
}
其中,localCache属性用于缓存MyBatis查询结果,localOutputParameterCache属性用于缓存存储过程调用结果。这两个属性在BaseExecutor构造方法中进行初始化
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
CacheKey
MyBatis通过CacheKey对象来描述缓存的Key值。在进行查询操作时,首先创建CacheKey对象(CacheKey对象决定了缓存的Key与哪些因素有关系)。如果两次查询操作CacheKey对象相同,就认为这两次查询执行的是相同的SQL语句。CacheKey对象通过BaseExecutor类的createCacheKey()方法创建
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
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.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
从上面的代码可以看出,缓存的Key与下面这些因素有关:
- Mapper的Id,即Mapper命名空间与
<select|update|insert|delete>
标签的Id组成的全局限定名。 - 查询结果的偏移量及查询的条数。
- 具体的SQL语句及SQL语句中需要传递的所有参数。
- MyBatis主配置文件中,通过
<environment>
标签配置的环境信息对应的Id属性值。
执行两次查询时,只有上面的信息完全相同时,才会认为两次查询执行的是相同的SQL语句,缓存才会生效。
BaseExecutor
接下来我们看一下BaseExecutor的query()方法相关的执行逻辑
@Override
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;
}
在BaseExecutor类的query()方法中,首先根据缓存Key从localCache属性中查找是否有缓存对象,如果查找不到,则调用queryFromDatabase()方法从数据库中获取数据,然后将数据写入localCache对象中。如果localCache中缓存了本次查询的结果,则直接从缓存中获取。
需要注意的是,如果localCacheScope属性设置为STATEMENT,则每次查询操作完成后,都会调用clearLocalCache()方法清空缓存。除此之外,MyBatis会在执行完任意更新语句后清空缓存,我们可以看一下BaseExecutor类的update()方法
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
可以看到,MyBatis在调用doUpdate()方法完成更新操作之前,首先会调用clearLocalCache()方法清空缓存。