在最近的一个项目上,遇到一个很奇葩的问题,应用无法访问或响应速度非常的慢,查看服务器资源发现CPU占用将近99%,经过多次分析,发现原来是进行Excel大数据导出导致的,问题找到后,立马寻找解决方案,当前的几个解决方案详细如下
一、将导出Excel改成CSV格式
分析:Excel的格式比CSV格式要复杂的多,CSV是纯文本的字符串,没有复杂的计算逻辑,而Excel中有各种的汇总计算,合并单元格,冻结行,冻结列等等,因此生成Excel要比生成CSV要慢的多,但是经过与客户的再三商讨后,客户不同意导出CSV格式,依然要导出Escel,因此这个解决方案被否决了。
二、将一个Excel拆分成多份导出
分析:因为导出小文件的时候,不会存在资源耗尽的问题,因此想到将一个大Excel拆分成多个导出,同时按拆分Excel的个数启动多线程,每一个线程导出一份Excel,这样可以避免导出一个大文件的问题,但是客户觉得这样导出完成后还要进行多个Excel数据的汇总,操作上很不方便,因此也不同意这个解决方案
三、将报表导出与主应用分离
分析:考虑到即使导出失败也不会影响到主应用的使用,且报表个数很多,因此有必要将报表单独出一个应用,独立部署,和主应用分离,这样也方便后续针对报表做对应的横向扩展,从架构方面考虑,这个方案用户接收
单单将报表与主应用分离,还远远不够,依然会出现耗尽资源的问题,因此还需要继续向下分析
四、修改Tomcat内存参数
分析:我们知道,Tomcat的bin目录下,是有很多运行相关的文件,其中就包括运行参数的设置,有一个文件catalina.sh(window:catalina.bat),可以在这个文件中配置tomcat的运行内存,最大内存等,在文件的最开始,加上如下的配置
export JAVA_OPTS="-Xms2048m -Xmx2048m -XX:PermSize=256m -XX:MaxPermSize=1024m"
配置项说明:
-Xms:初始堆大小,一般为物理机内存的1/4
-Xmx:最大堆大小,初始化大小一般与-Xms相同
PermSize:JVM启动时初始化Perm的内存大小
MaxPermSize:最大可占用的Perm内存大小,在生产环境上一般将这两个值设为相同
详细的内存参数配置,可参考这篇文章https://wyj-study.iteye.com/blog/2254845
五、程序优化
5.1 API优化
这部分的优化,是从另一位前辈那里得来的,主要是修改创建Workbook,Sheet页等相关API,几个非常重要的API优化前后对比,在优化前
XSSFWorkbook wb = new XSSFWorkbook(); //创建工作溥
XSSFSheet sheet = wb.createSheet(element.attributeValue(("name")));//创建Sheet页
XSSFRow headRow = sheet.createRow(0); //创建行
优化后
Workbook wb = new SXSSFWorkbook(500);//创建工作溥
Sheet sheet = wb.createSheet(element.attributeValue("sheetName"));///创建Sheet页
Row headRow = sheet.createRow(0);//创建行
5.2 变量声明优化
优化前,设置Excel单元格内容的代码是这样的
@SuppressWarnings("unchecked")
private static <T> void createContent4List(List<T> list, Sheet sheet, CellStyle cellStyle, Element element)
throws Exception {
String clazzString = element.attributeValue("class");
// 反射创建对象实例
Class<? extends Object> clazz = Class.forName(clazzString);
List<Element> columns = element.elements();
int lastRowNum = sheet.getLastRowNum();
String dataType = null;
// 根据list确认行数
for (int i = 0; i < list.size(); i++) {
Row contentRow= sheet.createRow(i + lastRowNum + 1);
for (int j = 0; j < columns.size(); j++) {
Method method = clazz.getMethod("get" + columns.get(j).attributeValue("name"));
Object obj= method.invoke(list.get(i));
String cellValue = initExcelValue(obj);
Cell contentCell = contentRow.createCell(j);
contentCell.setCellValue(cellValue);
contentCell.setCellStyle(cellStyle);
//数值过长,转换为double会变为科学计数法导出,所以不做处理
dataType = columns.get(j).attributeValue("type");
if (isDouble(contentCell.toString())&& !"String".equals(dataType)) {
contentCell.setCellValue(Double.parseDouble(contentCell.toString()));
}
}
}
}
从代码中可以看出,在for循环中重复创建了大量的变量,在此方法没有执行完成之前,这些变量一直存在于内存中,这样就会占用大量不需要的内存空间,很有可能会出现耗尽耗尽的情况,因此优化点就是将变量的声明放在for循环外面,这样就不用重复创建变量,经过优化后的代码如下
@SuppressWarnings("unchecked")
private static <T> void createContent4List(List<T> list, Sheet sheet, CellStyle cellStyle, Element element)
throws Exception {
String clazzString = element.attributeValue("class");
// 反射创建对象实例
Class<? extends Object> clazz = Class.forName(clazzString);
List<Element> columns = element.elements();
Row contentRow = null;
Cell contentCell = null;
Method method = null;
String cellValue = null;
Object obj = null;
int lastRowNum = sheet.getLastRowNum();
String dataType = null;
// 根据list确认行数
for (int i = 0; i < list.size(); i++) {
contentRow = sheet.createRow(i + lastRowNum + 1);
for (int j = 0; j < columns.size(); j++) {
method = clazz.getMethod("get" + columns.get(j).attributeValue("name"));
obj = method.invoke(list.get(i));
cellValue = initExcelValue(obj);
contentCell = contentRow.createCell(j);
contentCell.setCellValue(cellValue);
contentCell.setCellStyle(cellStyle);
//数值过长,转换为double会变为科学计数法导出,所以不做处理
dataType = columns.get(j).attributeValue("type");
if (isDouble(contentCell.toString())&& !"String".equals(dataType)) {
contentCell.setCellValue(Double.parseDouble(contentCell.toString()));
}
}
}
}
到此,综合以上几个优化点,Excel导出大数据CPU资源过高的问题就解决了,如果您有更好或不同的解决方案,欢迎留言交流,谢谢