Skip to content

项目上有个导出 excel 场景发现很慢,怎么优化? ​

  • 标签:后端、场景题

🏛 业务架构师 ​

导出 Excel 慢,我会先问三个业务问题定方案档位:数据量多大?谁在用(运营偶尔导,还是财务每月例行)?实时性要求? 导出是典型的内部工具场景,原则是绝不能为了运营的导出把交易库拖挂。

慢的根因(通常三连)

  • 一次性 SELECT * 全量拉数据,内存和 DB 都扛不住
  • POI 的 XSSFWorkbook 把整个工作簿放堆内存,几十万行直接 OOM 或分钟级 STW
  • 同步导出:HTTP 长连接等几十秒,超时断了全白干

分档优化

小数据量(<5 万行):EasyExcel 流式写 + 分页读取(每次查 1000 条),改造半小时,够用。

大数据量(十万到百万行)——异步任务化:

用户点导出 → 创建任务(状态:处理中) → 立即返回
后台: 游标分批读 → EasyExcel 分批写 → 上传OSS → 更新任务状态
用户在"导出中心"页面看到完成 → 下载带时效的链接

要点:

  • 查询走从库,游标分页(WHERE id > lastId LIMIT n)代替 OFFSET
  • EasyExcel 边读边写、分 sheet(单 sheet 上限 104 万行)
  • 大文件考虑先导 CSV(体积减半、生成快一倍),Excel 样式对账场景根本不需要

习惯性收尾:导出中心做成通用能力后,顺手解决三个业务问题——审计(谁导了敏感数据,导出是数据泄露高发口)、限流(单人并发 1 个导出任务)、留存(文件 7 天过期,控制存储成本和泄露面)。把一次性性能优化沉淀成平台能力,才是这题在架构层面的答案。

🏗 系统架构师 ​

导出慢的本质通常是两件事:一次性把大数据集拉进内存 + 全量同步等待。我按四步优化。

一、先定位瓶颈 慢在 DB(大查询、深分页 OFFSET)还是慢在写 Excel(POI XSSF 全量 DOM 建模,百万行直接 OOM 或几分钟 GC)?看接口耗时拆解和 GC 日志即可判断。

二、读写双侧全部流式化

  • 读:放弃 LIMIT 1000000, 1000 这种深分页,改游标分页 WHERE id > ? ORDER BY id LIMIT 5000;MyBatis 开流式 cursor(fetchSize 设为 Integer.MIN_VALUE),数据逐批流过而不驻留内存。
  • 写:用 EasyExcel 或 POI 的 SXSSFWorkbook(滑动窗口只保留内存中几百行,其余刷盘),边读边写边 flush,内存占用从"数据全量"降到 O(窗口大小)。

三、架构级改造(数据量大到一定程度必走)

  • 异步化:点击导出 → 创建任务落库 → 返回"处理中"。后台任务线程池/任务平台分批拉数生成文件 → 上传 OSS → 站内信/邮件发下载链接。用户不再同步等待,接口秒回。
  • 隔离:导出走独立的线程池甚至独立实例,避免导出把在线接口的连接池和 CPU 吃光——批处理与在线负载必须物理或逻辑隔离,这是容量设计的基本原则。
  • 分片并行:千万级数据按 ID 区间切成 N 片并行生成多个 sheet/文件再合并,多机分摊。

四、数据源降级 超大规模导出本质是离线问题,直接走数仓/ClickHouse 出数,不碰主库。我的分档:10 万行内同步流式导出;以上一律异步任务 + OSS 链路。

💻 全栈开发专家 ​

慢的根因基本都是:一次性查全量 + XSSFWorkbook 全内存建模。我按四步优化。

1. 换流式写:SXSSFWorkbook / EasyExcel

POI 的 XSSFWorkbook 会把整个工作簿的 DOM 放堆里,几十万行直接撑爆;SXSSFWorkbook 滑动窗口只在内存保留 N 行:

java
// 我生产上直接用 EasyExcel,分页查 + 分批写
try (ExcelWriter writer = EasyExcel.write(response.getOutputStream(), OrderDTO.class).build()) {
    WriteSheet sheet = EasyExcel.writerSheet("订单").build();
    int lastId = 0;
    while (true) {
        List<OrderDTO> list = orderMapper.pageByCursor(lastId, 2000); // 游标分页
        if (list.isEmpty()) break;
        writer.write(list, sheet);
        lastId = list.get(list.size() - 1).getId();
    }
}

2. 大数据量改异步导出

超过 10 万行的导出一律异步:用户点导出 → 任务落库 + 线程池生成文件传 OSS → 站内信/邮件给下载链接。避免 HTTP 长连接超时和网关 504。

3. 查询优化

  • 用游标分页 WHERE id > ? LIMIT n,深分页 LIMIT 1000000, 2000 会越来越慢。
  • 只 select 导出需要的列;查询和写出并行(生产者-消费者,一边查下一页一边写当前页)。

4. 坑

  • Excel 单 sheet 上限 104 万行,超过要分 sheet 或分文件打 zip。
  • 响应流导出别忘记设置 Content-Disposition 和关闭流;异步导出记得清理过期 OSS 文件。