1.场景&问题解决
MyBatis框架提供了一级缓存和二级缓存,其中一级缓存基于SqlSession实现,而二级缓存基于Mapper实现。MyBatis提供的缓存机制是为了提高性能,但有些场景下我们必须关闭,否则会获取错误的数据,造成逻辑错误。常见的一种场景就是跑批处理一些业务时,比如:
@Transactional(rollbackFor = Exception.class)
public void runOnTrigger(String batchNo) {
//分页处理数据
while (true) {
//获取该批次未处理的第一条数据id
Integer startIndex = originDataService.getFirstUnprocessedItemId(batchNo);
log.info("startIndex: {}", startIndex);
if (Objects.isNull(startIndex)) {
log.info("当前批次数据处理结束 batchNo: {}", batchNo);
break;
}
//获取未处理的数据
List<DemoPO> logPOList = originDataService.getUnprocessedItemsByBatchNo(startIndex, BATCH_SIZE, batchNo);
try {
log.info("logPOList: {}", JSON.toJSONString(logPOList));
originDataCollector.doCollect(batchNo, logPOList);
} catch (Exception e) {
log.error("数据处理失败", e);
//更新数据明细处理状态为:处理失败
List<Integer> targetIds = logPOList.stream().map(m -> m.getId()).collect(Collectors.toList());
originDataService.updateItemOnFail(targetIds);
}
}
}
上述代码中,代码段:
Integer startIndex = originDataService.getFirstUnprocessedItemId(batchNo);对应的Mapper为:
@Select("SELECT id FROM seller_trans_log WHERE data_date = #{batchNo} AND process_status = 0 ORDER BY id ASC LIMIT 1;")
Integer getFirstUnprocessedItemId(@Param("batchNo") String batchNo);
如果我们不关闭缓存,那么这条SQL在同一SqlSession中多次执行获取的id将会和第一次查询时的结果一样,始终不变(Mybatis缓存的原因);这显然是不能接受的(造成死循环或者业务处理失败)。怎么解决呢?既然是Mybatis的缓存原因,那我们关闭他就行了。我们可以通过Mybatis的Options注解关闭缓存,如下:
@Options(flushCache = Options.FlushCachePolicy.TRUE)
@Select("SELECT id FROM seller_trans_log WHERE data_date = #{batchNo} AND process_status = 0 ORDER BY id ASC LIMIT 1;")
Integer getFirstUnprocessedItemId(@Param("batchNo") String batchNo);
若sql在xml文件中,我们可以这样关闭缓存
<select id="getIdByFoo" flushCache="true" useCache="false" resultType="java.lang.Integer">
SELECT id
FROM table_a
WHERE foo = #{foo,jdbcType=VARCHAR}
</select>
2.Mybatis缓存概述
一级缓存
MyBatis的一级缓存是SqlSession级别的缓存,SqlSession在执行用户定义的sql语句时会交由Executor组件去执行,一级缓存的实现就是在Executor的子类BaseExecutor中实现的。
public abstract class BaseExecutor implements Executor {
...
//一级缓存的缓存类(内部维护了一个HashMap保存缓存内容)
protected PerpetualCache localCache;
...
以query方法为例,简述如下:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//获取要执行的sql
BoundSql boundSql = ms.getBoundSql(parameter);
//Mybatis中描述缓存值的对象,如果两次查询操作的CacheKey对象相同,就会认为这两次查询执行的是相同的SQL语句
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
createCacheKey:
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
//更新缓存key
CacheKey cacheKey = new CacheKey();
//Mapper的Id,即Mapper命名空间与<select|update|insert|delete>标签的Id组成的全局限定名
cacheKey.update(ms.getId());
//查询结果的偏移量
cacheKey.update(rowBounds.getOffset());
//查询结果的的条数
cacheKey.update(rowBounds.getLimit());
//具体的SQL语句
cacheKey.update(boundSql.getSql());
//SQL语句中需要传递的所有参数
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
...
if (configuration.getEnvironment() != null) {
//MyBatis主配置文件中,通过<environment>标签配置的环境信息对应的Id属性值
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
执行查询时,只有上面的信息完全相同时,才会判定执行的是相同的SQL,才会使用缓存
具体query的逻辑:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
//判断是否刷新缓存
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) {
// issue #482
clearLocalCache();
}
}
return list;
}
如上即为一级缓存的主要内容,感兴趣的可以详细看源码。
二级缓存
Mybatis3.x中默认开启了session级别的二级缓存(基于Mapper实现)
若要启用全局的二级缓存,需要在 SQL 映射文件中添加一行, 具体可参考官方文档:
sql的具体执行方仍然是Executor组件,类Configuration提供了创建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) {
//如果开启二级缓存,则创建CachingExecutor
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
Executor的UML关系如下:
CachingExecutor类实现了二级缓存的功能,类信息如下:
public class CachingExecutor implements Executor {
private final Executor delegate;
//二级缓存
//二级缓存是事务性的,也就是说,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存也会获得更新。
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
事务性缓存管理器:TransactionalCacheManager用于管理所有的二级缓存对象,类信息如下:
public class TransactionalCacheManager {
//通过HashMap维护二级缓存对应的TransactionalCache
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
//清空二级缓存
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
//二级缓存名称的由来
//查询缓存 先通过cache获取二级缓存TransactionalCache,然后再从TransactionalCache 中查询缓存key对应的值
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
//添加缓存
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
//提交时更新缓存
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
//
private TransactionalCache getTransactionalCache(Cache cache) {
//从HashMap中获取二级缓存TransactionalCache
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
//如果Map中没有获取到,创建,放入Map中
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
了解二级缓存后,我们可以看下CachingExecutor的query操作,可以直观看到二级缓存的作用
CachingExecutor:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
//创建缓存key 逻辑和一级缓存一样
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//获取MappedStatement 中维护的缓存对象
Cache cache = ms.getCache();
if (cache != null) {
//校验是否刷新二级缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
//从MappedStatement对象中的二级缓存中获取缓存的内容
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//缓存不存在,查库
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//将查询结果放到MappedStatement 对象对应的二级缓存中
tcm.putObject(cache, key, list);
}
return list;
}
}
//如果MappedStatement 中没有配置二级缓存,直接查库
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
如果执行的是update语句,则同一命名空间下的二级缓存将会被清空
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
//判断<select|update|delete|insert>标签的flushCache属性,如果属性值为true,就清空缓存
//<select>标签的flushCache属性值默认为false,而<update|delete|insert>标签的flushCache属性值默认为true
tcm.clear(cache);
}
}
public boolean isFlushCacheRequired() {
//类MapperBuilderAssistant中维护
return flushCacheRequired;
}
综上我们可以发下,Mybatis的一级缓存和二级缓存的默认实现底层都是通过HashMap实现的,也就是说默认都是本地缓存即堆缓存;二级缓存也可以指定到外部缓存中间件中,如Redis,有兴趣的可以去研究下