禁用 ORM 框架的全量缓存

      百万级数据导出时,核心问题是内存占用过高导致的 OOM(内存溢出),本质原因是传统 Excel 导出会将全量数据加载到内存后再写入文件。

     优化的思路是:分批次处理数据、流式写入文件、减少内存中间态,同时结合 Excel 格式特性和 JVM 调优辅助。

一、选用工具

      传统 POI 的XSSFWorkbook(处理.xlsx)会将整个 Excel 对象加载到内存,百万级数据直接 OOM,必须替换为流式写入的工具:

工具 / 方案 核心优势 适用场景 内存占用
Apache POI SXSSFWorkbook 官方主流,基于 POI 封装,支持流式 需兼容复杂格式(样式 / 公式) 低(可设置内存窗口)
EasyExcel(阿里) 轻量、API 友好,天然流式,无 OOM 大部分业务导出(简单格式) 极低
JExcelApi 仅支持.xls(最大 65536 行) 老系统兼容,数据量 < 6 万
CSV 格式(伪 Excel) 纯文本,无格式,读写最快 无格式要求、超大数据量 几乎可忽略

二、优化方案

数据库分页查询:使用limit offset(注意大 offset 性能问题,建议用 “游标分页”:基于主键 / 时间戳)

// 游标分页(避免offset越大数据越慢)
Long lastId = 0L;
int batchSize = 1000; // 每批1000行,可根据内存调整
while (true) {
    List<Data> dataList = queryByCursor(lastId, batchSize); // where id > lastId limit batchSize
    if (CollectionUtils.isEmpty(dataList)) break;
    // 写入Excel
    writeToExcel(dataList);
    lastId = dataList.get(dataList.size()-1).getId(); // 更新游标
}

禁用 ORM 框架的全量缓存:
MyBatis/Hibernate 默认会缓存查询结果,需关闭二级缓存,且设置fetchSize(流式读取 ResultSet):

xml
<!-- MyBatis设置流式查询 -->
<select id="queryByCursor" fetchSize="1000" resultType="Data">
    select id, name, ... from table where id > #{lastId} limit #{batchSize}
</select>

方案 1:EasyExcel(推荐,最简实现)

EasyExcel 天然支持流式写入,底层封装了 SXSSF 的优化,API 极简:

// 1. 定义实体(映射Excel列)
@Data
public class DataDTO {
    @ExcelProperty("ID")
    private Long id;
    @ExcelProperty("姓名")
    private String name;
    // 其他字段...
}

// 2. 流式导出(分批写入)
public void export(HttpServletResponse response) throws IOException {
    response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    response.setHeader("Content-Disposition", "attachment; filename=data.xlsx");
    
    // 构建ExcelWriter,指定写入流(直接输出到响应,不落地本地)
    ExcelWriter writer = EasyExcel.write(response.getOutputStream(), DataDTO.class).build();
    WriteSheet sheet = EasyExcel.writerSheet("数据列表").build();
    
    // 分批查询+写入
    Long lastId = 0L;
    int batchSize = 1000;
    while (true) {
        List<DataDTO> dataList = queryByCursor(lastId, batchSize);
        if (CollectionUtils.isEmpty(dataList)) break;
        writer.write(dataList, sheet); // 写入当前批次
        lastId = dataList.get(dataList.size()-1).getId();
        // 手动释放当前批次内存(可选,JVM会自动回收,但大批次建议显式清空)
        dataList.clear();
    }
    
    // 关闭writer,完成写入
    writer.finish();
}

方案 2:SXSSFWorkbook(复杂格式场景)

POI 的 SXSSFWorkbook 通过 “内存窗口” 机制,只保留最近 N 行在内存,超出部分刷到磁盘临时文件:


