java千萬級別數據生成文件思路和優化


              一年前寫過一個百萬級別數據庫數據生成配置xml文件的程序,程序目的是用來把數據庫里面的數據生成xml文件.程序可以配置多少文件生成到一個文件中去.

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

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

 

現場提的問題概況:

    數據量:生成xml,每個文件100W+ 條的數據

    內存控制:最好不要超過512M

    問題詳情:在處理70W左右的時候內存溢出

一、先來看一下程序要生成的xml文件的結構

<File>
  <FileType>1</FileType>
  <RType>12</RType>
  <Version>03</Version>
  <BNo>004</BNo>
  <FileQ>5</FileQ>
  <FNo>0006</FNo>
  <RecordNum>1000000</RecordNum>
  <!-- 上面是文件頭  下面是百萬個<RecordList>  -->
  <RecordList>
    <Msisdn>10350719507</Msisdn>
    <State>1</State>
    <StartDate>20110303</StartDate>
    <Date>20110419</Date>
    <Balance>45000</Balance>
  </RecordList>
   ...  <!-- 可能百萬個  <RecordList> 塊-->
 </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條數據信息,造成了內存溢出.

錯誤的方法如下:

try{
            conn.setAutoCommit(false);
            pst = conn.prepareStatement(insertSql);
            pstDel = conn.prepareStatement(delSql);
            pstUpdate = conn.prepareStatement(sql);
            ... 
            //totalSize = 100W數據 / 3000一批次
            for (int i = 1; i <= totalSize; i++) {
                
                client.appendXML(list);
               
            }
            // 錯誤的使用方法
            client.finishXML();
            pst.executeBatch();
            pstDel.executeBatch();
        }
         ...
        finally {
            try {
                if (isError) {
                    conn.rollback();
                }
                else
                    conn.commit();
               ...
            }
          ...
        }

正確的方法如下

try{          
  conn.setAutoCommit(false);
            pst = conn.prepareStatement(insertSql);
            pstDel = conn.prepareStatement(delSql);
            pstUpdate = conn.prepareStatement(sql);
            ... 
            //totalSize = 100W數據 / 3000一批次
            for (int i = 1; i <= totalSize; i++) {
                list = 從數據庫中查詢3000條數據
                client.appendXML(list);

               pst.executeBatch();
               pstDel.executeBatch();
            }
            client.finishXML();
            
        }
         ...
        finally {
            try {
                if (isError) {
                    conn.rollback();
                }
                else
                    conn.commit();
               ...
            }
          ...
        }

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

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

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

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

 

                       <千萬級別數據生成xml文件> by dyllove98 @ http://www.cnblogs.com/dyllove98

                        轉載請表明出處。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM