问题:报表系统导出几十万大数据量会导致系统卡死,需要进行优化
解决方案: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);
}