因负责了公司的后端系统,业务人员经常有一些导出数量较大的操作(百万以上),我们大部分通过成熟的批处理框架解决,但是少不了一些繁琐的配置。故写了一个基于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的问题。
1571971665(1).png

通过代码执行,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>

本篇下记录到此,后续会看一下二级缓存是否也造成内存溢出的情况。


tiger1000
2 声望3 粉丝

每天进步一点点,轻松又又快乐


下一篇 »
MVCC