目录
1. 什么是缓存
缓存,即存储在内存中的临时数据。对于一些数据,如果它们经常被访问到并且在一定时间内不会被改变,那么可以考虑将其缓存下来以提高查询的效率。对于有缓存的数据的完整访问过程如下:
- 先查看缓存中是否已经有数据,如果有直接取出并返回;
- 如果缓存中没有数据,则查询数据库,并将查询到的数据写入到缓存以备下次使用;
- 下次再来访问同样数据的时候,由于缓存中已经有数据,无需再次访问数据库,达到提高效率的目的。
之所以可以使用这种思路是因为内存的访问速度是很快的,远远快过访问网络或者磁盘的速度。
2. MyBatis一级缓存
MyBatis的一级缓存默认开启,是属于每一个sqlsession的。当满足以下条件的时候,才可以命中缓存:
- 相同的 sql 和 参数
- 必须是在一个会话 Session当中
- 必须是执行 相同的方法
- 必须是相同的 namespace (同一个命名空间 -> 同一个mapper文件)
- 不能够在查询之前执行 clearCache
- 中间不能执行 任何 update ,delete ,insert (会将SqlSession中的数据全部清空)
3. 源码分析
3.1 一级缓存的保存与使用
首先,mybatis最终通过BaseExecutor.query()方法操作数据库(追踪源码即可以发现)。源码如下:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
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 {
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;
}
在上面的程序中,直接可见的流程如下:
- 生成key,由于缓存的底层数据结果是map,以k-v结构存储。
- 带着这个key,list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;去判断缓存中是否存在,
- 如果存在,返回数据。
- 如果不存在,调用queryFromDatabase查询数据库,将结果赋值给list返回。
重点来看一级缓存空间是谁,以及如何存储,即queryFromDatabase方法的源码:
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);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
其中使用doQuery方法查询数据库,得到结果,在返回结果之前,先通过localCache.putObject(key, list);方法将结果保存,显然,这个属性就是我们的一级缓存,我们可以点进其类型定义中就会发现,其本质就是一个hash: Mapprivate Map<Object, Object> cache = new HashMap<Object, Object>();
至此,一级缓存的查询与插入的时机已经清楚,接下来就看一级缓存什么时候清除。
3.2 一级缓存的清空
在上面的源码中可以发现有这样的一个方法:clearLocalCache();,根据命名就可以很容易地确定,这就是清空缓存的方法,所以只需要找到何时调用了这个方法,就可以知道一级缓存何时清空。总结如下:
- update时,一级缓存会被清空。delete和insert都是调用这个update。可以从SqlSession的insert、update、delete方法跟踪。
- LocalCacheScope.STATEMENT时,一级缓存会被清空。在BaseExecutor里的query方法中:
- 事务提交回滚时,一级缓存会被清空。
- flushCache="true"时,一级缓存会被清空。
3.3 一级缓存的key
我们现在来追踪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();
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的生成逻辑,我们并不关心,我们只关心key的结构,很显然,它被定义在CacheKey类中,所以只要知道了这个类的属性,就可以知道key如何构成,其所以属性和toString方法如下:
private static final long serialVersionUID = 1146682552656046210L;
public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
private final int multiplier;
private int hashcode;
private long checksum;
private int count;
private transient List<Object> updateList;
@Override
public String toString() {
StringBuilder returnValue = new StringBuilder().append(hashcode).append(':').append(checksum);
for (Object object : updateList) {
returnValue.append(':').append(ArrayUtil.toString(object));
}
return returnValue.toString();
}
于是,我们得到了key的结构:id + offset + limit + sql + param value + environment id。