流實現低內存下讀取大量數據和處理並存儲大文件

昨天看到有朋友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

相關文章
相關標籤/搜索