問題:報表系統導出幾十萬大數據量會導致系統卡死,需要進行優化
解決方案:1、異步處理 2、分批處理 3、分文件處理(暫時沒做)
一、異步處理
在springboot項目中,實現異步處理特別簡單,加兩個註解(@EnableAsync、@Async)就完事兒了,在傳統的web項目中,實現異步處理有點點複雜。
1、配置文件修改
xmlns:task="http://www.springframework.org/schema/task"
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd
2、添加註解
TIPS:調用方法和異步方法不能在同一個類。
二、分批處理
1、技術選型
開始我考慮導出CSV(Comma-Separated Values)文件,如果不要求格式、樣式、公式等等,會比POI快很多,現在項目使用的是POI的XSSF,後面又瞭解到POI的HSSF和SXSSF。此時,我在想這三者有什麼關係和區別?
HSSF:Excel97-2003版本,擴展名爲.xls。一個sheet最大行數65536,最大列數256。
XSSF:Excel2007版本開始,擴展名爲.xlsx。一個sheet最大行數1048576,最大列數16384。
SXSSF:是在XSSF基礎上,POI3.8版本開始提供的支持低內存佔用的操作方式,擴展名爲.xlsx。
Excel版本兼容性是向下兼容。
重點講一下SXSSF,因爲是今天要使用的技術:
SXSSF擴展自XSSF,用於當非常大的工作表要導出且內存受限制的時候。SXSSF佔用很少的內存是因爲它限制只能訪問滑動窗口的數據,而XSSF可以訪問文檔中所有的數據。那些不在滑動窗口中的數據是不能訪問的,因爲它們已經被寫到磁盤上了。
我們可以通過SXSSFWorkbook workbook = new SXSSFWorkbook(int windowSize);設置滑動窗口大小,默認是100。如果設置爲-1,則表示不限,沒有記錄被自動刷新到磁盤,除非你手動調用flushRow()刷新。當通過sheet.createRow();創建新行時,總的行數可能會超過窗口大小,這個時候行號最低的那行會被刷新到磁盤而且不能通過getRow()訪問。
我們也可以通過sheet.setRandomAccessWindowSize(int windowSize);設置每個工作表的窗口大小。
注意事項:SXSSF會產生臨時文件,必須明確清理,調用workbook.dispose();方法。
2、實現SXSSF
package com.km.util;
import com.alibaba.fastjson.JSONObject;
import com.km.entity.DataExport;
import com.km.entity.HospDrugSales;
import com.km.entity.RequestData;
import com.km.service.HandleExcelDataService;
import org.apache.log4j.Logger;
import org.apache.poi.hssf.util.HSSFColor;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.streaming.SXSSFCell;
import org.apache.poi.xssf.streaming.SXSSFRow;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFFont;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
public class ExportExcelUtils {
protected static Logger logger = Logger.getLogger(ExportExcelUtils.class);
private static ThreadLocal<Long> startTime = new ThreadLocal<>();
public static void exportExcel(Map<String, Object> data, List<?> list, RequestData requestData){
logger.info("導出數據開始,導出數量:" + list.size());
startTime.set(System.currentTimeMillis());
OutputStream out = null;
SXSSFWorkbook workbook = null;
try {
ArrayList<Map<String,String>> arrayList = new ArrayList<>();
String title = data.get("title").toString();
String remark = data.get("remark").toString();
if(list != null){
for(int i = 0; i< list.size(); i++){
arrayList.add(BeanToMapUtil.convertBean(list.get(i)));
}
}
workbook = ExportExcelUtils.exportExcel(title,remark, arrayList,data.get("heads").toString().split(","),data.get("fields").toString().split(","));
//設置成null是爲了讓JVM優先選擇回收
arrayList = null;
String path = requestData.getPath();
path = path.substring(0, path.indexOf("km_edw")).replaceAll("\\\\", "/");
String time = DateUtils.getSimpleDateTime().replaceAll(" ", "");
StringBuilder filePath = new StringBuilder(path).append("excel").append("/").append(title).append(time).append(".xlsx");
out = new FileOutputStream(filePath.toString());
out.flush();
workbook.write(out);
long usedTime = (System.currentTimeMillis() - startTime.get())/1000;
logger.info("導出數據結束, 導出時間:" + usedTime + "s");
DataExport dataExport = requestData.getDataExport();
dataExport.setExportAmount(String.valueOf(list.size()));
dataExport.setExportUsedTime(String.valueOf(usedTime));
dataExport.setExportAddress(filePath.toString());
} catch (IOException e) {
logger.error("導出報表異常,異常信息:" + e.getMessage());
}finally{
if(out!=null){
try {
out.close();
} catch (IOException e) {
logger.error(e.getMessage());
}
}
if (null != workbook){
//處理工作表在磁盤上產生的臨時文件
workbook.dispose();
}
}
}
/**
* 使用SXSSFWorkbook導出excel
* @param title
* @param remark
* @param arrayList
* @param headers
* @param fileds
* @return
*/
public static SXSSFWorkbook exportExcel(String title, String remark, ArrayList<Map<String, String>> arrayList, String[] headers, String[] fileds){
SXSSFWorkbook workbook = new SXSSFWorkbook(1000);
SXSSFSheet sheet = (SXSSFSheet) workbook.createSheet();
workbook.setSheetName(0, title);
XSSFCellStyle style = ExportExcelUtils.setSXSSFCellStyleAttribute(workbook);
//添加第一行表頭和備註
SXSSFRow titleRow = (SXSSFRow) sheet.createRow((short) 0);
SXSSFCell titleCell = (SXSSFCell) titleRow.createCell((short) 0);
titleCell.setCellStyle(style);
if(remark==null){
remark="";
}
titleCell.setCellValue(title+remark);
sheet.addMergedRegion(new CellRangeAddress(0,0,0,headers.length-1));
// 創建第二行標題
SXSSFRow headRow = (SXSSFRow) sheet.createRow((short) 1);
for (int i = 0; i < headers.length; i++) {
SXSSFCell cell = (SXSSFCell) headRow.createCell(i);
cell.setCellStyle(style);
XSSFRichTextString text = new XSSFRichTextString(headers[i]);
cell.setCellValue(text);
}
SXSSFRow row;
for (int i = 0; i < arrayList.size(); i++) {
Map<String, String> map = arrayList.get(i);
row = (SXSSFRow) sheet.createRow((i + 2));
int j = 0;
for (int m = 0; m < fileds.length; m++) {
Object o = (map.get(fileds[m])) == null ? "" : (map.get(fileds[m]));
if (o instanceof BigDecimal) {
row.createCell(j++).setCellValue((o).toString());
} else{
row.createCell(j++).setCellValue(o.toString());
}
}
}
return workbook;
}
private static XSSFCellStyle setSXSSFCellStyleAttribute(SXSSFWorkbook workbook){
//生成一個樣式,用來設置標題樣式
XSSFCellStyle style = (XSSFCellStyle) workbook.createCellStyle();
//設置這些樣式
style.setFillForegroundColor(HSSFColor.SKY_BLUE.index);
style.setFillPattern(XSSFCellStyle.SOLID_FOREGROUND);
style.setBorderBottom(XSSFCellStyle.BORDER_THIN);
style.setBorderLeft(XSSFCellStyle.BORDER_THIN);
style.setBorderRight(XSSFCellStyle.BORDER_THIN);
style.setBorderTop(XSSFCellStyle.BORDER_THIN);
style.setAlignment(XSSFCellStyle.ALIGN_CENTER);
//生成一個字體
XSSFFont font = (XSSFFont) workbook.createFont();
font.setColor(HSSFColor.VIOLET.index);
font.setBoldweight(XSSFFont.BOLDWEIGHT_BOLD);
//把字體應用到當前的樣式
style.setFont(font);
return style;
}
}
3、導出記錄表
DROP TABLE IF EXISTS `data_export`;
CREATE TABLE `data_export` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`user_id` varchar(10) DEFAULT NULL COMMENT '用戶Id',
`user_name` varchar(10) DEFAULT NULL COMMENT '用戶名稱',
`export_module` varchar(20) DEFAULT NULL COMMENT '數據模塊',
`export_param` varchar(500) DEFAULT NULL COMMENT '數據參數',
`export_address` varchar(100) DEFAULT NULL COMMENT '數據地址',
`status` tinyint(3) DEFAULT '0' COMMENT '導出狀態 0:未導出 1:已導出 2:導出錯誤',
`export_amount` varchar(10) DEFAULT '0' COMMENT '導出數量',
`export_used_time` varchar(20) DEFAULT NULL COMMENT '導出耗時,單位:秒',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
`ext_fields` VARCHAR(500) COMMENT '擴展字段',
PRIMARY KEY (`id`),
KEY `user_id`(`user_id`),
KEY `status`(`status`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='報表數據記錄表';
4、下載頁面
三、遇到問題
1、java獲取項目的幾種方法
地址1:/project_name
地址2:/home/apache-tomcat-7.0.77/project_name
地址3:/home/apache-tomcat-7.0.77/webapps/project_name/
我將文件放在了webapps的excel目錄:
2、文件下載
2.1、HTML
<a class=" btn black btn-purple" href="javascript:download('${data.id}','${data.exportAddress }');">下載</a>
2.2、JS組裝Form表單
function download(id, path){
var form = $("<form></form>");
form.attr("action", "download.html");
form.attr("method", "post");
var idInput = $("<input type='text' name='id'/>");
var pathInput = $("<input type='text' name='path' />");
idInput.attr("value", id);
pathInput.attr("value", path);
form.append(pathInput);
form.append(idInput);
form.appendTo("body");
form.hide();
form.submit();
}
2.3、後臺代碼
/**
* 下載
* @param id
* @param path
* @param response
*/
@RequestMapping("/download")
public void download(String id, String path, HttpServletResponse response){
DataExport dataExport = new DataExport();
File file = new File(path);
dataExport.setStatus(Byte.valueOf("1"));
if (file.exists()){
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
OutputStream outputStream = new BufferedOutputStream(response.getOutputStream())){
String fileName = file.getName();
byte[] buffer = new byte[inputStream.available()];
inputStream.read(buffer);
response.reset();
response.addHeader("Content-Disposition", "attachment; filename=" + new String(fileName.getBytes("utf-8"), "iso-8859-1"));
response.addHeader("Content-Length", String.valueOf(file.length()));
response.setContentType("application/octet-stream");
outputStream.write(buffer);
} catch (FileNotFoundException e) {
logger.error("FileNotFoundException, 文件路徑:" + path + ", exception message: " + e.getMessage());
dataExport.setStatus(Byte.valueOf("2"));
} catch (IOException e) {
logger.error("IOException, 文件路徑:" + path + ", exception message: " + e.getMessage());
dataExport.setStatus(Byte.valueOf("2"));
}
}
dataExport.setId(Long.valueOf(id));
reportDownloadService.updateDownloadStatus(dataExport);
}