前言
MyBatis为了减少对数据库的查询,避免频繁的数据库交互,提供了一级缓存和二级缓存。本文将对一级缓存进行介绍并结合源码分析如果关闭一级缓存
提示:以下是本篇文章正文内容,下面案例可供参考
一、一级缓存
一级缓存在MyBatis中对应的属性为org.apache.ibatis.executor.BaseExecutor#localCache。在构造BaseExecutor对象的时候就会实例化这个属性
protected PerpetualCache localCache; // 一级缓存
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入到上面的localCache当中。这个缓存对象如下
public class PerpetualCache implements Cache {
private final String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
public PerpetualCache(String id) {
this.id = id;
}
// 其他方法略
}
不难看出,其实就是存储到一个HashMap当中的。在MyBatis执行查询方法的时候
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
这里就是通过org.apache.ibatis.executor.BaseExecutor#createCacheKey这个方法来创建缓存的主键对象的。这里面的几个参数都决定了最后的缓存主键对象。其中MappedStatement代表的是MyBatis中mapper.xml文件中的带有<select>
标签的语句,第二个parameter代表的是传入的参数,而第三个rowBounds是分页参数(一般仅在分页的时候使用),而boundSql是查询语句、参数以及参数类型的包装类。所以,最终决定两次查询是否是同一个的就是必须是同一个mapper文件中同一个<select>
,也就同一个namespace+id
,另外还有传入的参数必须一致,另外如果是分页语句,则分页的条件必须一致,如下所示:
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
一级缓存是存在于SqlSession的生命周期的,也叫本地缓存。为什么这么说呢?在这个类当中存在以下的方法
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
这个方法就是用于清空一级缓存的。查看这个方法的使用情况
只要对事务有一点了解的话,就不难看出在这里commit和rollback方法中都会调用清空缓存的方法,所以只要事务结束一级缓存就被清空了,而在MyBatis当中,SqlSession(会话)就代表一个事务了。在上面我们还可以看到在update方法中也调用了clearLocalCache方法,其实这里update代表了insert、update、delete三中类型的。以下为SqlSession接口在MyBatis中的唯一实现类DefaultSqlSession中的实现
所以,我们可以得出结论,任何的INSERT、UPDATE、DELETE操作都会清空一级缓存。
最后让我们来具体分析org.apache.ibatis.executor.BaseExecutor#query
方法中一级缓存是如何起作用的。
@Override
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.");
}
1. 如果<select>标签中flushCache属性设置为true的话 在查询之前就会清空缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
2. resultHandler如果为空 就会根据key查询一级缓存
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
3. 通过一级缓存查询的结果不为空 则尝试处理缓存的参数 只在查询类型为CALLABLE有用
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
4. 如果没有缓存数据 则执行数据库查询 并将查询结果缓存
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
5. 如果localCacheScope设置为STATEMENT类型,则查询完就清空缓存(语句级别)
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
从上面的代码可以看出,一级缓存就是在执行数据库真实查询之前首先从本地缓存中读取,如果能够读取到,就不会去查询数据库。
二、关闭一级缓存
同时我们也不难看出,有两种方式可以关闭一级缓存。第一种方式:通过flushCache标签(设置为true每次查询之前都会清空缓存)
<select id="selectUserJobs1" resultMap="userAndJobs1" flushCache="true">
第二种方式:通过通过设置localCacheScope这个配置来关闭一级缓存。以下为MyBatis核心配置文件中的配置参数,其中localCacheScope就可以将一级缓存限定在语句级别,而不是SqlSession级别。从某种程度来说就是关闭了一级缓存。
<!-- 参数设置 -->
<settings>
<!-- 这个配置使全局的映射器启用或禁用缓存 -->
<setting name="cacheEnabled" value="true" />
<!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载 -->
<setting name="lazyLoadingEnabled" value="true" />
<!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载 -->
<setting name="aggressiveLazyLoading" value="true" />
<!-- 允许或不允许多种结果集从一个单独的语句中返回(需要适合的驱动) -->
<setting name="multipleResultSetsEnabled" value="true" />
<!-- 使用列标签代替列名。不同的驱动在这方便表现不同。参考驱动文档或充分测试两种方法来决定所使用的驱动 -->
<setting name="useColumnLabel" value="true" />
<!-- 允许JDBC支持生成的键。需要适合的驱动。如果设置为true则这个设置强制生成的键被使用,尽管一些驱动拒绝兼容但仍然有效(比如Derby) -->
<setting name="useGeneratedKeys" value="true" />
<!-- 指定MyBatis如何自动映射列到字段/属性。PARTIAL只会自动映射简单,没有嵌套的结果。FULL会自动映射任意复杂的结果(嵌套的或其他情况) -->
<setting name="autoMappingBehavior" value="PARTIAL" />
<!--当检测出未知列(或未知属性)时,如何处理,默认情况下没有任何提示,这在测试的时候很不方便,不容易找到错误。 NONE : 不做任何处理
(默认值) WARNING : 警告日志形式的详细信息 FAILING : 映射失败,抛出异常和详细信息 -->
<setting name="autoMappingUnknownColumnBehavior" value="WARNING" />
<!-- 配置默认的执行器。SIMPLE执行器没有什么特别之处。REUSE执行器重用预处理语句。BATCH执行器重用语句和批量更新 -->
<setting name="defaultExecutorType" value="REUSE" />
<!-- 设置超时时间,它决定驱动等待一个数据库响应的时间 -->
<setting name="defaultStatementTimeout" value="25000" />
<!--设置查询返回值数量,可以被查询数值覆盖 -->
<setting name="defaultFetchSize" value="100" />
<!-- 允许在嵌套语句中使用分页 -->
<setting name="safeRowBoundsEnabled" value="false" />
<!--是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn
的类似映射。 -->
<setting name="mapUnderscoreToCamelCase" value="false" />
<!--MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。
默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession
的不同调用将不会共享数据。 -->
<setting name="localCacheScope" value="SESSION" />
<!-- 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。 某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如
NULL、VARCHAR OTHER。 -->
<setting name="jdbcTypeForNull" value="OTHER" />
<!-- 指定哪个对象的方法触发一次延迟加载。 -->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString" />
</settings>
如果不使用配置文件,可以直接用Java代码来设置这个参数
sqlSessionFactory.getConfiguration().setLocalCacheScope(LocalCacheScope.STATEMENT);
总结
一级缓存默认会启动,想要关闭一级缓存可以通过flushCache标签或者localCacheScope修改缓存范围,一级缓存存在于SqlSession的生命周期中,在同一个SqlSession中查询时,MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中(HashMap当中,这里用HashMap会有多线程安全问题吗?)。如果同一个SqlSession中执行的方法和参数完全一致(分页情况下还要考虑分页参数),那么通过算法会生成相同的键值,当Map缓存对象当中已经存在该键值时,则会返回缓存中的对象,任何的INSERT、UPDATE、DELETE操作都会清空一级缓存