二级缓存也称作应用缓存,与一级缓存不同的是它的作用范围是整个应该程序,而且是可以跨线程使用。所以二级缓存有更高的命中率,适合缓存一些修改较少的数据。
一般情况,二级缓存也是默认关闭的,可以直接在config.xml配置文件打开,也可以通过某个实体的mapping.xml打开,只要在里面加上
< cache></ cache>就可以了。
我们先来看访问二级缓存的执行流程。
简单来说,CachingExecutor里面的query方法做了大部分的工作,现在看看这个方法的源码
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
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) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
第一行代码获得一个cache对象,而获取的条件则是根据你的配置是否打开了二级缓存以及namespace等。flushCacheIfRequired(ms)方法则是清空二级缓存,判断条件是看你是否手动设置了需要清空缓存。
接下来便是是getObject方法,也是访问二级缓存的map,如果返回的list是空,则继续查询数据库,然后通过put放入二级缓存中。
其中在put的过程中还有一个缓存区的概念,并不是把数据库查出来的值直接放入二级缓存的,而是先放入缓存区,只有在session提交之后,才会把缓存区的数据库放入二级缓存。缓存区当然也是一个map,而它的key则是一个cache对象,也就是二级缓存。这个设计是为了防止update的同时query导致查询的数据不一致。因为在update还没提交session的情况下,二级缓存是没有被清空的,只是用了一个标记来说明已经清空了。
@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
/**这里就是标记是否已经进行了修改的操作,
如果是则返回null,表示二级缓存已被清空,但实际上二级缓存
仍然有值,只有session提交后才真的清空*/
if (clearOnCommit) {
return null;
} else {
return object;
}
}
我们可以继续看看update的代码,是不是真的没有清空操作
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
这里调用了flushCacheIfRequired(ms)方法,再来看这个方法干了什么。其实就调用了clear()方法
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
我们可以看到,他把clearOnCommit 设置为true,这就是上面用到的判断条件,同时第二行代码则是清空缓存区。
那么我们再来看看缓存的存取过程还做了什么。
在mybatis中有一个Cache接口类,也就是上面CachingExecutor获取到的对象,代码如图
很简单的方法,相信即便不看注释也能知道是用来干嘛的。其中接口的主要实现是PerpetualCache类,里面也是一个HashMap,也就是二级缓存的真正的实现。
在对map操作的时候,mybatis采用了一个叫做责任链的设计模式,其中的节点有
每个节点都做一些自己的事情,然后继续传入选一个节点,就形成了一条链,这里每个节点都是用了装饰器模式。我们可以来看看线程同步节点的源码,
@Override
public synchronized void putObject(Object key, Object object) {
delegate.putObject(key, object);
}
@Override
public synchronized Object getObject(Object key) {
return delegate.getObject(key);
}
两个主要的方法,其实里面只是通过synchronized加锁,然后就传入下一个节点了。我们再来看下一个节点,序列化的节点代码
@Override
public void putObject(Object key, Object object) {
if (object == null || object instanceof Serializable) {
delegate.putObject(key, serialize((Serializable) object));
} else {
throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);
}
}
@Override
public Object getObject(Object key) {
Object object = delegate.getObject(key);
return object == null ? null : deserialize((byte[]) object);
}
这两个方法则加上了序列化和反序列化的逻辑,然后继续交给下一个节点处理。这里就有一个问题,为什么一定要序列化呢,这是为了防止获取对象时,获取到同一个对象。
最后我们打个断点跑一下代码
可以看到依次访问了这么几个cache。对了还有二级缓存的命中条件