用過POI的人都知道,在POI之前的版本中並不支持大數據量的處理,若是數據量過多還會常報OOM錯誤,這時候調整JVM的配置參數也不是一個好對策(注:jdk在32位系統中支持的內存不能超過2個G,而在64位中沒有限制,可是在64位的系統中,性能並非太好),好在POI3.8版本新出來了一個SXSSFWorkbook對象,它就是用來解決大數據量以及超大數據量的導入導出操做的,可是SXSSFWorkbook只支持.xlsx格式,不支持.xls格式的Excel文件。
這裏普及一下,在POI中使用HSSF對象時,excel 2003最多隻容許存儲65536條數據,通常用來處理較少的數據量,這時對於百萬級別數據,Excel確定容納不了,並且在計算機性能稍低的機器上測試,就很容易致使堆溢出。而當我升級到XSSF對象時,它能夠直接支持excel2007以上版本,由於它採用ooxml格式。這時excel能夠支持1048576條數據,單個sheet表就支持近104萬條數據了,雖然這時導出100萬數據能知足要求,但使用XSSF測試後發現偶爾仍是會發生堆溢出,因此也不適合百萬數據的導出。mysql
如今咱們知道excel2007及以上版本能夠輕鬆實現存儲百萬級別的數據,可是系統中的大量數據是如何可以快速準確的導入到excel中這好像是個難題,對於通常的web系統,咱們爲了解決成本,基本都是使用的入門級web服務器tomcat,既然咱們不推薦調整JVM的大小,那咱們就要針對咱們的代碼來解決咱們要解決的問題。在POI3.8以後新增長了一個類,SXSSFWorkbook,採用當數據加工時不是相似前面版本的對象,它能夠控制excel數據佔用的內存,他經過控制在內存中的行數來實現資源管理,即當建立對象超過了設定的行數,它會自動刷新內存,將數據寫入文件,這樣致使打印時,佔用的CPU,和內存不多。但有人會說了,我用過這個類啊,他好像並不能徹底解決,當數據量超過必定量後仍是會內存溢出的,並且時間還很長。對你只是用了這個類,可是你並無針對你的需求進行相應的設計,僅僅是用了,因此接下來我要說的問題就是,如何經過SXSSFWorkbook以及相應的寫入設計來實現百萬級別的數據快速寫入。web
我先舉個例子,之前咱們數據庫中存在大量的數據,咱們要查詢,怎麼辦?咱們在沒有通過設計的時候是這樣來處理的,先寫一個集合,而後執行jdbc,將返回的結果賦值給list,而後再返回到頁面上,可是當數據量大的時候,就會出現數據沒法返回,內存溢出的狀況,因而咱們在有限的時間和空間下,經過分頁將數據一頁一頁的顯示出來,這樣能夠避免了大數據量數據對內存的佔用,也提升了用戶的體驗,在咱們要導出的百萬數據也是一個道理,內存突發性佔用,咱們能夠限制導出數據所佔用的內存,這裏我先創建一個list容器,list中開闢10000行的存儲空間,每次存儲10000行,用完了將內容清空,而後重複利用,這樣就能夠有效控制內存,因此咱們的設計思路就基本造成了,因此分頁數據導出共有如下3個步驟:sql
一、求數據庫中待導出數據的行數數據庫
二、根據行數求數據提取次數tomcat
三、按次數將數據寫入文件服務器
經過以上步驟在效率和用戶體驗性上都有了很高的提升,接下來上代碼性能
public void exportBigDataExcel(ValueDataDto valueDataDto, String path) throws IOException { // 最重要的就是使用SXSSFWorkbook,表示流的方式進行操做 // 在內存中保持100行,超過100行將被刷新到磁盤 SXSSFWorkbook wb = new SXSSFWorkbook(100); Sheet sh = wb.createSheet(); // 創建新的sheet對象 Row row = sh.createRow(0); // 建立第一行對象 // -----------定義表頭----------- Cell cel0 = row.createCell(0); cel0.setCellValue("1"); Cell cel2 = row.createCell(1); cel2.setCellValue("2"); Cell cel3 = row.createCell(2); cel3.setCellValue("3"); Cell cel4 = row.createCell(3); // --------------------------- List<valuedatabean> list = new ArrayList<valuedatabean>(); // 數據庫中存儲的數據行 int page_size = 10000; // 求數據庫中待導出數據的行數 int list_count = this.daoUtils.queryListCount(this.valueDataDao .queryExportSQL(valueDataDto).get("count_sql")); // 根據行數求數據提取次數 int export_times = list_count % page_size > 0 ? list_count / page_size + 1 : list_count / page_size; // 按次數將數據寫入文件 for (int j = 0; j < export_times; j++) { list = this.valueDataDao.queryPageList(this.valueDataDao .queryExportSQL(valueDataDto).get("list_sql"), j + 1, page_size); int len = list.size() < page_size ? list.size() : page_size;
<span style="white-space:pre"> </span> for (int i = 0; i < len; i++) { Row row_value = sh.createRow(j * page_size + i + 1); Cell cel0_value = row_value.createCell(0); cel0_value.setCellValue(list.get(i).getaa()); Cell cel2_value = row_value.createCell(1); cel2_value.setCellValue(list.get(i).getaa()); Cell cel3_value = row_value.createCell(2); cel3_value.setCellValue(list.get(i).getaa_person()); } list.clear(); // 每次存儲len行,用完了將內容清空,以便內存可重複利用 } FileOutputStream fileOut = new FileOutputStream(path); wb.write(fileOut); fileOut.close(); wb.dispose(); }
到目前已經能夠實現百萬數據的導出了,可是當咱們的業務數據超過200萬,300萬了呢?如何解決?測試
這時,直接打印數據到一個工做簿的一個工做表是實現不了的,必須拆分到多個工做表,或者多個工做簿中才能實現。由於一個sheet最多行數爲1048576。大數據
下面就以這種思路提供另一種解決方案,直接上代碼(後面會附上測試數據庫,及案例須要的jar包)this
public static void main(String[] args) throws Exception { Test3SXXFS tm = new Test3SXXFS(); tm.jdbcex(true); } public void jdbcex(boolean isClose) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException, IOException, InterruptedException { String xlsFile = "f:/poiSXXFSBigData.xlsx"; //輸出文件 //內存中只建立100個對象,寫臨時文件,當超過100條,就將內存中不用的對象釋放。 Workbook wb = new SXSSFWorkbook(100); //關鍵語句 Sheet sheet = null; //工做表對象 Row nRow = null; //行對象 Cell nCell = null; //列對象 //使用jdbc連接數據庫 Class.forName("com.mysql.jdbc.Driver").newInstance(); String url = "jdbc:mysql://localhost:3306/bigdata?characterEncoding=UTF-8"; String user = "root"; String password = "123456"; //獲取數據庫鏈接 Connection conn = DriverManager.getConnection(url, user,password); Statement stmt = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,ResultSet.CONCUR_UPDATABLE); String sql = "select * from hpa_normal_tissue limit 1000000"; //100萬測試數據 ResultSet rs = stmt.executeQuery(sql); ResultSetMetaData rsmd = rs.getMetaData(); long startTime = System.currentTimeMillis(); //開始時間 System.out.println("strat execute time: " + startTime); int rowNo = 0; //總行號 int pageRowNo = 0; //頁行號 while(rs.next()) { //打印300000條後切換到下個工做表,可根據須要自行拓展,2百萬,3百萬...數據同樣操做,只要不超過1048576就能夠 if(rowNo%300000==0){ System.out.println("Current Sheet:" + rowNo/300000); sheet = wb.createSheet("個人第"+(rowNo/300000)+"個工做簿");//創建新的sheet對象 sheet = wb.getSheetAt(rowNo/300000); //動態指定當前的工做表 pageRowNo = 0; //每當新建了工做表就將當前工做表的行號重置爲0 } rowNo++; nRow = sheet.createRow(pageRowNo++); //新建行對象 // 打印每行,每行有6列數據 rsmd.getColumnCount()==6 --- 列屬性的個數 for(int j=0;j<rsmd.getColumnCount();j++){ nCell = nRow.createCell(j); nCell.setCellValue(rs.getString(j+1)); } if(rowNo%10000==0){ System.out.println("row no: " + rowNo); } // Thread.sleep(1); //休息一下,防止對CPU佔用,其實影響不大 } long finishedTime = System.currentTimeMillis(); //處理完成時間 System.out.println("finished execute time: " + (finishedTime - startTime)/1000 + "m"); FileOutputStream fOut = new FileOutputStream(xlsFile); wb.write(fOut); fOut.flush(); //刷新緩衝區 fOut.close(); long stopTime = System.currentTimeMillis(); //寫文件時間 System.out.println("write xlsx file time: " + (stopTime - startTime)/1000 + "m"); if(isClose){ this.close(rs, stmt, conn); } } //執行關閉流的操做 private void close(ResultSet rs, Statement stmt, Connection conn ) throws SQLException{ rs.close(); stmt.close(); conn.close(); }
數據庫截圖:
案例執行結果截圖: