1. 程式人生 > >java千萬級別資料生成檔案思路和優化

java千萬級別資料生成檔案思路和優化

iteye/csdn 個人原創,轉載請標明出處

              一年前寫過一個百萬級別資料庫資料生成配置xml檔案的程式,程式目的是用來把資料庫裡面的資料生成xml檔案.程式可以配置多少檔案生成到一個檔案中去.

程式剛開始設計的時候說的是最多百萬級別資料,最多50W資料生成到一個xml檔案裡面去,所以在做測試的時候自己也只是造了100W的資料並沒有做過多資料量的測試,然後問題就來了....由於程式使用的局點資料量巨大,需要生成xml檔案的客戶資料接近千萬級別的程度,而現場對程式的配置大約是100W條資料生成一個xml檔案裡面去,程式在這樣的大資料量下面偶爾會有崩潰.

最近幾天現場催的比較緊,最近抽空把這個問題處理了一下,在解決問題的過程中我把解決的步驟和方法記錄了下來,正好和大家共享一下

現場提的問題概況:

    資料量:生成xml,每個檔案100W+ 條的資料

    記憶體控制:最好不要超過512M

    問題詳情:在處理70W左右的時候記憶體溢位

一、先來看一下程式要生成的xml檔案的結構

Xml程式碼  收藏程式碼
  1. <File>  
  2.   <FileType>1</FileType>  
  3.   <RType>12</RType>  
  4.   <Version>03</Version>  
  5.   <BNo>004</BNo>  
  6.   <FileQ>5</FileQ>  
  7.   <FNo>0006</
    FNo>  
  8.   <RecordNum>1000000</RecordNum>  
  9.   <!-- 上面是檔案頭  下面是百萬個<RecordList>  -->  
  10.   <RecordList>  
  11.     <Msisdn>10350719507</Msisdn>  
  12.     <State>1</State>  
  13.     <StartDate>20110303</StartDate>  
  14.     <Date>20110419</Date>  
  15.     <Balance>45000</
    Balance>  
  16.   </RecordList>  
  17.    ...  <!-- 可能百萬個  <RecordList> 塊-->  
  18.  </File>  

二、給大家說一下如何把大資料生成xml檔案

1、小資料量的情況下    <  1W條資料

比較好用的方法是使用開源框架,比如XStream 直接把javabean 生成 xml

 優點:api操作簡單,方便維護

缺點:資料量大的情況下太消耗記憶體

2、大資料量生成一個xml檔案(本程式採用的方法)

自己做的一個可以使用極少的記憶體生成無限制大的xml檔案框架由3部分生成xml檔案

第一部分:生成檔案頭  

                            例如: xxx.toXML(Object obj, String fileName)

第二部分:通過每次向檔案裡面追加3000(可配置)條資料的形式生成檔案塊  

                            例如:xxx.appendXML(Object object);  //object 可以是ArrayList 或者一個單獨的javaBean

第三部分:生成xml檔案尾巴   

                           例如:xxx.finishXML();

程式中的呼叫:呼叫xxx.toXML(Object obj, String fileName) 生成檔案頭之後,可以迴圈從資料庫中讀取資料生成ArrayList,通過xxx.appendXML(Object object) 方法追加到xml檔案裡面,xxx.finishXML() 對檔案進行收尾

對框架說明:我上面提供的例子有檔案頭 + 檔案塊 + 檔案尾巴. 如果和你們的實際使用檔案不太一致的話,可以參考上面提供的思路修改一下即可,主要的方法是把相同的檔案塊部分分離出來通過追加的形式寫入xml檔案.

                      有了思路之後,大家可以嘗試著自己寫一個類似的大資料處理框架(千萬級別以上),如何有什麼需要幫助的可以直接聯絡我,因為是公司的程式,不太敢放出來,怕......

三、我是如何測試效能和優化的

1、手動排除

根據檔案崩潰時候的日誌發現是在生成xml的框架裡面報的錯誤,第一想到的是框架有些資源沒有釋放.於是把自己做的檔案生成框架整體的排查了一遍,並且自己寫個簡單程式生成200萬條資料,使用xml框架生成一個xml檔案,整個生成過程中工作管理員(xp)檢視程式對應的java程序使用的記憶體基本在20M左右,因此排除框架的問題.懷疑是資料庫查詢和呼叫框架的部門出現問題.

檢測了一遍主程式的關鍵部分程式碼,優化了一下字串處理.手動的釋放一些物件的記憶體(例如:呼叫ArrayList.clear(),或者把物件置空等),分配512記憶體後執行程式,60萬資料的時候記憶體溢位,因為能主動釋放的物件都已經釋放掉了,還是沒有解決,果斷放棄看程式碼,準備使用JProfile進行記憶體檢測.

2、手動排除沒有解決,藉助記憶體分析工具JProfile進行排除

通過在資料庫中生成300W條資料,在JProfile上面多跑程式,一邊執行,一邊呼叫JProfile 提供的執行GC按鈕主動執行垃圾回收,執行50W資料後,通過檢測中發現 java.long.String[] 和 oracle.jdbc.driver.Binder[] 兩個物件的數目一直保持在自增狀態,而且數目基本上差不多,物件數目 都在200W以上,由於java.long.String[]物件是需要依賴物件而存在的,因此斷定問題就出在oracle.jdbc.driver.Binder[]上面,由於改物件存在引用導致String[]不能正常回收.

3、通過在JProfile物件檢視物件的管理

                   檢測到oracle.jdbc.driver.Binder 被 oracle.jdbc.driver.T4CPreparedStatement 引起,而T4CPreparedStatement正好是Oracle對jdbc OraclePreparedStatement的具體實現,因此斷定是在資料庫處理方面出現的問題導致oracle.jdbc.driver.Binder物件不能正常釋放,通過再一次有目的的檢測程式碼,排查jdbc資料查詢的問題,把問題的矛頭直至資料庫的批處理和事務處理.因此程式是每生成一個檔案成功後,會把已經處理的資料轉移到對應的歷史表中進行備份,而再個表操作的過程中使用了批處理和事務,使用批處理主要是保證執行速度,使用事務主要是保證同時成功和失敗。

4、又因此程式每次從資料庫中查詢3000條資料處理,所以準備監控oracle.jdbc.driver.Binder的物件數目是否和查詢次數對應.,通過在程式中Sysout輸出查詢次數 + JProfile執行GC測試 Binder,資料匹配,證實是java在資料庫批處理的過程中有些問題.

5、專門把批處理程式碼提取出來通過JProfile記憶體分析.最終問題定位完畢.

原因如下:100W資料生成一個檔案的過程中,等檔案生成完畢之後才能把資料庫中的資料備份到歷史表中,這個時候才能進行事務的提交,也就是執行commit(), 並且刪除原表資料,100W資料按照3000一批寫入檔案,每批次只是通過 PreparedStatement.addBatch();加入到批次裡面去,並沒有執行PreparedStatement.executeBatch(),而是在commit()之前統一呼叫的PreparedStatement.executeBatch(),這樣的話PreparedStatement就會快取100W條資料資訊,造成了記憶體溢位.

錯誤的方法如下:

Java程式碼  收藏程式碼
  1. try{  
  2.             conn.setAutoCommit(false);  
  3.             pst = conn.prepareStatement(insertSql);  
  4.             pstDel = conn.prepareStatement(delSql);  
  5.             pstUpdate = conn.prepareStatement(sql);  
  6.             ...   
  7.             //totalSize = 100W資料 / 3000一批次  
  8.             for (int i = 1; i <= totalSize; i++) {  
  9.                 client.appendXML(list);  
  10.             }  
  11.             // 錯誤的使用方法  
  12.             client.finishXML();  
  13.             pst.executeBatch();  
  14.             pstDel.executeBatch();  
  15.         }  
  16.          ...  
  17.         finally {  
  18.             try {  
  19.                 if (isError) {  
  20.                     conn.rollback();  
  21.                 }  
  22.                 else  
  23.                     conn.commit();  
  24.                ...  
  25.             }  
  26.           ...  
  27.         }  

正確的方法如下

 try{

Java程式碼  收藏程式碼
  1.    conn.setAutoCommit(false);  
  2.    pst = conn.prepareStatement(insertSql);  
  3.    pstDel = conn.prepareStatement(delSql);  
  4.    pstUpdate = conn.prepareStatement(sql);  
  5.    ...   
  6.    //totalSize = 100W資料 / 3000一批次  
  7.    for (int i = 1; i <= totalSize; i++) {  
  8.        list = 從資料庫中查詢3000條資料  
  9.        client.appendXML(list);  
  10.       pst.executeBatch();  
  11.       pstDel.executeBatch();  
  12.    }  
  13.    client.finishXML();  
  14. ...  
  15. inally {  
  16.    try {  
  17.        if (isError) {  
  18.            conn.rollback();  
  19.        }  
  20.        else  
  21.            conn.commit();  
  22.       ...  
  23.    }  
  24.  ...  

如果碰到和我一樣的需要給大家一個提醒.

oracle在每次執行executeBatch();進行批處理的時候,當前connection對應的rownum會根據操作的結果發生變化.

在執行pst.executeBatch(); 之後,當前連線的 rownum 數就會發生變化. 因此凡是通過rownum查詢資料的程式都要小心這一點

                下一篇將整理寫java大資料(千萬級別以上的)處理,包括 ftp大資料處理、檔案生成大資料處理、資料庫轉移大資料處理、檔案讀取大資料處理等等. 

                        轉載請表明出處。

<script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"></script>