百萬級數據量導出EXCEL解決方案分析


百萬級數據量導出EXCEL解決方案分析

1. 問題概述
在web頁面上顯示的報表導出到excel文件里是一種很常見的需求, 報表的類excel模型,支持excel文件數據無失真的導入導出, 然而,當數據量較大的情況下,就會遇到一些問題:
1. 2003Excel本身的支持最多65535行數據
2. 在這種大數據量的報表生成和導出中,要占用大量的內存,甚至內存溢出
難點:1.數據量大,報表在運算成ireport對象時每個sheet頁內存不釋放,可能內存溢出.
2.即使能夠運算完ireport對象再導出Excel的過程中內存不釋放,可能內存溢出.

2 . 案例

3. 解決方案
一. 對於數據超過了65535行的問題,很自然的就會想到將整個數據分塊,利用excel的多sheet頁的功能,將超出65535行后的數據寫入到下一個sheet頁中,即通過多sheet頁的
方式,突破了最高65535行數據的限定, 具體做法就是,單獨做一個鏈接,使用JSP導出,在JSP上通過程序判斷報表行數,超過65535行后分SHEET寫入。這樣這個問題就得以解決了
二. 在這種大數據量的報表生成和導出中,要占用大量的內存,尤其是在使用TOMCAT的情況下,JVM最高只能支持到2G內存,則會發生內存溢出的情況。此時的內存開銷
主要是兩部分,一部分是該報表生成時的開銷,另一部分是該報表生成后寫入一個EXCEL時的開銷。由於JVM的GC機制是不能強制回收的,因此,對於此種情形,我們要改變一種方式:
1. 將該報表設置起始行和結束行參數,在API生成報表的過程中,分步計算報表,比如一張20萬行數據的報表,在生成過程中,可通過起始行和結束行分4-5次進行。這樣,就
降低了報表生成時的內存占用,在后面報表生成的過程中,如果發現內存不夠,即可自動啟動JVM的GC機制,回收前面報表的緩存
2. 導出EXCEL的過程,放在每段生成報表之后立即進行,改多個SHEET頁為多個EXCEL,即在分步生成報表的同時分步生成EXCEL,則通過 POI包生成EXCEL的內存消耗也得以降低。通過
多次生成,同樣可以在后面EXCEL生成所需要的內存不足時,有效回收前面生成EXCEL時占用的內存
3. 再使用文件操作,對每個客戶端的導出請求在服務器端根據SESSIONID和登陸時間生成唯一的臨時目錄,用來放置所生成的多個EXCEL,然后調用系統控制台,打包多個EXCEL為RAR
或者JAR方式,最終反饋給用戶一個RAR包或者JAR包,響應客戶請求后,再次調用控制台刪除該臨時目錄。
4. 通過分段運算和生成,有效降低了報表從生成結果到生成EXCEL的內存開銷。其次是通過使用壓縮包,響應給用戶的生成文件體積大大縮小,降低了多用戶並
發訪問時服務器下載文件的負擔,有效減少多個用戶導出下載時服務器端的流量,從而達到進一步減輕服務器負載的效果

4. 程序部分
1.JSP里導出的鏈接格式:<a href=“<%=request.getContextPath()%>/exportToExcel.jsp?report=分頁標簽.raq&path=reportFiles\&param=arg1=123,234;arg2=2″>導出Excel</a>
也可用javascript的方式
2.導出EXCEL中對於取最大行數,可以在報表里多定義一個數據集專門取最大行數,在后台通過API方式解析,保證通用性避免代碼中出現SQL,復雜SQL,存儲過程等,如圖:

