Skip to content

3 大数据量导出

guanquan.wang edited this page Sep 25, 2025 · 16 revisions

前面已经展示了简单的导出示例,例子中大部分都传入一个数组,但是我们知道对于十万级、百万级的数量,不可能一次拉取到内存,此时分片功能就突现价值了。

自动分片

ListSheet和ListMapSheet默认支持分片,我们只需要覆写protected List<T> more()方法即可。

下面代码将展示如何分页拉取学生数据,每次拉取1024条数据,这样内存中最多也就1024条数据,边写边拉取数据,直到返回空数组或者null。

new Workbook()
    .bestSpeed() // <- 性能模式
    .addSheet(new ListSheet<Student>() {
        private int pageNo = 0, limit = 1024;
        @Override
        protected List<Student> more() {
            return service.getPageData(++pageNo, limit);
        }
    }).writeTo(Paths.get("F:/excel"));

得益于EEC的Worksheet默认分页,我们无需更多的处理,上面代码即可支持百万级、千万级数据导出

如下代码测试200w随机数字导出的示例

// 导出200w数据
final int loop = 2000;
new Workbook("200w").addSheet(new ListSheet<E>() {
    int n = 0; // 页码
    @Override
    public List<E> more() {
        return n++ < loop ? data() : null;
    }
}).writeTo(Paths.get("F:/excel"));

// 生成测试数据
public List<E> data() {
    List<E> list = new ArrayList<>(1000);
    for (int i = 0; i < 1000; i++) {
        E e = new E();
        list.add(e);
        e.nv = random.nextInt();
        e.str = getRandomString();
    }
    return list;
}

// 测试对象
public static class E {
    @ExcelColumn
    private int nv;
    @ExcelColumn
    private String str;
}

打开文件效果如下:

第一页共写入1048576行,这也是xlsx格式单页最大行数

page 1

写满第一页后自动新增一个worksheet并写入剩余数据951426,共计200w+2(2个表头)

page 2

使用data-supplier拉取数据

v0.5.14版本新增data-supplier减化了分片开发难度,它被定义为BiFunction<Integer, T, List<T>>其中第一个入参Integer表示已拉取数据的记录数, 第二个入参T表示上一批数据中最后一个对象,业务端可以通过这两个参数来计算下一批数据应该从哪个节点开始拉取, 通常你可以使用第一个参数除以每批拉取的数据大小来确定当前页码,如果数据已排序则可以使用T对象的排序字段来计算下一批数据的游标从而跳过 limit ... offset ...分页查询从页极大提升取数性能。

new Workbook()
    .addSheet(new ListSheet<Item>().setData((i, lastOne) -> i < 2_000_000 ? service.scrollQuery(i, lastOne) : null))
    .writeTo(Paths.get("F:/excel/200w.xlsx"));

增加进度兼听

大数据量导出时往往耗时较长,表现出来就是程序卡在那里一动不动,为了防止这种假死现象我们可以在导出时增加一个进度兼听代码段,它位于Workbook#onProgress方法,它有两个入参 第一个是sheet表示当前正在导入哪个工作表,第二个是rows表示已写入的行数,onProgress方法每1000行被执行一次

new Workbook()
    // 添加进度兼听代码,外部可观察写入数据量,可做导出进度也简单写日志
    .onProgress((sheet, rows) -> System.out.println(sheet.getName() + " 已写入: " + rows))
    .addSheet(new ListSheet<E>().setData((i, lastOne) -> i < 2_000_000 ? E.data() : null))
    .writeTo(Paths.get("F:/excel/200w.xlsx"));

对于导出时长评估的补充说明:xlsx格式本质为zip格式,所以导出可分为两阶段,第一阶段为写数据阶段,第二阶段为压缩阶段。

  • 第一阶段可以根据总数据量和每1000条数据的耗时计算出该阶段的总耗时(每批动态计算来矫正偏差)
  • 第二阶段就比较难动态计算了,一般做法是提前拿文件做基准zip压缩测试,根据每mb的基准时间来计算,好在做完基准测试后可直接根据文件大小就能计算出压缩时长,可以粗估为第一阶段时间同等时间

读取分片数据

对于自动分片的数据,我们不需要一个sheet一个sheet读取,而是直接使用Stream的flatMap功能将worksheet降维处理,如下代码统计200w数据中数字大于1w的个数

try (ExcelReader reader = ExcelReader.read(Paths.get("F:/excel/200w.xlsx"))) {
    long count = reader.sheets()
        .flatMap(Sheet::dataRows) // 使用flatMap降维
        .map(row -> row.getInt(0)).filter(i -> i > 10000) // 取第1列数据并过滤大于10000的值
        .count();
    System.out.println("共计" + count + "个数大于1w");
}

贴出DEBUG日志

- load xl\worksheets\sheet1.xml
- Dimension-Range: A1:B1048576
- end of file.
- load xl\worksheets\sheet2.xml
- Dimension-Range: A1:B951426
- end of file.
共计999601个数大于1w

通过DEBUG日志可以看到使用flatMap解析完sheet1后接着解析sheet2,得出结果999601,与excel筛选出的结果一致

filter 1 filter 2

Clone this wiki locally