上次的多线程导出太过于潦草,未成结局,一直在记忆里萦绕,仿佛一颗钉子深深扎在心头,时刻鞭策我,为了群友更好摸鱼,我痛定思痛,反复思考,决定重开一篇。
串行导出压缩流程
- 生成表头
head - 生成数据
dataList - 基于
poi,EasyPoi,EasyExcel等工具输出到ByteArrayOutputStream ByteArrayOutputStream写入ZipOutputStream并压缩- 关闭流
观察上述流程,仔细思考哪一步能变成并行,然后我们重新梳理一下流程。
并行导出压缩流程
- 生成表头
head(串行) - 分页生成数据
dataList(并行) - 循环分页结果集,基于
poi,EasyPoi,EasyExcel等工具输出到ByteArrayOutputStream(并行) - 循环分页结果集
flush数据到ZipOutputStream(串行) - 关闭流(串行)
从上面可以看出,导出的模型是,多线程读数据,单线程写数据到流。其中第2步和第3步可以合并
有必要解释第1步,第4步,第5步为什么是串行
第1步和第5步不用多说,重点说第4步。简单而言就是数据写入到流的过程不是线程安全的,如果并行往流里写入数据,会发生本来线程 A 写的是,“张三,你竟敢摸鱼”,然后线程 B 写的是“老板娘!”,两个线程同时往流里面写入数据,结果输出了“张三,你竟敢摸老板娘!”这样的错误结果,所以第4步只能串行。这是个玩笑话,更多的时候会因为数据边界错误导致异常。
明确并行导出流程,下面我们解决因为并行而引出的问题!
分页切割问题
我们按照两个维度切割数据,文件和
sheet,理想效果是根据导出数据总量得出文件数量fileNum,根据sheetSize计算每个文件sheet数量sheetNum,其中每一页sheet查一次数据库所得,可以得出sheet总数量 =fileNum*sheetNum,同时也是查询数据库的次数
文件和sheet顺序问题
上图是3个文件,每个文件有两个sheet(其中第3个文件只有一个sheet,是因为第6个sheet没有数据,没必要输出),sheetNo的编号从1到5,对应SQL的limit sheetNo, sheetSize,然后文件也是要从file1,file2,file3这样排序(文件名是url编码了)
导出过程异常
导出会输出多个文件,每个文件多个
sheet,我们不能因为某一个sheet,某个文件出错让整个导出过程失败,要把错误的原因写在sheet返回客户端
三个可能出现异常的地方
- 查询数据,可能出现业务异常,数据库连接异常
- 基于导出工具写入流,可能会出现异常,概率不大
- 流提前关闭了,可能因为服务器资源不够,导致流异常关闭,神仙也救不了
出现异常,想方法补救,不是重新查询数据,应该是友好把错误信息写到sheet,这样客户就能知道出错了,怎么出错了。
上图是三个文件,第一个文件
sheet-1的数据没问题,sheet-2因为读数据错误,在sheet输出load fial cause / by zore,这样用户就知道哪里有问题。如果因为异常整个文件或者整个sheet都不输出,客户根本没法知道导出的文件是否正常!
解决上面几个问题,导出功能基本清晰,但我们是极客,怎么甘于平凡,于是我加了几个功能
导出中断
都多线程导出了,数据量肯定不小,几十万,几百万,几千万在那里导出,就看着进度条慢慢的涨,而你又等不下去了,把导出x停了,换个查询条件继续导出。由此可见,导出中断有必要。本教程我们基于future实现中断导出线程,通过构建futureList,与导出日志ID绑定到map,需要中断的时候从map拿出futureList,遍历futureList,调用future.cancel()。相关知识需要future和java中断线程,感兴趣的同学自行百度
断点续导
是的,有个名词叫断点续传,这两个功能是一样的,一个是可以暂停上传,一个是可以暂停下载。该方案需要导出文件到本地,基于RandomAccessFile实现。本教程基于web导出,实在是没有料呀,同学们感兴趣可以自己尝试。
导出进度条
没得说,进度条可以是假的,但不能没有,基于redis做个导出进度条没毛病吧!
github
项目放在github,主要核心DefaultedExporter和EasyExcelExecutor已经添加注释,不担心看不懂,clone项目还需要自己补充repository等