1.mybatis 缓存概述
myBatis中存在两个缓存,一级缓存和二级缓存。
- 一级缓存:也叫做会话级缓存,生命周期仅存在于当前会话,不可以直接关闭。但可以通过flushCache和localCacheScope对其做相应控制。
- 二级缓存:也叫应用级性缓存,缓存对象存在于整个应用周期,而且可以跨线程使用。
关于二级缓存将在后续章节,详细说明。文本先聚焦一级缓存。首先来看如何才能命中一级缓存。
2.一级缓存的存储形式
一级缓存是以Map形式存在的key-value。存在于BaseExecutor的局部变量中,SqlSession -> Executor -> localCache 一对一的关系。可知一级缓存仅存在于当前会话。
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory.getLog(BaseExecutor.class);
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
// 一级缓存存储形式
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
protected int queryStack;
private boolean closed;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
// 一级缓存存储形式
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
// ...
}
// 一级缓存,存储的对象
public class PerpetualCache implements Cache {
private final String id;
// HashMap,key-value形式
private final Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
// ...
}
3.一级缓存的命中场景
关于一级缓存的命中可大致分为两个场景
- 第一满足特定命中参数
- 第二不触发清空方法
3.1缓存命中参数
缓存的key为CacheKey对象,主要与以下几个参数有关:
- 同一个会话
- SQL相同
- 参数相同
- 相同的MapperStatement ID
- RowBounds行范围相同
- 相同的environmentId(防止切换数据库)
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
// MapperStatement ID
cacheKey.update(ms.getId());
// rowBounds分页参数有关
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
// 执行SQL
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
// 数据库环境ID,防止以上条件都相同切换数据库
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
3.2触发清空缓存
- 手动调用clearCache
- 执行提交回滚
- 执行update
- 配置flushCache=true
- 缓存作用域为Statement
// sqlSession代码
public void clearCache() {
executor.clearLocalCache();
}
// BaseExecutor代码
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
// BaseExecutor代码,回滚
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
// 清除缓存
clearLocalCache();
flushStatements(true);
} finally {
if (required) {
transaction.rollback();
}
}
}
}
// BaseExecutor代码,提交
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
// 清除缓存
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}
// 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);
}
// BaseExecutor代码,查询时设置flushCacheRequired
// MapperXml文件配置flushCache=true,查询前会清除缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
// BaseExecutor代码,查询数据后。先添加进缓存,后又清除
// 设置<setting name="localCacheScope" value="STATEMENT"/>
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// 清除缓存
clearLocalCache();
}
4.总结
一级缓存逻辑就存在于 BaseExecutor (基础执行器)里面。当会话接收到查询请求之后,会交给执行器的Query方法,在这里会通过 Sql、参数、分页条件等参数创建一个缓存key,在基于这个key去 PerpetualCache中查找对应的缓存值,如果有主直接返回。没有就会查询数据库,然后在填充缓存。一级缓存最终就是个HashMap
缓存的清空对应BaseExecutor中的 clearLocalCache.方法。只要找到调用该方法地方,就知道哪些场景中会清空缓存了。
- update: 执行任意增删改
- select:查询又分为两种情况清空,前置清空:配置了flushCache=true。后置清空:配置了缓存作用域为statement 查询结束合会清空缓存。
- commit:提交前清空
- Rolback:回滚前清空
缓存的清空对应BaseExecutor中的 clearLocalCache.方法。只要找到调用该方法地方,就知道哪些场景中会清空缓存了。
- update: 执行任意增删改
- select:查询又分为两种情况清空,前置清空:配置了flushCache=true。后置清空:配置了缓存作用域为statement 查询结束合会清空缓存。
- commit:提交前清空
- Rolback:回滚前清空
注意:clearLocalCache 不是清空某条具体数据,而清当前会话下所有一级缓存数据(整个HashMap)。