流实现低内存下读取大量数据和处理并存储大文件

昨天看到有朋友A在问,“个人程序一旦导出稍微大点的Excel必定会OOM,大家的应用导出多少数据量都没有问题,如何作到的?”。这里涉及到两个问题:读取和写入的内存占用。其实业务很简单:从数据库中读取数据,而后生成Excel。那么咱们以解决A的业务问题入手,从而来解决这一类的问题。html

        读取:A使用JDBC从数据库中读取数据,当返回结果较多且内存不足以容纳结果时可能致使OOM,这时候只须要增长虚拟机的heap内存便可解决当下问题,但这种方法并不能解决根本问题。在java.sql.Statement中有两个方法:java

//设置该Statement生成的全部java.sql.ResultSet可容纳结果的最大行数. 
void setMaxRows(int max) throws SQLException;      

//设置Statement生成的全部java.sql.ResultSet在获取更多行时应从数据库获取的行数
void setFetchSize(int rows) throws SQLException;

可使用setMaxRows限定结果集数量来控制数据量也是一个不错的办法,可是须要业务上妥协不能使用完整的数据,不太合适。第二个方法,setFetchSize按照一次获取rows条记录放在客户端,客户端处理完成后再去取rows条记录到客户端来,直至数据取完,若是只是为了取数据那么这种方式好像没什么卵用,由于最终数据仍是要全取回来的。分析A的业务,他想要的效果就是读取全部数据而后导出。若是以数据一批取回来一批处理掉这样相似于“流”的方式处理这个业务,只把Java程序当作整个过程当中的一个可加工数据管道就完美了。想必每一个使用Java的同窗在学习I/O类库时都知道“流”这个概念,它比较抽象,表明一些可以产出数据的数据源和可以接收数据的接收端对象,在A的业务中,咱们须要源和目标同时支持流的方式才能让Java程序成为管道而不是数据中转站。经过数据库的setFetchSize设置每批数据的大小,从而宏观上来看就是成为流的方式,读取没有问题了。理清方案,接下来处理写入。mysql

        写入:目标文件为Excel,目前Java读写Excel较为稳定且一直在更新的类库只有POI http://poi.apache.org/了,咱们以POI为依赖去找相关API。在较早的版本中彷佛OOM的问题一直存在,仔细寻找发现多了一个Api叫SXSSF (Since POI 3.8 beta3 https://poi.apache.org/spreadsheet/how-to.html#SXSSF+%28Streaming+Usermodel+API%29),开始提供了一组streaming XSSF的Api,其实就是流方式操做Excel。它在初始化时能够设置rowAccessWindowSize:sql

public SXSSFWorkbook(int rowAccessWindowSize)

这个值和Jdbc中的setFetchSize相似,当数据量达到rowAccessWindowSize时,将Java程序中的数据flush到磁盘中,当rowAccessWindowSize足够小时,宏观上就呈现出流的方式了。数据库

        以上面的方案为依据,作下数据对比 :apache

实验环境:-Xms256m -Xmx512m -XX:PermSize=128m
数据:select * from `big_table`  //825473行 约100m

实验一:未设置setFetchSize,未使用SXSSF。服务器

现象:运行约3分钟,导出文件失败,读取数据时内存直线飙升,生成XSSF后放缓,最终该线程抛出OOM后挂起,系统强制GC,内存恢复正常水位。oracle

结论:因为没有设置setFetchSize,数据所有拿到客户端,而客户端又要对数据生成XSSF对象,内存中将有原数据至少两倍大小的对象,很快就会出现异常。生产和消费胃口都比较大,最终仓库挂了。app

实验二:设置setFetchSize,未使用SXSSF。jvm

现象:运行约4分钟,导出文件失败,读取数据时内存缓慢上升,频繁gc,内存满了之后直至ResultSet没法获取下一批数据后县城hang住,最终OOM。

结论:设置了setFetchSize,内存中的数据全为XSSF对象,因为数据是按照批次来获取,当前批次未结束时内存不会增长,因此整个过程较为缓慢。生产者依赖消费者,消费慢生产就慢最终瘫痪。

实验三:未设置setFetchSize,使用SXSSF。

现象:运行约4分钟,导出文件成功,读取数据时内存上升较快,达到内存峰值时频繁gc,最终文件导出成功,内存释放恢复正常。

结论:未设置setFetchSize,数据被一次性所有捞取,在读取过程当中生成SXSSF,因为SXSSF会去生成临时文件,频繁appendRow、flush致使gc频繁,持久战成功。生产者大批数据,消费者不依赖仓库中转,高频运输最终完成工做。若是jvm内存较小时一样会OOM。

实验四:设置setFetchSize,使用SXSSF。这是咱们计划的方案。

现象:运行约1分钟,导出文件成功,读取数据时内存有内存起伏,总体稳定,gc相对较少,。

结论:这种方式中Java程序充当了管道的做用,几乎没有内存消耗,且执行速度快。

实验结论:在整个导出文件的逻辑中,任何一个动做均可能成为瓶颈,整条链路都为流模式时,程序只须要充当管道角色便可。Java I/O类库提供了丰富的输入和输出接口,熟知它们的应用场景和使用方法颇有必要,在遇到此类问题时多加注意便可避免系统不可用的风险。

 

附录信息:

setFetchSize:不一样的数据库类型JDBC的实现大有不一样,下面梳理了如下几种数据库类型如何使用流模式:

MySQL:从mysql-connector-java中能够看到StatementImpl包装了这份配置。http://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-implementation-notes.html

/* (non-Javadoc)
 * @see com.mysql.jdbc.IStatement#enableStreamingResults()
 */
public void enableStreamingResults() throws SQLException {
   synchronized (checkClosed().getConnectionMutex()) {
      this.originalResultSetType = this.resultSetType;
      this.originalFetchSize = this.fetchSize;

      setFetchSize(Integer.MIN_VALUE);
      setResultSetType(ResultSet.TYPE_FORWARD_ONLY);
   }
}

Oracle:默认从服务器一次取出fetchSize的数据,从代码中可看到默认为3. 但当遇到大数据量时还需设置ResultSetType为TYPE_FORWARD_ONLY https://docs.oracle.com/cd/B10501_01/java.920/a96654/resltset.htm

PostgreSQL:必须设置autocommit=false,而后设置fetchSize和ResultSetType。 https://jdbc.postgresql.org/documentation/94/query.html#query-with-cursor

SQLServer:须要设置SQLServer驱动下的一个参数setResponseBuffering,而后设置ResultSetType便可。https://msdn.microsoft.com/zh-cn/library/bb879937(v=sql.110).aspx

相关文章
相关标签/搜索