public void exportWithSXSSF(HttpServletResponse response) throws IOException {
    // 初始化SXSSF,设置内存中保留100行(超出刷盘),关闭时删除临时文件
    SXSSFWorkbook workbook = new SXSSFWorkbook(100);
    workbook.setCompressTempFiles(true); // 压缩临时文件
    SXSSFSheet sheet = workbook.createSheet("数据列表");
    
    // 创建表头
    Row headerRow = sheet.createRow(0);
    headerRow.createCell(0).setCellValue("ID");
    headerRow.createCell(1).setCellValue("姓名");
    
    // 分批写入数据
    Long lastId = 0L;
    int batchSize = 1000;
    int rowNum = 1; // 数据行起始行号
    while (true) {
        List<Data> dataList = queryByCursor(lastId, batchSize);
        if (CollectionUtils.isEmpty(dataList)) break;
        
        for (Data data : dataList) {
            Row row = sheet.createRow(rowNum++);
            row.createCell(0).setCellValue(data.getId());
            row.createCell(1).setCellValue(data.getName());
        }
        
        lastId = dataList.get(dataList.size()-1).getId();
        dataList.clear(); // 释放内存
    }
    
    // 写入响应流
    response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    response.setHeader("Content-Disposition", "attachment; filename=data.xlsx");
    workbook.write(response.getOutputStream());
    
    // 关闭workbook,清理临时文件
    workbook.dispose(); // 必须调用,否则会残留临时文件
}

方案 3:CSV 格式(超大规模,无格式要求)

CSV 是纯文本,无需解析 Excel 结构,内存占用极低,百万级数据秒级导出:

public void exportCsv(HttpServletResponse response) throws IOException {
    response.setContentType("text/csv; charset=utf-8");
    response.setHeader("Content-Disposition", "attachment; filename=data.csv");
    // 用BufferedWriter提升写入效率
    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(response.getOutputStream(), StandardCharsets.UTF_8));
    
    // 写入表头
    writer.write("ID,姓名");
    writer.newLine();
    
    // 分批写入数据
    Long lastId = 0L;
    int batchSize = 10000; // CSV可设置更大批次(如1万)
    while (true) {
        List<Data> dataList = queryByCursor(lastId, batchSize);
        if (CollectionUtils.isEmpty(dataList)) break;
        
        for (Data data : dataList) {
            // 处理特殊字符(逗号、换行、引号)
            writer.write(String.format("%s,%s", data.getId(), escapeCsv(data.getName())));
            writer.newLine();
        }
        
        lastId = dataList.get(dataList.size()-1).getId();
        dataList.clear();
        writer.flush(); // 每批刷一次流,避免缓冲区溢出
    }
    
    writer.close();
}

// CSV特殊字符转义
private String escapeCsv(String str) {
    if (str == null) return "";
    // 包含逗号、双引号、换行符需用双引号包裹,双引号转义为两个
    if (str.contains(",") || str.contains("\"") || str.contains("\n") || str.contains("\r")) {
        return "\"" + str.replace("\"", "\"\"") + "\"";
    }
    return str;
}

内存与 JVM 优化

即使做了分批,JVM 参数不合理仍可能 OOM,需针对性调整:

  • 调整堆内存:根据服务器配置,增大堆内存(如-Xms4g -Xmx8g),避免堆空间不足;
  • 设置临时文件目录:SXSSF 的临时文件默认写系统临时目录(如 /tmp),若磁盘不足会报错,需指定:System.setProperty("java.io.tmpdir", "/data/tmp"); // 指向大容量磁盘
  • 禁用 POI 的日志冗余:POI 默认日志输出量大,占用内存,可调整日志级别为 ERROR;
  • 避免创建大量临时对象:循环中避免重复创建 CellStyle、Font 等对象(可复用):

    // 错误:循环内创建CellStyle(百万次创建,内存溢出)
    for (Data data : dataList) {
        CellStyle style = workbook.createCellStyle(); // 重复创建
        cell.setCellStyle(style);
    }

    // 正确:复用CellStyle
    CellStyle dataStyle = workbook.createCellStyle();
    dataStyle.setAlignment(HorizontalAlignment.CENTER);
    for (Data data : dataList) {
        cell.setCellStyle(dataStyle); // 复用
    }

架构层面优化(超大规模场景)

若数据量超千万级,单节点导出仍有瓶颈,需架构升级:

  • 异步导出:前端发起导出请求,后端异步处理(如 MQ + 定时任务),生成文件后存储到 OSS,前端轮询下载链接;
  • 分片导出:将数据按范围分片(如按 ID 分段),多线程 / 多节点并行导出多个 Excel 文件,最后合并(或直接分文件下载);
  • 文件落地后下载:先将 Excel 写入服务器本地 / OSS,再通过 Nginx/Apache 提供下载,避免直接通过应用服务器输出流传输(减少应用内存占用)。

    Logo

    DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

    更多推荐