记录一次mybatis localCache引起的bug
情况描述:有一个多次查询条件相同,查询结果脏读的bug。多次查询在同一个事务中.这里进行源码分析。
这里没有过多描述我的环境配置,更加关注源码的实现。
必备知识
- java基本语法了解
- hashMap理解原理,以及了解java实现.
这里主要要介绍几个
- SqlSession
- Executor
- Cache
- Transaction
下面使用语言描述其关系
sqlSession:应该可以说是mybatis的核心了,执行方法全在里面.Executor Cache Transaction都是它的功能.
Executor:执行器,真正的sql语句在其中。transaction就是它的属性。这次的bug,也是出现在这里的.就是因为同一个SqlSession.
Cache:缓存,这一篇幅只讨论PerpetualCache localCache,它的实现很简单就是一个hashMap还有一个name名称.
Transaction:负责事务部分.
类结构图
bug发生原因
我们进入BaseExecutor的select方法来看就一目了然了。
@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);
}
@SuppressWarnings("unchecked")
@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.");
}
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();
}
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
}
return list;
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
//把查询结果插入缓存 上面清空了 也相当于 刷新了一遍刷新缓存 为什么不直接putObject?这里也就是出现问题的,根源,直接把查询结果引用返回给我了。
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
然后我还阅读了,mybatis-Spring的文档。这样写到。
在事务处理期间,一个单独的 SqlSession 对象将会被创建 和使用。当事务完成时,这个 session 会以合适的方式提交或回滚。
前面我也说了,我的整个流程在一个事务中。通过上面的源码,给我的感觉它除了最开始的查询是走了数据库,之后就全走了缓存。在我的业务代码中,我和localCache对象同时持有一个对象。我还修改了对象中的,数据。造成了后续流程的错误。
解决方案
- 执行查询前,调用BaseExecutor.clearLocalCache清空缓存
- 拆分大事务为,一个个小事务。
bug总结
事务臃肿
对mybatis的不熟悉