因负责了公司的后端系统,业务人员经常有一些导出数量较大的操作(百万以上),我们大部分通过成熟的批处理框架解决,但是少不了一些繁琐的配置。故写了一个基于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>
本篇下记录到此,后续会看一下二级缓存是否也造成内存溢出的情况。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。