mybatis的缓存主要是分为两种:一级缓存和二级缓存:
一级缓存:一直开启,不能关闭,默认是session级别的,但是可以设置成statement级别。
session级别:表现为---MyBatis执行SQL语句之后,这条语句就是被缓存,以后再执行这条语句的时候,会直接从缓存中拿结果,而不是再次执行SQL。但是在两个相同的查询之间,如果当前数据被修改,会进行两次查询。作用域是SqlSession级别。
1.在同一个SqlSession中相同的查询,只会走一次数据库查询,第二次走缓存
2.在同一个SqlSession中不同的查询,走两次数据库查询
3.在不同的SqlSession相同的查询,走两次数据库查询
4.在同一个SqlSession中相同的查询,如果在两次查询之前,修改当前数据,一级缓存会被清空,走两次数据库查询
我们看一下这个一级缓存在代码中是如何起作用的:
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);
}
在Executor(BaseExecutor)的查询方法中,我们可以看到当resultHandler 为空时,我们直接从localCache中获取数据
我们看一下这个localCache的数据结构是个PerpetualCache对象,实现了Cache接口,其中有一个字段cache
private Map<Object, Object> cache = new HashMap<Object, Object>();
这里可以看到是Map类型的。
然后我们可以看到在查询方法里面,会把查询结果放到缓存里面
我们看到当设置的是stament级别时,每次操作完成都会清空一级缓存
那么为什么在同一session下,相同操作中间有修改动作的时候,缓存就无效呢?我们可以看一下修改方法里面做了什么
这里我们看到在执行修改操作之前,清空了当前session的一级缓存。
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
二级缓存:可以关闭,默认是开启的,作用域是全局的,但是二级缓存在SqlSession关闭或提交之后才会生效,也就是作用在两个session之间的。
1.在setting中配置cacheEnabled参数,默认为true,可关闭(false)
2.在mapper.xml的select节点上,设置useCache参数,默认是true开启,可关闭(false)
3.在mapper.xml中,添加<cache /> 标签 可指定缓存类型等
我们依然来看一下二级缓存在源码中的使用:
1.解析:是在我们解析每一个mapper.xml文件的时候,解析cache标签
详细看一下cache标签的解析
private void cacheElement(XNode context) throws Exception {
if (context != null) {
//这里获取制定的缓存类型 如果没有就设定默认的
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
//创建缓存
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
//创建缓存
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
//添加到configuration中
configuration.addCache(cache);
currentCache = cache;
return cache;
}
然后在解析mapper的SQL语句时:这里解析当前方法是否开启useCache,默认查询是开启
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
然后把cache设置到mapperdStatement
当我们设置了cache的时候,我们可以看到生成的Executor会被包一层CachingExecutor
那我们看一下CachingExecutor的query方法:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//这里获取到对应的缓存cache
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
//如果当前查询开启了缓存 并且resultHandler 为空 走二级缓存
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//从缓存中获取数据
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//如果没有数据走数据库查询
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//把数据存到缓存中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
我们看一下获取缓存数据的方法:
//TransactionalCacheManager
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
//TransactionalCache
public Object getObject(Object key) {
//这里通过内部的cache实例 获取数据
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}
而把数据放到缓存里面的操作是:这里只是添加到了一个map中,并没有放到内部cache实例中
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
这里其实是通过sqlsession的commit方法进行提交到缓存的。我们看下代码
//DefaultSqlSession
public void commit(boolean force) {
try {
//调用executor的commit
executor.commit(isCommitOrRollbackRequired(force));
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
//调用executor的commit
//这里有缓存,所以是CachingExecutor
public void commit(boolean required) throws SQLException {
delegate.commit(required);
//缓存就是在这里提交的
tcm.commit();
}
//TransactionalCacheManager
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
//TransactionalCache
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
//刷新缓存
flushPendingEntries();
reset();
}
//刷新缓存
private void flushPendingEntries() {
//循环所有的entriesToAddOnCommit map数据,添加到缓存cache中
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
总结:从上面的解析可以看到,如果我们开启了二级缓存,会先从二级缓存中获取数据,如果二级缓存里面没有,这从一级缓存中获取,一级缓存如果没有数据,查询数据库;拿到结果后,把数据库存储到一级缓存中,当sqsession执行commit的时候把数据放到二级缓存中。