异步导出让 HTTP 请求先返回任务编号,后台生成文件,用户再查询状态与下载。异步只改变执行位置,不会自动降低内存,也不会自动获得失败重试和断点续传。

先做一个文件实验

下面的 Java 21 程序在后台写 10 万行 CSV,内存中不保存所有行。它没有数据库、HTTP 接口或 Excel 图片,先用它观察线程、输出文件与完成确认。

保存 AsyncCsvDemo.java:

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class AsyncCsvDemo {
public static void main(String[] args) throws Exception {
Path output = Path.of("export.csv").toAbsolutePath();
Path partial = Path.of("export.csv.part").toAbsolutePath();
if (Files.exists(output) || Files.exists(partial)) {
throw new IllegalStateException("输出已存在,请换一个空实验目录");
}
var executor = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2), new ThreadPoolExecutor.AbortPolicy());
try {
var task = executor.submit(() -> {
try {
try (var writer = Files.newBufferedWriter(partial, StandardCharsets.UTF_8)) {
writer.write("id,name\r\n");
for (int id = 1; id <= 100000; id++) {
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException("任务被取消");
}
writer.write(id + ",demo-" + id + "\r\n");
}
}
Files.move(partial, output);
return Files.size(output);
} catch (Exception failure) {
try { Files.deleteIfExists(partial); }
catch (Exception cleanup) { failure.addSuppressed(cleanup); }
throw failure;
}
});
System.out.println("后台任务已提交");
try {
System.out.println("完成,字节数=" + task.get(30, TimeUnit.SECONDS));
} finally {
if (!task.isDone()) task.cancel(true);
}
} finally {
executor.shutdownNow();
executor.awaitTermination(5, TimeUnit.SECONDS);
}
}
}
Terminal window
javac -encoding UTF-8 AsyncCsvDemo.java
java AsyncCsvDemo
(Get-Content export.csv | Measure-Object -Line).Lines

预期有 100001 行,含一行表头。输出先写 .part,关闭后才移动为完成文件。真实 CSV 字段需要转义引号、逗号和换行,并根据使用场景处理表格公式注入,不能直接拼任意用户文本。

接进 Spring Boot

提交接口校验用户与查询参数,创建带所有者的任务记录,将工作提交给有界线程池,返回任务 ID。状态至少区分等待、运行、成功、失败、取消与过期;队列拒绝时应立即报告,不能留下永远等待的任务。

@Async 通过 Spring 代理生效,同一个对象内部直接调用方法不会转为异步。后台异常要进入任务结果;日志中没有异常不代表任务完成。下载接口重新检查所有者与文件状态,文件路径不由客户端指定。

大数据与 Excel

查询按稳定主键游标或一致性快照分页,明确导出期间数据更新的处理;简单 offset 分页可能漏行或重复。每批读取和写入后释放资源,图片下载设置超时、大小与并发限制。

POI 的 SXSSFWorkbook 可以限制内存中的行窗口,但图片、样式、共享字符串和临时文件仍有成本;一张 Excel 工作表最多 1048576 行,包含表头。实现分表/分文件、关闭与清理临时文件后,再用真实规模验证。

进程重启后本例任务消失。生产重试需要确定是重新生成文件还是从一致性检查点恢复,不能把失败后重新提交称为断点续传。

参考:ThreadPoolExecutor、POI SXSSF。