- 为什么要用一级缓存?
Mybatis在开始使用一次数据库的时候,会创建一个新的SqlSession,简称一次会话。
在对数据库的一次会话中,有时候会反复快速地执行完全相同的查询语句,如果没一级缓存的话,每一次查询都会查询一次数据库,那么它们的结果极有可能完全相同,由于查询一次数据库的代价很大,这有可能造成很大的资源浪费。如果使用一级缓存的话,会将每次查询结果缓存起来,当下次查询的时候,会判断是否存在完全一样的查询,存在的话直接从缓存中将结果取出,返回给用户,不存在的话再去数据库中查,接着将查询结果缓存起来。
2. Mybatis一级缓存怎么存储的?
其实每次Mybatis的查询与更新都是Executor这个接口来完成, Executor的实现类BaseExecutor中拥有一个Cache接口的实现类PerpetualCache,如下所示:
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++;
//每次查询都会调用PerpetualCache的getObject()方法来验证先前是否有相同的查询
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;
}
//如果先前有相同的查询,会调用这个方法
private void handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter, BoundSql boundSql) {
if (ms.getStatementType() == StatementType.CALLABLE) {
//调用PerpetualCache的getObject()的方法来获取相同查询的查询结果
final Object cachedParameter = localOutputParameterCache.getObject(key);
if (cachedParameter != null && parameter != null) {
final MetaObject metaCachedParameter = configuration.newMetaObject(cachedParameter);
final MetaObject metaParameter = configuration.newMetaObject(parameter);
for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
if (parameterMapping.getMode() != ParameterMode.IN) {
final String parameterName = parameterMapping.getProperty();
final Object cachedValue = metaCachedParameter.getValue(parameterName);
metaParameter.setValue(parameterName, cachedValue);
}
}
}
}
}
所以Mybatis的一级缓存实际上是 PerpetualCache来实现,PerpetualCache其实也是靠HashMap来存储一级缓存,代码如下:
public class PerpetualCache implements Cache {
private String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
public PerpetualCache(String id) {
this.id = id;
}
public String getId() {
return id;
}
public int getSize() {
return cache.size();
}
public void putObject(Object key, Object value) {
cache.put(key, value);
}
public Object getObject(Object key) {
return cache.get(key);
}
public Object removeObject(Object key) {
return cache.remove(key);
}
//Mybatis的任何一个update操作(update()、delete()、insert()) ,都会
//调用这个方法,清空PerpetualCache对象存储的一级缓存数据
public void clear() {
cache.clear();
}
public ReadWriteLock getReadWriteLock() {
return null;
}
public boolean equals(Object o) {
if (getId() == null) throw new CacheException("Cache instances require an ID.");
if (this == o) return true;
if (!(o instanceof Cache)) return false;
Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}
public int hashCode() {
if (getId() == null) throw new CacheException("Cache instances require an ID.");
return getId().hashCode();
}
}
3. 怎么判断查询是否相同?
PerpetualCache将每次查询的特征作为key,查询的结果作为value存储到Map中。所以看下key是怎么生成的,就知道怎么判断了。通过源码,发现key是通过BaseExecutor的createCacheKey的方法生成,代码如下:
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
//statementId
cacheKey.update(ms.getId());
//rowBounds的Offset()
cacheKey.update(rowBounds.getOffset());
//rowBounds的Limit()
cacheKey.update(rowBounds.getLimit());
//SQL语句
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;
}
从上面的代码中可以看出,如果下面条件一样,就可以判断为两个相同的查询,条件如下:
1、statementId ;2、RowBounds的offset、limit的结果集分页属性;3、SQL语句;4、传给JDBC的参数值
对了还有一点,添加一些无关的参数值(比如添加Student的age的参数,但查询条件没有age这个参数),还是相同的两个查询;只要statementId,rowBounds,最后生成的SQL语句,以及这个SQL语句所需要的参数完全一致就是两个相同的查询。