阅读须知
- Mybatis源码版本:3.4.4
- 文章中使用/* */注释的方法会做深入分析
前言
本文从源码分析Mybatis一级和二级缓存的应用,进而阐述使用Mybatis缓存时的一些注意事项,建议读者首先去了解一下Mybatis的读写流程,可以看一下笔者对相关流程源码分析的文章。
在介绍Mybatis一级缓存和二级缓存之前,需要首先理解两个概念:
SqlSession: 引用官方文档中对这个接口作用的说明—SqlSession完全包含了面向数据库执行SQL命令所需的所有方法。你可以通过SqlSession实例来直接执行已映射的SQL语句,也可以通过SqlSession得到映射和管理事务。
namespace: 这里提到的namespace指代的是在应用中配置的mapper配置文件中的namespace。eg:<mapper namespace="xxx"></mapper>
。
一级缓存
SqlSession维度的缓存,也就是每个SqlSession独享的缓存,我们在使用Mybatis的时候,通常会使用SqlSession的getMapper方法获取到映射,eg:UserDao userDao = sqlSession.getMapper(UserDao.class);
获取到映射之后执行相关方法进行数据库操作,获取到的映射对象是通过动态代理生成的代理类MapperProxy对象,这部分内容我们在之前的文章中分析过。Mybatis的数据库操作是通过Executor来执行,一级缓存也是通过Executor来维护,每个SqlSession都会持有一个Executor:
图中LocalCache就是Mybatis的一级缓存,LocalCache的查询和写入是在Executor内部完成的,BaseExecutor是一个实现了Executor接口的抽象类,通过阅读源码发现,一级缓存LocalCache就是BaseExecutor内部的一个成员变量。
public abstract class BaseExecutor implements Executor {
protected PerpetualCache localCache;
public class PerpetualCache implements Cache {
private Map<Object, Object> cache = HashMap<Object, Object>();
一级缓存保存在PerpetualCache维护的一个Map中,下面为一级缓存执行的流程图:
从流程图中看到,在执行Executor query动作时会尝试从一级缓存中获取缓存数据,下面为相关源码:
BaseExecutor:
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
/* 创建缓存key */
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 执行查询
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
BaseExecutor:
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId()); // MappedStatement id
cacheKey.update(rowBounds.getOffset()); // offset
cacheKey.update(rowBounds.getLimit()); // limit
cacheKey.update(boundSql.getSql()); // sql
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value); // param
}
}
if (configuration.getEnvironment() != null) {
// Environment id
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
在BaseExecutor的query方法中发现了缓存key的构建过程,Mybatis用CacheKey这个对象作为缓存的key,上文中说到一级缓存最终会保存在PerpetualCache维护的一个Map中,我们知道,Map用hashcode和equals来确定对象的唯一性,在CacheKey的update方法中,我们发现了hashcode的运算:
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
update方法会计算hashcode和checksum并将参数对象保存到了updateList中,而在CacheKey的equals方法中,除去hashcode、checksum和count的比较外,只要updatelist中的元素一一对应相等,那么就可以认为是相等:
CacheKey:
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof CacheKey)) {
return false;
}
final CacheKey cacheKey = (CacheKey) object;
if (hashcode != cacheKey.hashcode) {
return false;
}
if (checksum != cacheKey.checksum) {
return false;
}
if (count != cacheKey.count) {
return false;
}
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (!ArrayUtil.equals(thisObject, thatObject)) {
return false;
}
}
return true;
}
所以,上面createCacheKey方法中调用CacheKey的update方法时传入的元素,就是Mybatis用来确定缓存唯一性的元素。继续分析上文中提到的query方法中调用的重载query方法:
BaseExecutor:
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// flushCache可以在mapper配置文件中配置
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache(); // 清空一级缓存
}
List<E> list;
try {
queryStack++;
// 尝试从缓存中获取
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 缓存为null则查询数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
deferredLoads.clear();
// localCacheScope可以配置
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache(); // 清空一级缓存
}
}
return list;
}
在query方法中发现了清空一级缓存的两处代码,这两处的判断在Mybatis中都是可以配置的。可以在mapper配置文件中配置flushCache来控制数据库操作是否要清空缓存,select默认为false,insert、update、delete默认为true,注意,这个配置一级缓存和二级缓存都会生效,下文会介绍二级缓存。可以使用localCacheScope选项来控制一级缓存的范围,默认为SESSION,会缓存一个SqlSession中的所有查询,如果设置为STATEMENT,则查询会清除缓存。在查询数据库queryFromDatabase方法中,Mybatis做数据库查询操作,并将返回的结果存入一级缓存:
BaseExecutor:
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.putObject(key, parameter);
}
return list;
}
数据库的写操作会清除缓存, 我们在分析Mybatis写操作流程的源码时看到insert、update、delete都会统一走update方法:
BaseExecutor:
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache(); // 清空一级缓存
return doUpdate(ms, parameter);
}
二级缓存
二级缓存是namespace维度的缓存。上文提到SqlSession中会持有一个Executor,在构建SqlSession的时候,Mybatis会根据cacheEnable选项来确定是否使用一个缓存的Executor,也就是CachingExecutor。cacheEnable可以配置,默认为true:
Configuration:
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) {
// 如果cacheEnabled配置为true,则用CachingExecutor封装executor
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
cacheEnable开启的状态下在mapper配置文件中配置<cache/>
和<cache-ref/>
可以控制缓存策略,介绍一级缓存的时候我们提到了可以使用flushCache选项来控制是否清空缓存,这个配置同样会作用于二级缓存。mybatis在解析配置文件的时候,会将<cache/>
配置的参数解析成Cache对象保存到MappedStatement(可以理解为mapper配置文件的java代码实现,对象中保存了在mapper配置文件中配置的选项)中,解析配置文件的过程我们已经分析过。构建好SqlSession后,数据库操作就会委托给CachingExcutor进行执行:
CachingExecutor:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache(); // 获取缓存对象,也就是上文提到的mapper配置文件中的<cache/>
if (cache != null) {
flushCacheIfRequired(ms); /* 如果配置了flushCache=true,则刷新缓存 */
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
// 尝试从二级缓存中获取
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 如果缓存为null则走上面一级缓存的流程
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // 记录本次查询将要新增的缓存
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
这是一个典型的按需加载缓存的方式,注意,tcm.putObject方法执行完之后缓存并没有真正的生效,这里只是记录了这次查询将要产生缓存变更,这时候相同的sql查询缓存是不会生效的。同样的,写操作也不是马上会清除缓存:
CachingExecutor:
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms); /* 在默认配置下,写操作会刷新缓存 */
return delegate.update(ms, parameterObject);
}
CachingExecutor:
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
/* 缓存管理器清空缓存 */
tcm.clear(cache);
}
}
TransactionalCacheManager:
public void clear(Cache cache) {
/* 获取TransactionalCache,清空缓存 */
getTransactionalCache(cache).clear();
}
TransactionalCacheManager:
private TransactionalCache getTransactionalCache(Cache cache) {
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
// 这里构造TransactionalCache对象时传入的Cache对象在做commit操作时会操作这个Cache独享
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
TransactionalCache:
public void clear() {
clearOnCommit = true; // 提交时清理缓存的标记
entriesToAddOnCommit.clear(); // 清空将要加入到缓存中的对象
}
在执行SqlSession的commit方法之后,缓存的变更会真正的被刷新到缓存中去,开始真正的发挥作用:
DefaultSqlSession:
public void commit(boolean force) {
try {
/* 执行commit */
executor.commit(isCommitOrRollbackRequired(force));
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
CachingExecutor:
public void commit(boolean required) throws SQLException {
delegate.commit(required); // 执行BaseExecutor的commit方法,用于提交事务、清理一级缓存、刷新Statement
tcm.commit(); /* 缓存管理器提交 */
}
TransactionalCacheManager:
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
/* 遍历管理的所有TransactionalCache,执行commit操作 */
txCache.commit();
}
}
TransactionalCache:
public void commit() {
if (clearOnCommit) {
// 如果设置了提交时清空缓存则进行清空操作
delegate.clear();
}
/* 将暂时保存的缓存真正刷新到缓存中去 */
flushPendingEntries();
reset();
}
TransactionalCache:
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);
}
}
}
我们看到在保存缓存和刷新缓存时用到的delegate对象就是上文中提到的构造TransactionalCache对象时传入的Cache对象了,在构建Cache对象时,Mybatis采用装饰者模式为Cache装饰了不同的功能,我们在之前的文章中已经分析过相关内容。缓存保存在PerpetualCache中:
public void putObject(Object key, Object value) {
cache.put(key, value);
}
PerpetualCache:
public void clear() {
cache.clear();
}
cache就是一个HashMap对象:
PerpetualCache:
private Map<Object, Object> cache = new HashMap<Object, Object>();
到这里,整个Mybatis缓存的源码分析就完成了。
缓存使用注意事项
在介绍一级缓存时我们提到Mybatis的一级缓存是SqlSession级别的缓存,不同的SqlSession之间缓存是不共享的,如果两个SqlSession操作同一张表,这时候就可能出现其中一个SqlSession获取到的数据是过期的,我们在使用这个SqlSession查询就有可能读取过期的脏数据。
在介绍二级缓存时我们提到二级缓存是namespace维度的缓存,全局共享整个namespace的缓存,当我们把针对同一张表的sql操作写到两个不同的mapper文件中或者使用表关联查询时,就很容易出现两个mapper中查询出来的同一条数据不一致的情况。所以在实际应用中,我们建议将cacheEnable设置为false、localCacheScope设置为STATEMENT,不使用mybatis的缓存,需要缓存的时候在应用程序中进行缓存的控制。
关于MyBatis缓存不一致的测试,详见:https://blog.csdn.net/heroqiang/article/details/85339834。