本文将模拟一个生产场景的性能分析:从 Mysql 数据库中返回百万行数据并且导出数据到 Excel 文件,期间使用 JVisualVM 监控工具查看代码性能。
插件市场搜索关键字,
VisualVM Launcher
如果 IDEA 因为网络问题无法连接,可以直接去官网下载,
https://plugins.jetbrains.com/idea
下载完 jar 包直接拖拽到 IDEA,
插件安装完成后就会在右上角多出两个按钮,接下来配置 JVisualVM 参数,
配置完成后,点击右上角的运行按钮即可启动项目,同时会启动 JVisualVM ,
安装 GC 插件,
如果遇到网络不通,可以把下载地址复制到下载软件加速下载,
重新启动,可以看到新增了 Visual GC 栏位,
官方文档中介绍了 Select 方法的其他高级版本,其中,可以重写 ResultHandler 对大数据集进行自定义操作,
Mybatis 官方有一个 ResultHandler 的默认实现,将结果塞到 list 中返回,
接下来我们重新实现一个 ResultHandler,将结果集导到 Excel 文件中。
package com.example.demo.vo;
import lombok.Data;
/**
* 测试 Vo
*
* @author yushanma
* @since 2023/7/11 9:30
*/
@Data
public class TestVo {
private String f1;
private String f2;
private String f3;
private String f4;
private String f5;
private String f6;
private String f7;
private String f8;
private String f9;
private String f10;
private String f11;
private String f12;
private String f13;
private String f14;
private String f15;
private String f16;
private String f17;
private String f18;
private String f19;
private String f20;
}
/**
* 返回百万数据
* @param resultHandler 结果处理器
*/
void selectMillionData(ResultHandler resultHandler);
简单起见,这里使用 jackson 序列化代替文件写入,后续再实现 Excel 写入,
package com.example.demo.handler;
import com.example.demo.utils.JacksonUtil;
import com.example.demo.vo.TestVo;
import org.apache.ibatis.session.ResultContext;
import org.apache.ibatis.session.ResultHandler;
/**
* 结果集处理器
*
* @author yushanma
* @since 2023/7/11 9:36
*/
public class ExportResultHandler implements ResultHandler {
@Override
public void handleResult(ResultContext extends TestVo> resultContext) {
System.out.printf("result count %d", resultContext.getResultCount());
TestVo testVo = resultContext.getResultObject();
try {
System.out.println(JacksonUtil.toJsonString(testVo));
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void testResultHandler() {
testDao.selectMillionData(new ExportResultHandler());
}
请求时间 1m35.46 s,而且从输出字符来看,是一行一行处理结果集,接下来观察 GC 情况, 并没有 OOM 问题,
POM 引用 Easy Excel,
com.alibaba
easyexcel
3.2.1
定义 BaseExportResultHandler,增量把数据写入 Excel 文件,
package com.example.demo.handler;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.util.ListUtils;
import com.alibaba.excel.util.MapUtils;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy;
import com.example.demo.utils.JacksonUtil;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.ibatis.session.ResultContext;
import org.apache.ibatis.session.ResultHandler;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.poi.ss.usermodel.Cell;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* 通用导出数据结果集处理器
*
* @author yushanma
* @since 2023/7/11 11:37
*/
public abstract class BaseExportResultHandler implements ResultHandler {
/**
* Excel 表头
*/
private String[] header;
/**
* 模板文件名
*/
private String templateName;
/**
* 日志
*/
private final Logger logger = LogManager.getLogger(BaseExportResultHandler.class.getName());
/**
* 模板文件路径
*/
private final String TEMPLATE_FILE_PATH = "src/main/resources/templates/";
/**
* 文件类型
*/
private final String FILE_TYPE = ".xlsx";
/**
* 每隔 100 条数据写入 Excel ,然后清理 list ,方便内存回收
*/
private final int BATCH_COUNT = 100;
/**
* 缓存 list
*/
private List cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
/**
* excelWriter
*/
private ExcelWriter excelWriter;
/**
* writeSheet
*/
private WriteSheet writeSheet;
/**
* httpServletResponse
*/
private HttpServletResponse response;
/**
* zip io 输出流
*/
private ZipOutputStream zos = null;
/**
* io 输出流
*/
private OutputStream os = null;
/**
* 是否导出 zip
*/
private boolean isExportZip;
/**
* 重写自适应列宽策略
*/
static class CustomLongestMatchColumnWidthStyleStrategy extends AbstractColumnWidthStyleStrategy {
private static final int MAX_COLUMN_WIDTH = 255;
private final Map> cache = MapUtils.newHashMapWithExpectedSize(8);
/**
* 无参构造
*/
public CustomLongestMatchColumnWidthStyleStrategy() {
}
/**
* 设定列宽
*
* @param writeSheetHolder
* @param cellDataList
* @param cell
* @param head
* @param relativeRowIndex
* @param isHead
*/
@Override
protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList);
if (needSetWidth) {
HashMap maxColumnWidthMap = (HashMap) this.cache.computeIfAbsent(writeSheetHolder.getSheetNo(), (key) -> {
return new HashMap(16);
});
Integer columnWidth = this.dataLength(cellDataList, cell, isHead);
if (columnWidth >= 0) {
if (columnWidth > MAX_COLUMN_WIDTH) {
columnWidth = 255;
}
Integer maxColumnWidth = (Integer) maxColumnWidthMap.get(cell.getColumnIndex());
if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
writeSheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), columnWidth * 256);
}
}
}
}
private Integer dataLength(List> cellDataList, Cell cell, Boolean isHead) {
if (isHead) {
return cell.getStringCellValue().getBytes().length;
} else {
WriteCellData> cellData = (WriteCellData) cellDataList.get(0);
CellDataTypeEnum type = cellData.getType();
if (type == null) {
return -1;
} else {
switch (type) {
case STRING:
return cellData.getStringValue().getBytes().length;
case BOOLEAN:
return cellData.getBooleanValue().toString().getBytes().length;
case NUMBER:
return cellData.getNumberValue().toString().getBytes().length;
default:
return -1;
}
}
}
}
}
/**
* 解析 header 返回
*
* @return header 列表
*/
private List> getXlsTemplateHeader(String[] header) {
List> list = new ArrayList>();
for (String h : header) {
List head = new ArrayList();
head.add(h);
list.add(head);
}
return list;
}
/**
* 模板内容
* 1、模板注意用 {} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用 "\{","\}" 代替
* 2、填充 list 的时候还要注意 模板中 {.} 多了个点 表示 list
* 3、如果填充 list 的对象是 map , 必须包涵所有 list 的 key , 哪怕数据为 null,必须使用 map.put(key,null)
*
* @param header header
* @return List>
*/
private List> getXlsTemplateContent(String[] header) {
List> list = new ArrayList<>();
List
封装 MyExcelUtil 用于获取实体类对应的 Excel header,否则可以直接用 HashMap 替代 TestVo 作为实体类,
package com.example.demo.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import java.util.HashMap;
/**
* MyExcelUtil
*
* @author yushanma
* @since 2023/7/15 17:10
*/
public class MyExcelUtil {
/**
* 获取 header
*
* @param obj 通用对象
* @return 对象属性数组
*/
public static String[] getHeader(Object obj) throws JsonProcessingException {
String jsonString = JacksonUtil.toJsonString(obj);
HashMap objectMap = (HashMap) JacksonUtil.toObject(jsonString, HashMap.class);
return objectMap.keySet().toArray(new String[objectMap.keySet().size()]);
}
}
调用 BaseExportResultHandler,
@Override
public void testResultHandler() throws IOException {
// old school
// testDao.selectMillionData(new ExportResultHandler());
// new school
// 每次导出 new 一个 BaseExportResultHandler 对象,参数为 header、 templateName、 isExportZip
BaseExportResultHandler handler = new BaseExportResultHandler(
MyExcelUtil.getHeader(new TestVo()), "TestVo", false
) {
/**
* 子类重写 fetchDataByStream ,自定义获取数据的方式
*/
@Override
public void fetchDataByStream() {
// 这里的this 指的就是 BaseExportResultHandler handler 这个对象,在这里写 mapper 调用获取数据的调用
testDao.selectMillionData(this);
}
};
// startExportExcel 方法中调用 fetchDataByStream 方法,
// 而 fetchDataByStream 方法中 selectMillionData 方法会调用 handler 中的 handleResult 方法
// 最终 handleResult 方法调用 invoke 处理数据
handler.startExportExcel();
}
@GetMapping("/testResultHandler")
public void testResultHandler() throws IOException {
// old school
// testService.testResultHandler();
// new school
testService.testResultHandler();
}
报错:Error: Maximum response size reached
解决:修改响应数据报文大小,原 50 MB,现修改为 1024 MB
如果 Postman 还是崩溃闪退,就直接用浏览器,
最终下载的文件大小为 505 MB,
应用刚启动时,
导出 Excel 时,
导出完成,
总耗时,cost PT4M20.169S,Excel 文件大小 505 MB,内容总字符 1000000 * 36 * 20 = 7.2 亿,并没有产生 OOM 问题,平均每秒处理 3846 行数据、276.9230 万字符,
尝试压缩后下载,
解压后文件正常打开,但是压缩率并不高。
sql 查询接口的返回值,类型为 void,所以并没有接受这个返回值,不会产生大对象,只是在查询数据的过程中,处理了每一条数据,并没有保存数据在内存中。
1、通过Mybatis查询并导出超大Excel,防止内存溢出_resulthandler 导出excel_冰之杍的博客-CSDN博客
2、关于Easyexcel | Easy Excel
3、MyBatis中ResultHandler的使用
4、mybatis – MyBatis 3 | Java API
5、Java 使用 Easyexcel 导出大量数据_java easyexcel导出_余衫马的博客-CSDN博客
6、Java 使用 opencsv 库导出大量数据_余衫马的博客-CSDN博客