缓存key
在前面的初始时,xml配置中的每一个jdbc语句都会被封装为一个MappedStatement对象,存储在Configuration中。key就是xml中配置的namespace.id
查询时,从Configuration根据statement获取对应的MappedStatement,交给执行器执行
CachingExecutor
首先,MappedStatement调用getBoundSql,获取参数值对应的完整sql语句。此处是BoundSql。
然后,创建缓存key,CacheKey对象
cacheKey主要由以下部分组成
//语句id
cacheKey.update(ms.getId());
//分页参数
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
//sql语句
cacheKey.update(boundSql.getSql());
//参数的值
cacheKey.update(value);
//当前环境id
cacheKey.update(configuration.getEnvironment().getId());
由此可见,如果传递的参数一致,语句一致,那么缓存就会生效。
二级缓存
概念理解
二级缓存是namespace级别的缓存。默认关闭状态。二级缓存是可以跨SqlSession存在的。
开启二级缓存的方式:
当前xml中加入<cache/>,会开启当前namespace的缓存
全局开启,在mybatis配置文件中加入 <setting name="cacheEnabled" value="true"/>
二级缓存生效时机:
在执行语句时,二级缓存只是放入了TransactionalCache的entriesToAddOnCommit中,并没有真正放入缓存。而是当调用SqlSession.commit()或者close()时,才会放入真正缓存中。
二级缓存失效时机:
默认过期失效
执行了insert,update,delete语句,且commit或者close之后清空缓存
配置了useCache = false或者resultHandler不为null,不会使用二级缓存
配置了flushCache=true,会在提交事务时清除缓存。
注意事项
entriesToAddOnCommit
这个是缓存中一个map,在执行语句时的缓存操作,都是操作该对象。只有提交或者关闭sqlSession的时候,才会根据该属性写入真实的缓存或者清除缓存。
生产环境不建议开启二级缓存。
源码分析
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) {
//刷新二级缓存(存在缓存且flushCache为true)
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);
}
ms.getCache()是从ms上获取的,因此二级缓存的开关位于mapper.xml中配置。每一个命名空间可以设置一个独立的二级缓存。<cache-ref>可以共享别的二级缓存
<mapper namespace="user">
<cache/>
<select id="findUserById" parameterType="int" resultType="com.mahaonan.test.pojo.User" flushCache="true">
select id,username from user where id = #{id}
</select>
</mapper>
如上所示,就开启了该xml对应的mybatis的二级缓存。
一级缓存
概念理解
一级缓存是mybatis默认开启的,且不可关闭。
有两种缓存级别,SESSION和STATEMENT,默认为SESSION级别。
SESSION:在一个sqlSession内共享缓存
STATEMENT:缓存只对当前执行的这一个statement有效。
在mybatis.config中进行配置
<settings>
<setting name="localCacheScope" value="STATEMENT"/>
</settings>
虽然mybatis不可关闭一级缓存,但是可以设置为STATEMENT级别,也相当于关闭了。或者在语句中加入flushCache=true配置
一级缓存是称为PerpetualCache的本地缓存,其实现就是一个简单的map缓存。在创建Executor的时候会被创建并保存在Executor中。
在默认的mybatis中,一级缓存返回的是同一个对象的引用,因此需要注意坑,如果修改了该对象,会引起问题。
一级缓存的清除时机:
一个sqlSession结束后清除
insert,update,delete语句执行后
配置了flushCache=true,每次执行都会清除
在整合spring中,在没有事务的情况下,每次执行sql,都会新创建一个sqlSession,因此没有一级缓存。
开启事务的情况下,会用到一级缓存。
缓存问题
@Transactional
public SpeakLogDTO get(Long id) {
LambdaQueryWrapper<SpeakLogPO> queryWrapper = Wrappers.<SpeakLogPO>lambdaQuery().eq(SpeakLogPO::getId, id);
List<SpeakLogPO> speakLogPOS = mapper.selectList(queryWrapper);
List<SpeakLogPO> speakLogPOS2 = mapper.selectList(queryWrapper);
speakLogPOS.get(0).setContent("123");
System.out.println(speakLogPOS == speakLogPOS2);
System.out.println(speakLogPOS.get(0) == speakLogPOS2.get(0));
return converter.PO2DTO(speakLogPOS2.get(0));
}
如上所述:在事务中,一级缓存是生效的。因为spring会从ThreadLocal中获取同一个sqlSession。
此时speakLogPOS和speakLogPOS2是同一个对象。内部的元素也是同一个对象。因此如果修改了对象属性,会导致返回结果出问题。
因此,对于事务查询出来的结果,且两次查询之间没有增删改操作,避免去直接修改结果,而是转换为DTO再去修改。
或者设置local-cache-scope为statement级别
源码分析
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;
if (list != null) {
//如果一级缓存中有数据,则处理本地缓存结果输出参数(存储过程)
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//如果一级缓存中没有数据,则查询数据库并且将查询结果放入一级缓存
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
//如果缓存是STATEMENT级别,清除缓存
// issue #482
clearLocalCache();
}
}
return list;
}
从上面源码可以看出,当resultHandler不为null时,就不会用到一级缓存。
mybatis-plus的个别方法,例如selectOne就是利用这种方式避免了一级缓存返回同一对象导致问题。
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
//1. 首先将占位符放入缓存
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//2. 执行查询
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
//3. 执行完成移除占位符
localCache.removeObject(key);
}
//4. 将查询结果放入缓存
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}