本篇源码解析基于mybatis 3.5.8版本。
MyBatis 中的缓存指的是 MyBatis 在执行一次SQL查询时,在满足一定的条件下,会把这个sql和对应的查询结果缓存起来。当再次执行相同SQL语句的时候,就会直接从缓存中进行提取,而不是请求到数据库。当然如果中间有更新操作,缓存会失效。
MyBatis中的缓存分为一级缓存和二级缓存,一级缓存又被称为 SqlSession 级别的缓存,二级缓存又被称为表级缓存。通俗的说,一级缓存是本次会话有效,二级缓存可以跨越多个会话共享缓存。
当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
本篇我们关注二级缓存,一级缓存请查看文章:
mybatis的缓存机制源码分析之一级缓存解析
开启二级缓存的配置是:
二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。
二级缓存也可以认为是表级别的缓存,因为通常我们在代码里定义mapper文件类似下面这种:
...
一个mapper文件里面有一个namespace,对应一个表的所有增删改查操作。
二级缓存使用的缓存管理类是CachingExecutor
,它的初始化在org.apache.ibatis.session.Configuration#newExecutor
方法中:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
...
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);
}
//如果二级缓存开关开启的话,是使用CahingExecutor装饰BaseExecutor的子类
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
...
可以看到CachingExecutor持有一个Executor
的委托,这就是装饰器模式。CachingExecutor在委托给代理类执行之前,先用自己的逻辑进行装饰,这也是二级缓存实现的关键。
我们通过query方法看看具体的逻辑:
@Override
public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
//判断是否需要刷新缓存,默认情况下select不需要刷新,update/delete/insert需要
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//从缓存获取数据
List list = (List) 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);
}
首先从MappedStatement获取cache的实例,如果获取不到,直接通过代理进入一级缓存的查询逻辑了。
flushCacheIfRequired
判断是否需要刷新缓存。
这里的重点是tcm
,可以看到缓存的数据是从这里获取的,tcm是TransactionalCacheManager
的实例,用来实现缓存的事务管理。来看看:
public class TransactionalCacheManager {
private final Map transactionalCaches = new HashMap<>();
...
TransactionalCacheManager
持有一个map,key是Cache本身,value是TransactionalCache,是对Cache做了一层包装,这层包装是实现事务的关键。我们来看下TransactionalCacheManager的put方法:
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
entriesToAddOnCommit
是个map,
private final Map
然后看下get方法,
@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;
}
}
是不是觉得比较奇怪,为啥put方法放到一个本地的map,但是get方法居然是从delegate拿数据,其实这就是事务缓存的关键。当我们使用put方法提交数据时只是把缓存临时放到了内存里,并没有使缓存真正生效。那么什么时候生效呢?
我们来看下TransactionalCache
的commit方法,
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
然后看看flushPendingEntries方法,
private void flushPendingEntries() {
for (Map.Entry
看到了没,只有调用了commit方法才会触发缓存提交。那么commit方法什么时候调用呢?如果你顺着链路往上找,会发现最终调用者是:org.apache.ibatis.session.defaults.DefaultSqlSession#commit(boolean)
。原来最终是被sql session的commit方法调用的。
最后,如果获取的list为空,通过代理类进入了一级缓存的流程,这部分前面已经有文章解析了。
参考