适用于项目实例内存小的情况 大数据量导出总是oom的情况
原理: 采用分页查询,一次查询一点点,查询到后立马写入文件 不在内存中继续持有对象
easyexcel版本采用3.3.2
工具类(业务类没提供 提供了可能也不适用):
CustomCellWriteWeightConfig CustomCellWriteHeightConfig 为导出自适应宽高配置 自行百度
import cn.hutool.core.io.FileUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.metadata.WriteSheet;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Supplier;
/**
* 分页导出工具类 防止项目实例内存小 无法加载对象到内存中 导致oom
* 使用此类的项目会引入 infra-api report-api
* @date 2023/09/26 16:44
**/
@Slf4j
@Component
public class TaskExcelUtil {
/**
* 每次查询 一页的条数
*/
private static final Integer PAGE_SIZE = 1000;
/**
* 每个sheet最多有多少条数据
* 不要超过 100w
*/
private static final Integer SHEET_MAX_SIZE = 500000;
/**
* 文件上传
*/
private static FileApi fileApi;
/**
* 任务中心
*/
private static TaskCenterApi taskCenterApi;
@SuppressWarnings("all")
public TaskExcelUtil(FileApi fileApi, TaskCenterApi taskCenterApi) {
this.fileApi = fileApi;
this.taskCenterApi = taskCenterApi;
}
/**
* 执行分页导出任务
* 注意传入分页查询函数时不要重复去查询count
* @param taskInfoRespDTO 异步任务详情
* @param countSupplier 获取总条数的函数
* @param queryFunc 分页查询函数
* @param clazz 结果类
*/
// TODO: 2024/09/27 将来改造为多线程查询提升效率 注意查询到的数据需要重新去排序
public static <T> void executeTask(TaskInfoRespDTO taskInfoRespDTO
, Supplier<Long> countSupplier
, BiFunction<Integer,Integer,List<T>> queryFunc
, Class<T> clazz){
ExcelTypeEnum excelTypeEnum = ExcelTypeEnum.XLSX;
log.info("导出{}任务开始",taskInfoRespDTO.getTaskName());
// 临时文件
File tempFile = FileUtil.createTempFile(excelTypeEnum.getValue(),true);
Long totalCount = countSupplier.get();
log.info("{}任务,总条数;{}",taskInfoRespDTO.getTaskName(),totalCount);
int writeCount = 0;
long pageTotal = (totalCount % PAGE_SIZE) > 0 ? ( totalCount / PAGE_SIZE ) + 1 : totalCount / PAGE_SIZE;
try (ExcelWriter excelWriter = EasyExcel.write(tempFile, clazz).excelType(excelTypeEnum)
.registerWriteHandler(new CustomCellWriteWeightConfig())
.registerWriteHandler(new CustomCellWriteHeightConfig())
.build()) {
Map<String, WriteSheet> sheetMap = new HashMap<>();
for (int pageNo = 1; pageNo <= pageTotal; pageNo++) {
List<T> list = queryFunc.apply(pageNo, PAGE_SIZE);
log.info("pageNo:{},resultSize:{}",pageNo,list.size());
writeCount += list.size();
int sheetNo = writeCount / SHEET_MAX_SIZE;
String sheetName = "sheet" + ( sheetNo + 1);
WriteSheet writeSheet = sheetMap.get(sheetName);
if (Objects.isNull(writeSheet)) {
log.info("构建:{}",sheetName);
writeSheet = EasyExcel.writerSheet(sheetNo,sheetName).build();
sheetMap.put(sheetName, writeSheet);
}
excelWriter.write(list, writeSheet);
}
}
//标记删除临时文件
tempFile.deleteOnExit();
log.info("导出到文件完成,path:{},size:{}",tempFile.getPath(),tempFile.length());
String url = fileApi.createExcelFile(taskInfoRespDTO.getFileName()+excelTypeEnum.getValue(), FileUtil.readBytes(tempFile));
log.info("上传完成,url:{}",url);
taskCenterApi.changeFinish(taskInfoRespDTO.getTaskId(), url);
log.info("导出{}任务结束",taskInfoRespDTO.getTaskName());
}
}
使用范例:
@Slave
@Async
public void exportToTaskCenter(TaskInfoRespDTO taskInfo, OrderReqVO reqVO) {
LambdaQueryWrapper<TransOrderDO> queryWrapper = transOrderMapper.builderWrapper(reqVO);
//如果不升序 导出时如果进来一条会导致导出数据错位
queryWrapper.orderByAsc(TransOrderDO::getId);
try {
TaskExcelUtil.executeTask(taskInfo, () -> transOrderMapper.selectCount(queryWrapper), (no, size) -> {
List<TransOrderDO> list = transOrderMapper.selectPage(new PageParam(no, size), queryWrapper, false).getList();
return TransOrderConvert.INSTANCE.convertExcel(list);
}, OrderExcelVO.class);
} catch (Exception e) {
log.error("导出异常", e);
taskCenterApi.taskFailed(taskInfo.getTaskId(), e.getMessage());
}
}
使用注意事项:
1.导出数据错位问题 如果是顺序id可以采用主键升序(用降序如果执行导出时有新增数据也会有此问题)解决问题,如果采用时间字段排序若数据同一秒有很多条(或可多字段排序保证唯一即可) 多次查询依然会导致错位问题
2.效率问题 查询时注意关闭mybatis分页查询的count功能(自行百度) 我这里的mybatisplus 2次封装过所以有三个参
优化建议:
项目实例2g,数据库4c8g情况 本人实测180w数据单线程导出大概要1小时多点
故可采用一次几个线程去查询数据 查询后排序好 写入文件后 再去继续查