jsp具體代碼:
<%@ page contentType=”text/html;charset=GBK“%>
<%@ page import=”com.runqian.report4.model.*”%>
<%@ page import=”com.runqian.report4.usermodel.*“%>
<%@ page import=”com.runqian.report4.view.excel.*”%>
<%@ page import=”com.runqian.report4.util.*”%>
<%@ page import=”java.util.ArrayList“%>
<%@ page import=”java.io.*”%>
<%@ page import=”java.sql.*“/>
<html>
<head>
<title>導出EXCEL</title>
</head>
<body>
<%
request.setCharacterEncoding(“GBK“);
String root = getServletContext().getRealPath(“/”);
String path = request.getParameter(“path“); //相對路徑
String report = request.getParameter(“report“); //報表名
String param = request.getParameter(“param“); //報表參數
String filePath = root + path + report;
ReportDefine rd2 = (ReportDefine) ReportUtils.read(filePath);//讀取報表
Context context = new Context();
// 獲得報表定義中的數據集集合
DataSetMetaData dsmd = rd2.getDataSetMetaData();
// 取到需要修改的數據集對象
//SQL檢索類型:SQLDataSetConfig
//復雜SQL類型:CSQLDataSetConfig
//存儲過程類型:ProcDataSetConfig
SQLDataSetConfig dsc = (SQLDataSetConfig) dsmd.getDataSetConfig(0);
String sql = dsc.getSQL();
//連接數據庫,查詢查詢有多少條記錄
Connection conn = context.getConnectionFactory(“zHouHuiHui“)
.getConnection();
ResultSet rs = null;
Statement stmt = conn.createStatement();
rs = stmt.executeQuery(sql);
int num = 0;
try {
if (rs.next()) {
num = rs.getInt(1);
System.out.println(“條數:” + num);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (rs != null)
rs.close();
if (stmt != null)
stmt.close();
}
int rowCount = 65536;//每頁最大行數
int pageCount = 0;
if (num % rowCount == 0) {
pageCount = num / rowCount;
} else {
pageCount = num / rowCount + 1;
}
System.out.println(“總共多少頁 :” + pageCount);
try {
for (int k = 0; k < pageCount; k++) {
//讀取報表模板
context = new Context();
ReportDefine rd = (ReportDefine) ReportUtils.read(filePath);
//運算報表
//創建要導出的excel報表
ExcelReport excel = new ExcelReport();
//計算前,設置參數
ParamMetaData pmd = rd.getParamMetaData();
if (param != null && param.trim().length() != 0
&& pmd != null) {
String[] para = param.split(“;”);
String[] pa = null;
String paramName = “”; //參數名
String paramValue = “null”; //參數值
for (int i = 0; i < para.length; i++) {
out.println(para[i]);
pa = para[i].split(“=”);
paramName = pa[0];
if (pa.length == 1) {
paramValue = “”;
context.setParamValue(paramName, paramValue);
} else {
if (pa[1].indexOf(“,”) == -1) {
paramValue = pa[1];
context
.setParamValue(paramName,
paramValue);
} else {
ArrayList objArrayList = new ArrayList();
String[] b = pa[1].split(“,”);
for (int j = 0; j < b.length; j++) {
objArrayList.add(b[j]);
}
context.setParamValue(paramName,
objArrayList);
}
}
}
}
context.setParamValue(“startRow“, 30000 * k);
context.setParamValue(“endRow“, 30000 * (k + 1));
System.out.println(context.getParamValue(“startRow”));
System.out.println(context.getParamValue(“endRow”));
Engine enging = new Engine(rd, context);
IReport iReport = enging.calc();
System.out.println(“[ToExcelServlet] – 運算報表結束!”);
PrintSetup ps = iReport.getPrintSetup(); //取打印配置
ps.setPagerStyle(PrintSetup.PAGER_ROW); //設置報表分頁方式為按數據行數分頁
ps.setPaper((short) 0); //設置紙張類型
ps.setPaperWidth(1000);
ps.setRowNumPerPage(65535); //設置按數據行數分頁時每頁的數據行數
iReport.setPrintSetup(ps); //設置打印配置*/
PageBuilder pb = new PageBuilder(iReport); //根據iReport中的PrintSetup里的信息進行分頁
IReport[] ireports = pb.getAllPages(); //獲得分頁后的所有頁集合
for (int i = 0; i < ireports.length; i++) {
excel.export(ireports[i]);
}
excel.saveTo(“d://test//test” + k + “.xls”);
}
//將1個文件壓縮成RAR格式
ExcelOutPut objRARFile = new ExcelOutPut();
int arg = objRARFile.RARFile(“d://test”, “d://test”, “”);
OutputStream objOutputStream = response.getOutputStream();
response.setContentType(“application/octet-stream“);
String httpHeader = “attachment;filename=report1.rar“;
response.addHeader(“Content-Disposition”, httpHeader);
FileInputStream infile = null;// 讀取文件用
//生成對象用infile,准備讀取文件
infile = new FileInputStream(“D://test.rar“);
byte[] buffer = new byte[500];
int count = 0;
count = infile.read(buffer);
while (count > 0) {
objOutputStream.write(buffer, 0, count);
count = infile.read(buffer);
}
//ServletOutputStream sos = response.getOutputStream();
objOutputStream.close();
infile.close();
//以下兩句必須要加,否則會報響應的輸出流已經被占用
out.clear();
out = pageContext.pushBody();
} catch (Exception e) {
e.printStackTrace();
} catch (Throwable e) {
e.printStackTrace();
}
%>
</body>
</html>
3.將文件壓縮成RAR格式,ExcelOutPut.java,具體代碼:
package com.runqian.report4.usermodel;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class ExcelOutPut {
/*
*
* cmd 下調試成功的命令
*
* 壓縮:Rar a c:\test.rar c:\3Out_20050107.MDB
*
* 解壓:UnRar x * c:\test.rar d:\
*
*/
private static String rarCmd = “C://Program Files//WinRAR//Rar.exe a “;
private static String unrarCmd = “C://Program Files//WinRAR//UnRar x “;

private static int count = 0;
/**
*
* 將1個文件壓縮成RAR格式
*
* rarName 壓縮后的壓縮文件名(不包含后綴)
*
* fileName 需要壓縮的文件名(必須包含路徑)
*
* destDir 壓縮后的壓縮文件存放路徑
* return 0 表示壓縮完成
*
* 1 表示由於一些原因失敗
*
*
* @throws IOException
*
* @throws InterruptedException
*
*/ 
public static int RARFile(String rarName, String fileName, String destDir) {
rarCmd += destDir + rarName + “.rar ” + fileName;
String readStr = “”;
try {
System.out.println(“正在壓縮文件…” + rarName + “.rar “);
String s;
Process p = Runtime.getRuntime().exec(rarCmd);
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(p.getInputStream()));
while ((s = bufferedReader.readLine()) != null)
System.out.println(s);
try {
p.waitFor();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(rarCmd);
System.out.println(“finished rar the file: ” + fileName);
} catch (IOException e) {
e.printStackTrace();
return 1;
}
return 0;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
// ExcelOutPut objRARFile = new ExcelOutPut();
// objRARFile.RARFile(“d://test”, “d://test”, “”);
int count = 199999; // 記錄數
count = count / 30000 + 1;
System.out.println(count);
}
}
5. 總結
1. 該問題本身並不是潤乾報表運算或者生成時有內存溢出BUG,而是WEB容器本身的問題和JVM本身的機制問題
2. 經過外部API改造,可以使用方法對問題進行規避
3. 該案例具有一定的通用性,完全可以做成一個獨立的大報表導出的功能,將大數據量的報表分段運算成多個EXCEL,PDF或者WORD,通過外部程序進行壓縮處理,響應給用戶壓縮包
4. 該需求本身合理性值得斟酌,但存在就是合理,也需要客戶的配合,讓步最終導致了該問題的解決


免責聲明!

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



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