因负责了公司的后端系统,业务人员经常有一些导出数量较大的操作 (百万以上),我们大部分通过成熟的批处理框架解决,但是少不了一些繁琐的配置。故写了一个基于 mybatis 分页一页页的查询写入文件的方式功能,没想到引发了了一场 OutOfMemoryError。现将问题原因记录。模拟此次事故代码如下
public void export(){try (SqlSession session = sqlSessionFactory.openSession()) {OrderMapper mapper = session.getMapper(OrderMapper.class);
for(int i=0;i<MAX_PAGE;i++){List<Map> list=mapper.query(i*10,10);
for (Map map : list) {writeToCsv(map)
...
}
list=null;
}
}catch (Exception e){e.printStackTrace();
}
}
在我的机器上本地运行,设置内存相关参数:
-Xms20M -Xmx20M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=e:/
运行几秒中抛出 OutOfMemoryError 异常。
通过打印的堆内存文件分析,org.apache.ibatis.cache.impl.PerpetualCache 占用了 85% 的堆内存,问题的原因肯定时这个 cache 的问题。
通过代码执行,query 代码如下。同一个 sqlSession(也即同一个 BaseExecutor) 查询的时候首先根据条件生成 CacheKey(具体细节看源码), 再根据 cacheKey 查询 localCache 有无结果,如果有缓存直接返回,无缓存在查询后放入缓存返回。这是 mybatis 的一级缓存。
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 (this.closed) {throw new ExecutorException("Executor was closed.");
} else {if (this.queryStack == 0 && ms.isFlushCacheRequired()) {this.clearLocalCache();
}
List list;
try {
++this.queryStack;
list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
if (list != null) {this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {--this.queryStack;}
if (this.queryStack == 0) {Iterator var8 = this.deferredLoads.iterator();
while(var8.hasNext()) {BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
deferredLoad.load();}
this.deferredLoads.clear();
if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {this.clearLocalCache();
}
}
return list;
}
}
如何规避掉 mybatis 一级缓存呢,通过源码来看有两种方式
1、通过 queryStack == 0 且 flushCacheRequired==true 则会清理缓存。queryStack 是每次用同一个 sqlSession 执行这个 query 方法的时候 +1,执行结束放入缓存后减 1。单线程的查询 == 0 的条件每次都能满足,多线程同一个 sqlSession 的话可能会有些问题哦。flushCacheRequired 的设置如下:
<select id="query" resultType="map" parameterType="map" flushCache="true">
</select>
2、queryStack== 0 且 configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) 会清理缓存。
localcacheScope 的设置如下:
<settings>
<setting name="localCacheScope" value="STATEMENT"/>
</settings>
本篇下记录到此,后续会看一下二级缓存是否也造成内存溢出的情况。