一级缓存
每个SqlSession中持有了Executor,每个Executor中持有一个PerpetualCache对象。PerpetualCache中维护了一个Map集合实例cache,当发起查询时先创建CacheKey,若命中缓存则直接返回结果,如果没有命中缓存则查询数据库,结果写入PerpetualCache,最后返回结果。具体实现代码如下。
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
// 创建缓存key,CacheKey包含MappedStatement的Id、SQL的offset、SQL的limit、SQL本身以及SQL中的参数
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
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 {
// 查询数据库,并将结果缓存
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
...
// query方法执行的最后,会判断一级缓存级别是否是STATEMENT级别,如果是的话则清空缓存,这就是STATEMENT级别的一级缓存无法共享localCache的原因
if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
this.clearLocalCache();
}
...
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 将查询出来的结果存入一级缓存
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
// 如果是存储过程把参数存入localOutputParameterCache
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
一级缓存源码分析的最后,我们确认一下缓存就会刷新的时机,SqlSession的insert、delete、update方法都会统一走update,每次执行update前都会执行clearLocalCache清除缓存。
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
this.clearLocalCache();
return this.doUpdate(ms, parameter);
}
}一级缓存总结
1、MyBatis一级缓存的生命周期和SqlSession一致。
2、MyBatis一级缓存内部设计是Executor中持有一个没有容量限定的HashMap。
3、MyBatis一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。
二级缓存
如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询。同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。当开启缓存后,数据的查询执行的流程就是:二级缓存 -> 一级缓存 -> 数据库。
CachingExecutor的query方法中首先会从MappedStatement中获得在配置初始化时赋予的Cache。看到第二个if判断时我们可以总结出开启二级缓存的条件:
1、全局配置变量参数cacheEnabled=true(在创建Executor的时候会进行该条件判断,成立则用CachingExecutor包装。)
2、该select语句所在的Mapper,配置了<cache> 或<cached-ref>节点,并且有效。
3、该select语句设置了useCache=true。
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) {
// 根据xml配置的属性flushCache来判断是否需要刷新缓存暂存区,默认的设置中SELECT语句不会刷新缓存,insert/update/delte会刷新缓存。<select id="findUserById" useCache="true" flushCache="true">
this.flushCacheIfRequired(ms);
// 根据xml配置的属性useCache来判断是否使用缓存,<select id="findUserById" useCache="true">
if (ms.isUseCache() && resultHandler == null) {
// 确保方法没有Out类型的参数,mybatis不支持存储过程的缓存,所以如果是存储过程,这里就会报错
this.ensureNoOutParams(ms, boundSql);
// 根据key从TransactionalCacheManager取出缓存,如果没有缓存则执行查询,并且将查询结果放到缓存中并返回取出结果
List<E> list = (List)this.tcm.getObject(cache, key);
// 如果二级缓存中没有这个数据时便会利用被包装的执行器查询
if (list == null) {
// 这里会先查一级缓存,没有的话便会到数据库中查找,详见上面一级缓存查询逻辑
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// TransactionalCacheManager对象会创建一个TransactionalCache对象,它对传入的Cache进行再一次的包装,它持有的entriesToAddOnCommit对象用来保存临时的缓存结果
// 当SqlSession执行commit或者close时都会执行transactionCache的flushPendingEntries方法将entriesToAddOnCommit中的数据提交到mapper的cache当中
this.tcm.putObject(cache, key, list);
}
return list;
}
}
// 委托模式,交给SimpleExecutor等实现类去实现方法
return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
// 同样在执行insert/update/delete也会执行该方法进行缓存的清除
private void flushCacheIfRequired(MappedStatement ms) {
// 先从MapperStatement取出缓存,只有通过<cache/>、<cache-ref/>或@CacheNamespace、@CacheNamespaceRef标记使用缓存的Mapper.xml或Mapper接口(同一个namespace,不能同时使用)才会有二级缓存。
// 如果缓存不为空,说明是存在缓存。如果cache存在,那么会根据sql配置insert/select/update/delete的flushCache属性来确定是否清空缓存。
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
// tcm是TransactionalCacheManager的实例,持有Cache和用TransactionalCache包装后的Cache的映射关系Map集合
//这里的clear并不会去去清空二级缓存区域,而是设置了一个提交标识和清空entriesToAddOnCommit,如果事务回滚或者不提交事务,则不对缓存产生影响
tcm.clear(cache);
}
}同样在执行insert/update/delete时,CachingExecutor也会执行flushCacheIfRequired方法进行是否清除缓存暂存区的判断,即清除entriesToAddOnCommit中的值。
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
this.flushCacheIfRequired(ms);
return this.delegate.update(ms, parameterObject);
}
// 提交和回滚都是直接调用TransactionalCacheManager的commit和rollback方法
// TransactionalCacheManager会将对应TransactionalCache中的数据提交到二级缓存或清除
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
public void rollback(boolean required) throws SQLException {
try {
delegate.rollback(required);
} finally {
if (required) {
tcm.rollback();
}
}
}
二级缓存总结
1、MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。但正是由于所有SqlSession实例共享,基于namespace隔离的特点,故如果不同namespace定义了同时操作一个表的SQL语句,则会造成不同namespace之间的缓存不一致的问题。
2、MyBatis在多表查询时,极大可能会出现脏数据。采用cache-ref来进行命名空间的依赖能够避免二级缓存,但是总不能每次写一个XML配置都会采用这种方式吧,最有效的方式还是避免多表操作使用二级缓存。
3、在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。