多线程导出压缩方案

2,596 阅读4分钟

上次的多线程导出太过于潦草,未成结局,一直在记忆里萦绕,仿佛一颗钉子深深扎在心头,时刻鞭策我,为了群友更好摸鱼,我痛定思痛,反复思考,决定重开一篇。

串行导出压缩流程

  1. 生成表头head
  2. 生成数据dataList
  3. 基于poi, EasyPoi, EasyExcel等工具输出到ByteArrayOutputStream
  4. ByteArrayOutputStream写入ZipOutputStream并压缩
  5. 关闭流

观察上述流程,仔细思考哪一步能变成并行,然后我们重新梳理一下流程。

并行导出压缩流程

  1. 生成表头head(串行)
  2. 分页生成数据dataList(并行)
  3. 循环分页结果集,基于poi, EasyPoi, EasyExcel等工具输出到ByteArrayOutputStream(并行)
  4. 循环分页结果集flush数据到ZipOutputStream(串行)
  5. 关闭流(串行)
从上面可以看出,导出的模型是,多线程读数据,单线程写数据到流。其中第2步和第3步可以合并
有必要解释第1步,第4步,第5步为什么是串行

第1步和第5步不用多说,重点说第4步。简单而言就是数据写入到流的过程不是线程安全的,如果并行往流里写入数据,会发生本来线程 A 写的是,“张三,你竟敢摸鱼”,然后线程 B 写的是“老板娘!”,两个线程同时往流里面写入数据,结果输出了“张三,你竟敢摸老板娘!”这样的错误结果,所以第4步只能串行。这是个玩笑话,更多的时候会因为数据边界错误导致异常。
明确并行导出流程,下面我们解决因为并行而引出的问题!

分页切割问题

我们按照两个维度切割数据,文件和sheet,理想效果是根据导出数据总量得出文件数量 fileNum,根据sheetSize 计算每个文件sheet数量sheetNum,其中每一页sheet查一次数据库所得,可以得出sheet总数量 = fileNum * sheetNum,同时也是查询数据库的次数

截屏2022-12-04 15.14.36.png

截屏2022-12-04 15.15.24.png

文件和sheet顺序问题

上图是3个文件,每个文件有两个sheet(其中第3个文件只有一个sheet,是因为第6个sheet没有数据,没必要输出),sheetNo的编号从1到5,对应SQLlimit sheetNo, sheetSize,然后文件也是要从file1,file2,file3这样排序(文件名是url编码了)

导出过程异常

导出会输出多个文件,每个文件多个sheet,我们不能因为某一个sheet,某个文件出错让整个导出过程失败,要把错误的原因写在sheet返回客户端

三个可能出现异常的地方

  1. 查询数据,可能出现业务异常,数据库连接异常
  2. 基于导出工具写入流,可能会出现异常,概率不大
  3. 流提前关闭了,可能因为服务器资源不够,导致流异常关闭,神仙也救不了

出现异常,想方法补救,不是重新查询数据,应该是友好把错误信息写到sheet,这样客户就能知道出错了,怎么出错了。

image.png 上图是三个文件,第一个文件sheet-1的数据没问题,sheet-2因为读数据错误,在sheet输出load fial cause / by zore,这样用户就知道哪里有问题。如果因为异常整个文件或者整个sheet都不输出,客户根本没法知道导出的文件是否正常!

解决上面几个问题,导出功能基本清晰,但我们是极客,怎么甘于平凡,于是我加了几个功能

导出中断

都多线程导出了,数据量肯定不小,几十万,几百万,几千万在那里导出,就看着进度条慢慢的涨,而你又等不下去了,把导出x停了,换个查询条件继续导出。由此可见,导出中断有必要。本教程我们基于future实现中断导出线程,通过构建futureList,与导出日志ID绑定到map,需要中断的时候从map拿出futureList,遍历futureList,调用future.cancel()。相关知识需要futurejava中断线程,感兴趣的同学自行百度

断点续导

是的,有个名词叫断点续传,这两个功能是一样的,一个是可以暂停上传,一个是可以暂停下载。该方案需要导出文件到本地,基于RandomAccessFile实现。本教程基于web导出,实在是没有料呀,同学们感兴趣可以自己尝试。

导出进度条

没得说,进度条可以是假的,但不能没有,基于redis做个导出进度条没毛病吧!

github

项目放在github,主要核心DefaultedExporterEasyExcelExecutor已经添加注释,不担心看不懂,clone项目还需要自己补充repository