Mybatis版本 3.4.6
配置
config配置
<configuration>
<properties resource="db.properties"></properties>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
<!--一级缓存默认打开,下面这一行关闭了一级缓存-->
<!--<setting name="localCacheScope" value="STATEMENT"/>-->
<!--二级缓存默认打开,除非配置了下面的setting-->
<setting name="cacheEnabled" value="false"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="personMapper.xml"/>
</mappers>
</configuration>
一级二级缓存都是默认有效的,除非使用上述配置显式关闭。若想使用二级缓存,还必须在mapper.xml中配置<cache />,否则也无法使用二级缓存。
一级缓存
我们知道,Mybatis真正运行SQL的操作都委托给了BaseExecutor.java类,每次新open一个SqlSession,都会新建一个BaseExecutor,而一级缓存实际是BaseExecutor的一个实例变量,所以一级缓存是SqlSession独有的,不同的SqlSession不能共享;
请看下面查询操作:
//代码-1 BaseExecutor的query方法
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
......
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//若localCache中没有,则从数据库中查询并放入localCache中
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
if (queryStack == 0) {
......
//若配置中配了<setting name="localCacheScope" value="STATEMENT"/>,则这句话就会生效,那么localCache就会被清空。
//效果就是关闭了一级缓存,每次查询都会查到数据库中。
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
}
return list;
}
笔者觉得上述关闭一级缓存的方式并不优雅,因为总是要先把结果放入localCache中,如要关闭一级缓存,再清空。
一级缓存分成Session 和 Statement两种,Statement实际上就是没有一级缓存。
由于一级缓存是SqlSession级别的,这样就可能引发一个问题,SqlSession1修改了数据库,SqlSession2却感知不到这一变化,读到一级缓存中的脏数据。
二级缓存
二级缓存相关的类是CacheingExecutor.java,它的实例变量private final TransactionalCacheManager tcm = new TransactionalCacheManager()实际上就管理着二级缓存,其查询方法如下:
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);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//先查二级缓存
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//二级缓存中没有,就通过BaseExecutor查询;BaseExecutor中会查询一级缓存
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // A
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
Mybatis中的二级缓存是通过TransactionalCacheManager(tcm)维护的, 真正的二级缓存是TransactionalCache。
public class TransactionalCache implements Cache {
//存储我们查询到的数据的地方
private final Cache delegate;
private boolean clearOnCommit;
//查询的数据会先暂时放在这里,等commit()后,才能移到上面的delegate里
private final Map<Object, Object> entriesToAddOnCommit;
private final Set<Object> entriesMissedInCache;
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<Object, Object>();
this.entriesMissedInCache = new HashSet<Object>();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
@Override
public void putObject(Object key, Object object) {
//这里可以发现,代码A并没有把查询结果真正放入到缓存里,只是放在了entriesToAddOnCommit这个临时的地方
entriesToAddOnCommit.put(key, object);
}
@Override
public Object removeObject(Object key) {
return null;
}
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
//调用该方法,查询结果才会放入二级缓存
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
//把临时保存的数据保存到缓存中
flushPendingEntries();
reset();
}
public void rollback() {
unlockMissedEntries();
reset();
}
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
//一次commit,可以将一个事务中多次查询的结果一次性放入缓存
private void flushPendingEntries() {
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);
}
}
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
try {
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
+ "Consider upgrading your cache adapter to the latest version. Cause: " + e);
}
}
}
}
缓存的初始化
从上文可知,缓存是与Executor关联在一起,使用不同的Executor执行sql,就会使用不同的缓存。下面看一下Executor的初始化:
Configuration.java
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) { //可以通过setting配置为false;默认为true;
executor = new CachingExecutor(executor); //二级缓存
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
二级缓存的初始化
二级缓存是通过解析mapper.xml中的<cache />或<cache-ref />进行初始化的,若没有这两个中的任意一个配置,是无法使用二级缓存的。
XMLMapperBuilder.java
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);
}
}
上面的attribute实际上就是cache标签的属性,这里实例化了各种Cache,并进行了装饰,装饰后,默认情况下如下图所示;然后将该cache的引用赋值给MappedStatement实例,MappedStatement实例代表了一个sql,这样就把mapper.xml的sql语句与cache联系到了一起。当该sql查询出结果后,放入该cache中并交给TransactionalCacheManager管理。
Cache.png
本文参考了这里
网友评论