本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。
MyBatis 缓存
MyBatis 内置了两层缓存,SqlSession 级的一级缓存,和 Mapper 级的二级缓存,结构都为 HashMap
1) 一级缓存(LocalCache)
一级缓存存储位置 sqlSession -> executor(delegate) -> localCache
1.1) 一级缓存原理
当发起一次查询时,在 Executor 中会调用基础的 query 方法:
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter); // 获取实际执行的 SQL
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); // 1.1.1) 创建 CacheKey
return query(ms, parameter, rowBounds, resultHandler, key, boundSql); // 1.1.2) 将 CacheKey 放入扩展的 query 方法查询
}
1.1.1) Create
CacheKey 由以下构成:
- Mapper namespace
- Statement id
- RowBounds 分页参数
- BoundSql 中的 SQL 执行参数列表
- Environment id
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());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
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);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
1.1.2) Use
生成 CacheKey 后就轮到真正的查询了
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.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; // 如 CacheKey 已被缓存,直接取结果
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); // 无缓存,走 DB 查询,并将结果塞入 Cache
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
1.1.3) Clear
Executor 中还有个 clearLocalCache 方法用于清除一级缓存,那么何时会被清理呢:
- update(insert,update,delete) 必然
- query 配置了禁用缓存时
- commit 必然
- rollback 必然
- close 必然
2) 二级缓存(MethodCache)
二级缓存默认是不开启的,需要使用 <cache></cache>
或 @CacheNamespace
标注 mapper 开启二级缓存
在每个 select statement 上还有两个参数可以控制:useCache
和 flushCache
,注解开发时可使用 @Options
配置这两个参数
2.1) 二级缓存原理
2.1.1) Load
从 openSession 开始讲起,DefaultSqlSessionFactory:
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType); // 这里创建了一个 executor 实例
return new DefaultSqlSession(configuration, executor, autoCommit); // 用 DefaultSqlSession 包装了 executor
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
获取 executor 实例的过程,如果配置文件中开启了二级缓存,则使用装饰者模式将默认 executor 包装扩展
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) { // 是否开启二级缓存,如果开启则包装扩展
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
从 CachingExecutor 中成员变量和构造方法可以看出装饰者模式
public class CachingExecutor implements Executor {
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
}
CachingExecutor 中真正的查询是这个方法
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache(); // 从 MappedStatement 加载 Cache(此 Cache 是在加载配置时初始化的)
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);
}
以上就是二级缓存的加载。
2.1.2) Create
创建就是在上一节 tcm.putObject(cache, key, list) 这一行,数据结构和一级缓存基本一致
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
2.1.3) Use
使用和还是在这个核心 query 方法中,但是这个 putObject 只是将数据放入了一个待提交的 Map 中
,在 commit 方法被调用时才真正提交至二级缓存,略复杂先不深究
List<E> list = (List<E>) tcm.getObject(cache, key); // 从二级缓存中取
if (list == null) { // 取不到则走 DB 查询并塞入缓存
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
2.1.4) Clear
CachingExecutor 的清除缓存方法为 flushCacheIfRequired 方法用于操作 clear 的标志位
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
真正的清理缓存也是在 TransactionCache 的 commit 中进行的
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
二级缓存略微复杂,先暂搁了,本文也参考的美团的一篇文章:
聊聊 MyBatis 缓存机制