概述
当用户频繁查询某些固定的数据时,第一次将这些数据从数据库中查询出来,保存在缓存(内存,高速磁盘)中。
当下次用户再次查询这些数据时,不用再通过数据库查询,而是去缓存里面查询。
这么做的目的,一是提升查询速度,二是降低数据库的并发请求压力。
在Mybatis中,缓存分为两种 : 一级缓存和二级缓存。
一级缓存是SqlSession级别的,二级缓存是Mapper级别的。
一级缓存
如果你读过我之前写的Mybatis相关文章,那一定知道最后我们的请求都是由SqlSession来执行的,
确切的说,应该是由DefaultSqlSession来执行的,
更确切的说,其实是由DefaultSqlSession的Executor执行器来执行的。
Query
我们随便找一个DefaultSqlSession的查询方法,看看它的源代码
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
}
}
方法首先根据statement获取MappedStatement,MappedStatement其实就是sql操作的具体参数。
然后使用执行器executor执行查询方法。
看一下executor的query()方法,这里默认使用的是BaseExecutor。
@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);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
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--;
}
return list;
}
首先看这一句
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
执行器首先创建了CacheKey,由字面意思不难推断,这是一个用于追踪缓存的唯一ID,也就是说这个CacheKey可以唯一确定一次请求。然后执行重载的query()方法。
重点看一下几行代码。
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);
}
代码的大概意思是说,首先通过cachekey去localCache里面去寻找我们之前的缓存结果,若结果为空。则去数据库里查询。
显然localCache就是我们所说的一级缓存,打开源代码看一下。
public class PerpetualCache implements Cache {
private Map<Object, Object> cache = new HashMap<Object, Object>();
...
}
原来是一个PerpetualCache类,实现了Cache接口,为什么要实现Cache接口呢,因为缓存有多种,PerpetualCache只是其中一种。往下看,发现其实真正的缓存就是用HashMap来存储的。
我们知道HashMap不是一个线程安全的数据结构,那为什么这里还要用HashMap呢,博主也百思不得其解。
Update
我们再看一下executor的update方法
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
我们发现,当Mybatis执行update方法时,首先要做的就是清空缓存,其实update、delete、add等方法都是清空缓存,这在一定程度上保证了缓存数据的有效性。
CacheKey
我们再来看一下CacheKey是如何生成的,因为CacheKey是获取缓存结果的唯一ID,所以它的值的生成过程比较重要。看一下代码。
@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(Integer.valueOf(rowBounds.getOffset()));
cacheKey.update(Integer.valueOf(rowBounds.getLimit()));
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
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.update(value);
}
}
if (configuration.getEnvironment() != null) {
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
CacheKey的update方法
public void update(Object object) {
if (object != null && object.getClass().isArray()) {
//如果是数组,则循环调用doUpdate
int length = Array.getLength(object);
for (int i = 0; i < length; i++) {
Object element = Array.get(object, i);
doUpdate(element);
}
} else {
//否则,doUpdate
doUpdate(object);
}
}
CacheKey的doUpdate方法
private void doUpdate(Object object) {
int baseHashCode = object == null ? 1 : object.hashCode();
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
再看一下CacheKey的toString()方法
@Override
public String toString() {
StringBuilder returnValue = new StringBuilder().append(hashcode).append(':').append(checksum);
for (int i = 0; i < updateList.size(); i++) {
returnValue.append(':').append(updateList.get(i));
}
return returnValue.toString();
}
根据以上代码,我们可以得出结论,CacheKey主要有一下几部分组合,并生成了一个hash码:
- statementId
- rowBounds.offset和rowBounds.limit 查询时要求的结果集中的结果范围
- Sql语句字符串 : boundSql.getSql()
- 传递给JDBC的参数 : parameterMappings
注意:doUpdate方法的最后一行updateList.add(object);
以及toString方法的returnValue.append(’:’).append(updateList.get(i));
含义为:不同的请求有极小的概率会生成相同的hash码,在toString方法中生成hash的过程中加入每个对象的实际内容,作为区分。
生命周期
MyBatis在开启一个数据库会话时,会创建一个新的SqlSession对象,SqlSession对象持有一个Executor对象,
Executor对象持有一个PerpetualCache对象;
-
当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
-
如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。
-
如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。
-
SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用。
存在问题
- 一级缓存并没有做粒度控制,而是在一次会话中对数据库中所有表的sql操作均做了缓存,这显然是不合理的。
- 任何更新(增、删、改)操作都会清除一级缓存,其实在有些情况下,这完全没有必要,比如两个完全不相关的表1和表2,如果你更新了表1,那为什么也要把表2的缓存也清除呢?
- 即便更新操作置空了当前SqlSession的一级缓存,也只是能够保证当前SqlSession的缓存失效,保证数据一致性,但是也不能保证其他SqlSession的缓存数据失效,这会导致数据不一致问题。
- 一级缓存只适用于单点,而不支持分布式部署。
综合以上一级缓存存在的问题,Mybatis设计开发了二级缓存,我们将在下一篇文章中介绍。
温馨提示
- 如果您对本文有疑问,请在评论部分留言,我会在最短时间回复。
- 如果本文帮助了您,也请评论,作为对我的一份鼓励。
- 如果您感觉我写的有问题,也请批评指正,我会尽量修改。