基于EasyExcel多线程导出

1,372 阅读3分钟

写在前面

文章开始之前,请各位看官查阅EasyExcel简单用法。

EasyExcel.write(fileName)  
// 这里放入动态头  
.head(head()).sheet("模板")  
// 当然这里数据也可以用 List<List<String>> 去传入  
.doWrite(data());
  • headList<List<String>>
  • dataList<List<Object>>
    根据官方文档,实现写Excel基本上只需要准备headdata,事实上也是如此,并且这个feature助于我们实现解藕。回想一下传统POI好像也是这种写法。

目标

基于多线程构造data,输出多个文件,并压缩,基于redis记录导出进度,基于future实现下载中断,基于网络IO输出到客户端。

多线程构造data

  • 准备一个线程池,java 线程池有四个,选哪个好。根据你的需求场景,我这边的场景是报表导出,报表导出是个频繁的需求,频率随太阳上升而超频,随太阳下降而降频,这么看来newCachedThreadPool很适合。为什么?因为报表读多写少,基于缓存的线程池,帮助我们应对白天频繁的导出,夜晚导出变少之后缓存线程会回收,而夜晚有很多 job 会写入数据到报表系统。
  • 不要用 JDK 自带线程池,尽管刚说 newCachedThreadPool 很适合报表,但没人会建议使用 JDK 自带线程池,至于为什么,被同事发现并打一顿就知道了。

缓冲流 ByteArrayOutputStream & # BufferedOutputStream

第一个要排除普通 IO 流,就那写一个字节输出一个字节的节奏,遇到几 M 的文件,得好久才能写完。 这两个流都起到缓冲数据,一次性输出缓存数据到客户端,减少数据在用户态和系统态之间 copy 的次数,提高数据输出效率

写不下去了,直接贴代码吧!

private long writeFile(OutputStream out, T condition, int fileNum, int sheetNum, Long exportId, long searchCount) throws IOException {
    List<Future<List<List<Object>>>> futureList = new ArrayList<>(fileNum);
    AtomicInteger executedCount = new AtomicInteger(0);
    this.futureMap.put(exportId, futureList);

    try (ByteArrayOutputStream bOut = new ByteArrayOutputStream()) {
        IntStream.range(1, fileNum*sheetNum+1).boxed().forEach(sheetNo ->
                futureList.add(this.taskPoolExecutor.submit(() -> this.loadSheet(condition, sheetNo))));

        try (ZipOutputStream zOut = new ZipOutputStream(out)) {
            ExecutorMeta executorMeta = ExecutorMeta.builder()
                    .bOut(bOut)
                    .futureList(futureList)
                    .exportId(exportId)
                    .executedCount(executedCount)
                    .searchCount(searchCount)
                    .head(this.head())
                    .sheetNum(sheetNum)
                    .function(this.stringRedisTemplate.opsForValue()::get)
                    .consumer((k,v) -> this.stringRedisTemplate.opsForValue().set(k,v))
                    .biFunction(this::percentage)
                    .build();
            for (int fileNo = 0; fileNo < fileNum; fileNo++) {
                executorMeta.setFileNo(fileNo);
                EasyExcelExecutor.write(executorMeta);
                this.zip(zOut, bOut, fileNo+1);
            }
        }
    }
    return executedCount.get();
}

以上是核心代码,逐个解析参数

  1. out http 的 response 里的 OutPutStream
  2. condition 是数据查询的条件
  3. fileNum 还记得我们的目标是导出多个文件吗,该值根据导出总数据量searchCount大小和每个文件数据量pageSize大小算出来的
  4. sheetNum 每个 excel file 都有诺干个 sheet,该值通过pageSizesheetSize计算出来的
  5. exportId 每次导出请求需要落库,拿到 exportId 做一些事,例如基于 redis 的导出进度

下面解析代码

  1. futureList 还记得多线程查询数据吗,构建 future,后面遍历 future 拿到请求结果,这么做是为了实现导出中断
  2. executedCount 无需多说
  3. this.futureMap.put(exportId, futureList); 这一步和步骤一一起,为了后面导出中断时,通过 exportId 找到对于 future
  4. 后面的代码就是多线程加载数据,main 线程遍历 futureList ,调用 easyExcel 导出
  5. 剩下的看代码吧,我放到github github.com/Raidohub/re…

课后拓展

实现导出中断

  1. 将任务放到 future
  2. 执行任务过程,根据实际情况设置中断检查
sheetData = future.get();
if (sheetData.isEmpty()) {
    return;
}

future 阻塞原理

基于 LockSupport,底层是 UNSAFE,AQS 就是基于这玩意。如果有兴趣可以继续学习 Thread.join() 原理做个对比。

错误提示也很重要

image.png

结尾,今天的敷衍到此!