EasyExcel 低内存导出大数据量的Excel方案探索 50万行 50列 (附:实现代码)

news/2024/4/29 18:52:27/文章来源:https://blog.csdn.net/qq_38249409/article/details/129127289

文章目录

  • 1.前言
  • 2.准备工作
  • 3.导出测试
    • 3.1.单次查询、全量导出
    • 3.2. 多次查询,多个文件,单次写入
    • 3.3.多次查询,多个文件,多次写入
    • 3.4.多线程导出探索
    • 3.5.文件打包成ZIP
    • 3.6.响应给客户
  • 4.实现代码
  • 5.结语

1.前言

最近接到一个需求,需要从服务器中导出大量数据到Excel中,数据量大概为50万行,50列,借助这个机会,就想对使用使用低内存导出大数据量的方案进行探索,总结出一个通用可行性方案,以方便日后随时可以使用,同时也分享一下探索的过程。

2.准备工作

技术栈选型
市面上常用的导出 Excel 的工具有 apache poijxlsAlibaba EasyExcel 等,在以往的使用经验以及查询的资料中显示,前面两者对于导出的优化不太好,在大数据量导出时容易OOM,Easy Excel 针对OOM问题做过优化,而且社区活跃,使用比较简单,此处选择EasyExcel。
附:《EasyExcel官方文档》

环境准备

  • 客户端:工作使用笔记本电脑,CPU为 8核i5 低压,排除其他应用消耗后的可用内存约为4GB。
  • 数据库:本地安装的 MySQL 数据库。
  • 服务:新建SpringBoot服务,除需要验证的Excel功能以外,没有其他功能的消耗。

数据准备
在本地 MySQL 数据库中插入 50w 条数据备用。

探索目标

  • 能否正常导出Excel文件
  • 导出过程中的 CPU、内存等消耗情况
  • 各操作步骤的耗时
  • 导出的文件大小,文件能否正常操作

3.导出测试

对不同的导出方式进行验证,通过 Java visual VM 来观察导出过程中的CPU与内存使用情况,这里会使用到一个插件Visual GC,用来查看JVM的各分代信息,不太清楚这个插件的同学可以查看这篇文章《垃圾回收算法和垃圾收集器》中的第5点,里面有工作所在位置,以及插件的安装方法。

下面主要导出到本地硬盘(后面会补充响应给用户客户端的验证),展示的是导出过程中的性能相关指标,实现代码会放在后面的第4点中。

3.1.单次查询、全量导出

一次性将数据全部查询出来,放到内存中,再将所有的数据插入的Excel中。
在这里插入图片描述

查询耗时:19710 ,约 20 s
导出耗时:87588 ,月 88 s
内存消耗:峰值约为 1.5 GB
CPU消耗:约有 20s 左右的高消耗,峰值约为 65%
文件大小:约 120 MB

通过我的工作电脑打开这个 Excel 文件,大于耗时1分钟,打开后基本上就卡死了,无法操作。

通过输出的性能指标来看,需要针对性的做以下几点优化:

  • 减少CPU的消耗
  • 减少内存的使用
  • 缩小Excel文件的大小

首先,缩小Excel文件的大小可以考虑将文件进行拆分,例如:拆分为每个文件5w数据,生成10个文件,再将10个文件打包成zip进行下载。

其次,减少内存的使用,可以考虑将一次性查询 50w 的数据,修改为分批次查询每次查询5w条数据,这样与上面的缩小文件大小不谋而合。生成了一个Excel之后,已经插入到Excel文件中的数据就不需要存在与内存中了,此时通过 GC 清理掉,就可以减少内存的消耗。

最后,减少CPU的消耗,目前阶段暂时还不清楚 CPU 的消耗主要是存在与何处,可以先做上面的两个优化,再观察一下效果。

3.2. 多次查询,多个文件,单次写入

由于我在准备数据阶段插入的数据是比较均匀的,所以理论上50w数据占用1.5GB,那么 5w 条数据占用的内存 就是 150MB,为了尽可能的模拟实际使用场景,此处将JVM的堆大小限制为:-Xms256m -Xmx256m ,再次执行导出,结果如下:
在这里插入图片描述
在这里插入图片描述

查询+导出耗时:耗时:106525 ,约 107s
内存消耗:峰值约为170 MB
CPU消耗:持续约 2.5 min,峰值约为 35%


优化效果明显,导出时间几乎没有变化,内存峰值下降了88%,CPU峰值下降了46%

从CPU的角度来看,可以猜测生成Excel时,插入的数据量越小,则CPU的消耗就越小,带着这样的猜想,进一步进行验证。同时也可以注意一下上面第二张图中的老年代 Full GC 的次数,可以和下一个方案做一下对比。

3.3.多次查询,多个文件,多次写入

之所以有这么一个验证,主要是看到了 EasyExcel文档中有这么一个注释:
在这里插入图片描述
这里取一个极限值5000,将5w条数据再次拆分为10分,写入Excel的时候分10次写入,再看一下消耗情况。
在这里插入图片描述
在这里插入图片描述

耗时:173848 ms 约 170s
内存消耗:峰值约为190 MB
CPU消耗:持续约 2.5 min,峰值约为 17%


先看 CPU ,CPU 的峰值进一步降低了50%,则我们先前的猜测是正确的。

内存的峰值上虽然多了20MB,但是最小值也也降低到了50MB左右,这里把两张图对比可以可以直观的感受到区别:
在这里插入图片描述
同时再对比一下visual GC中的两个图,Full GC的次数由20次缩小为7次,证明大部分的内存在新生代就已经回收了。

也就是说,当前这种方案是可以承受更低的堆内存限制的,之所以峰值还是会上去,是因为垃圾回收器自动回收垃圾时会有一个阈值,在没有超过这个阈值的时候,垃圾回收器也不会随意的回收内存,毕竟回收内存是会产生停顿时间的。

3.4.多线程导出探索

如果使用多线程并发生成10个文件,理论上速度会更快,但是由于同样的,数据会全部加载到内存中,对于内存的消耗也会大增,抱着不死心的态度,还是验证一下。
将代码修改为多线程之后,执行一下导出:
在这里插入图片描述
这次CPU的消耗简直不忍直视,我在截完图后,电脑就直接卡死了只能强制重启。不死心的我,换了另一台台式机进行测试,配置为 i79700 8核16线程,这次顺利导出了,耗时约为10s,但是即使是这种配置,CPU的峰值也飙升到了70%左右。


用性能来换时间是可以实现的,但是需求的硬件配置太高,现在服务器又那么贵,实现的时候需要评估功能收益与硬件成本,在业务要求的导出实时性不高的情况下,使用以空间换时间的方式更为合适。

3.5.文件打包成ZIP

响应给客户时,需要将多个文件打包成一个ZIP文件,不然用户就得下载10次,体验极差。

至于压缩方面,Excel本身对数据就已经做了压缩,再将其打包成zip的形式所带来的数据压缩率就很低了,所以几乎不考虑压缩的问题。

这里的打包主要是通过hutool包提供的压缩方式,文件服务器使用的是腾讯云的COS,先测试一下执行时间以及性能消耗。
在这里插入图片描述

打包耗时:5459
内存和CPU几乎没有消耗

3.6.响应给客户

完成ZIP的打包之后,就需要将文件响应给客户了,有两种形式:

  • 通过Response同步响应给用户
  • 上传到文件系统,给用户返回一个下载地址

第一种方式,只能做成同步响应的,也就是说,用户在点击下载之后需要在当前页面等待3到5分钟,不能做其他的操作。

第二种方式,可以做成异步响应的,用户在点击下载之后,返回一个“提交成功,正在处理中”的提示,用户就可以做其他事情去了,待上传到文件系统成功之后将下载地址通知给用户即可。
通知的方式多种多样,可以用邮件通知、企业微信群通知、站内信通知,甚至可以做一个下载记录列表将下载地址存入到数据库中。


同时,对于多个用户同时下载的情况,也可以通过队列进行排队,一次只处理1个或两个(需要通过硬件来评估)导出请求。

4.实现代码

引入依赖

 <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.2.1</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-core</artifactId><version>4.6.1</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.2</version>
</dependency>

我这里使用的ORM框架是mybatis-plus,可以换成任意自己喜欢的


单次查询,全量导出

public void export() {long t1 = System.currentTimeMillis();List<MemberInfo> memberInfos = memberInfoMapper.selectList(new QueryWrapper<>());long t2 = System.currentTimeMillis();String fileName = "d://excel/simpleWrite" + System.currentTimeMillis() + ".xlsx";EasyExcel.write(fileName, MemberInfo.class).sheet("模板").doWrite(() -> memberInfos);long t3 = System.currentTimeMillis();System.out.println("查询耗时:" + (t2 - t1));System.out.println("导出耗时:" + (t3 - t2));}

多次查询,多个文件,单次写入

public void export2() {long t1 = System.currentTimeMillis();for (int i = 0; i < 10; i++) {// 分页去数据库查询数据 这里可以去数据库查询每一页的数据QueryWrapper<MemberInfo> queryWrapper = new QueryWrapper<>();queryWrapper.last("limit " + i * 50000 + ",50000");List<MemberInfo> memberInfos = memberInfoMapper.selectList(queryWrapper);String fileName = "d://excel/simpleWrite" + "模板" + i + System.currentTimeMillis() + ".xlsx";try (ExcelWriter excelWriter = EasyExcel.write(fileName, MemberInfo.class).build()) {WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();excelWriter.write(memberInfos, writeSheet);}}long t2 = System.currentTimeMillis();System.out.println("耗时:" + (t2 - t1));
}

多次查询,多个文件,多次写入

public void export4() {long t1 = System.currentTimeMillis();for (int i = 0; i < 10; i++) {String fileName = "d://excel/simpleWrite" + "模板" + i + System.currentTimeMillis() + ".xlsx";try (ExcelWriter excelWriter = EasyExcel.write(fileName, MemberInfo.class).build()) {WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();for (int j = 0; j < 10; j++) {// 分页去数据库查询数据 这里可以去数据库查询每一页的数据QueryWrapper<MemberInfo> queryWrapper = new QueryWrapper<>();queryWrapper.last("limit " + (10 * i + j) * 5000 + ",5000");List<MemberInfo> memberInfos = memberInfoMapper.selectList(queryWrapper);excelWriter.write(memberInfos, writeSheet);}}}long t2 = System.currentTimeMillis();System.out.println("耗时:" + (t2 - t1));
}

打包

 public void pack() {long t1 = System.currentTimeMillis();ZipUtil.zip("d://excel", "d://zip/导出数据.zip");long t2 = System.currentTimeMillis();System.out.println("打包耗时:" + (t2 - t1));
}

多线程探索

public void export3() {long t1 = System.currentTimeMillis();CountDownLatch countDownLatch = new CountDownLatch(10);for (int i = 0; i < 10; i++) {int finalI = i;new Thread(() -> {String fileName = "d://excel/simpleWrite" + "模板" + finalI + System.currentTimeMillis() + ".xlsx";try (ExcelWriter excelWriter = EasyExcel.write(fileName, MemberInfo.class).build()) {QueryWrapper<MemberInfo> queryWrapper = new QueryWrapper<>();queryWrapper.last("limit " + finalI * 50000 + ",50000");List<MemberInfo> memberInfos = memberInfoMapper.selectList(queryWrapper);WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();excelWriter.write(memberInfos, writeSheet);countDownLatch.countDown();}}).start();}try {countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}long t2 = System.currentTimeMillis();System.out.println("耗时:" + (t2 - t1));}

上传到文件服务器涉及到公司信息就不放在这里了,我这边使用的是腾讯云的COS,可以替换为你们自己公司所用的文件服务器。

5.结语

从几个方面总结一下低内存导出大数据量的Excel的方法:

  • 降低存入到内存中的数据,使用分批次查询、分批次插入数据的方式。
  • 尽可能的减少并发,避免使用多线程操作Excel,同时,还可以通过队列做异步和限流,排队处理导出请求。
  • 考虑到Excel文件过大无法操作,可以将一个大文件拆分为多个小文件。

以上,是从Demo的角度验证了可行性,实际生产使用还需要考虑到文件的过期、文件的加密等,如果是发布到容器中还需要考虑文件路径无法找到等问题,但是与主题关系不大,这里就不做过多的讨论了,可以实际遇到问题再做分析和考虑。


如果觉得本文有帮助的话,可以帮忙点点赞哦!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_260702.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

多元化增长引擎业务占比超四成,联想开启混动模式?

一句话概括联想集团的业绩&#xff1a;预料之内的整体下滑&#xff0c;超出预期的第二曲线。 上周五&#xff08;2月17日&#xff09;&#xff0c;联想集团发布了2022到2023财年第三季度业绩。根据财报&#xff0c;联想集团实现营收152.67亿美元&#xff0c;同比下降24%&#…

【机器学习】决策树-Gini指数

1. CART树 分类回归树(CART,Classification And Regression Tree)算法是一种决策树分类方法。CART每一个节点上都采用二分法&#xff0c;采用一种二分递归分割的技术&#xff0c;CART生成的树必须是二叉树&#xff0c;也就是无论回归还是分类&#xff0c;无论特征离散还是连续&…

15-基础加强3-单元测试日志

文章目录1.单元测试1.1概述【理解】1.2特点【理解】1.3使用步骤【应用】1.4相关注解【应用】2.日志2.1概述【理解】2.2日志体系结构和Log4J【理解】2.3入门案例【应用】1.单元测试 1.1概述【理解】 JUnit是一个 Java 编程语言的单元测试工具。JUnit 是一个非常重要的测试工具…

Profinet转ModbusTCP网关连接昆仑通态触摸屏配置案例

本案例是模拟将Modbus TCP 设备数据接入到西门子PROFINET 网络中。 使用设备为西门子 S7-1500 型 PLC, Profinet转ModbusTCP网关。MODBUS 从站昆仑通态触摸屏。 配置方法&#xff1a; 打开博图&#xff0c;新建项目并添加站点。 添加1513PLC。 设置好IP并处于联网状态 导入Pr…

VTK学习笔记(四十)vtk提取单层数据

VTK学习笔记&#xff08;四十&#xff09;vtk提取单层数据1. VTK学习笔记&#xff08;四十&#xff09;vtk提取单层数据1.1 vtkImageReslice提取单层图像1.2 vtkExtractVOI提取单层图像1. VTK学习笔记&#xff08;四十&#xff09;vtk提取单层数据 VTK实现单层图片的提取方法有…

力扣mysql刷题记录

mysql刷题记录 刷题链接https://leetcode.cn/study-plan/sql/?progressjkih0qc mysql冲&#xff01;mysql刷题记录一. 1699. 两人之间的通话次数题解二、1251. 平均售价题解三. 1571. 仓库经理题解四.1445. 苹果和桔子解五.1193. 每月交易 I题解六.1633. 各赛事的用户注册率题…

Android Spider XX兔 Wechat Cookie 校验 注册案例(二)

声明 此次案例只为学习交流使用&#xff0c;抓包内容、敏感网址、数据接口均已做脱敏处理&#xff0c;切勿用于其他非法用途&#xff1b; 文章目录声明前言一、资源推荐二、任务说明三、App抓包分析四、还原JS加密1、Date类2、MD5类3、组合调用还原 api_token 参数4、execjs模…

Android 基础知识4-2.10 GridLayout(网格布局)详解

一、GridLayout&#xff08;网格布局&#xff09;概述 GridLayout 布局是 Android 4.0 以后引入的新布局&#xff0c;和 TableLayout(表格布局) 有点类似&#xff0c;不过它功能更多&#xff0c;也更加好用&#xff0c;最大的特点是放置的组件自动占据网格的整个区域,每个组件的…

《FPGA学习》->呼吸灯

&#x1f34e;与其担心未来&#xff0c;不如现在好好努力。在这条路上&#xff0c;只有奋斗才能给你安全感。你若努力&#xff0c;全世界都会为你让路。呼吸灯&#xff0c;简而言之就像人类呼吸一样&#xff0c;有节奏的让LED灯从&#xff1a;灭->微微亮->微亮->亮-&g…

K_A12_014 基于STM32等单片机驱动S12SD紫外线传感器模块 串口与OLED0.96双显示

K_A12_014 基于STM32等单片机驱动S12SD紫外线传感器模块 串口与OLED0.96双显示一、资源说明二、基本参数参数引脚说明三、驱动说明IIC地址/采集通道选择/时序对应程序:数据对比&#xff1a;四、部分代码说明1、接线引脚定义1.1、STC89C52RCS12SD紫外线传感器模块1.2、STM32F103…

Leetcode.2257 统计网格图中没有被保卫的格子数

题目链接 Leetcode.2257 统计网格图中没有被保卫的格子数 Rating &#xff1a; 1709 题目描述 给你两个整数 m和 n表示一个下标从 0开始的 m x n网格图。同时给你两个二维整数数组 guards和 walls&#xff0c;其中 guards[i] [rowi, coli]且 walls[j] [rowj, colj]&#xff…

Jmeter常用断言之BeanShell断言详解

BeanShell断言可以使用beanshell脚本来执行断言检查&#xff0c;可以用于更复杂的个性化需求&#xff0c;使用更灵活&#xff0c;功能更强大&#xff0c;但是要能够熟练使用beanshell脚本 在这里除了可以使用beanshell的内置变量外&#xff0c;主要通过 Failure 和 FailureMess…

Dart中的混入类mixin

介绍 Mixin 是一种在多重继承中复用某个类中代码的方法模式。 Mixin 是面向对象程序设计语言中的类&#xff0c;提供了方法的实现。其他类可以访问mixin类的方法、变量而不必成为其子类。 简单来说就是官方设计了一个种可以方便复用的类&#xff0c;不必去实现很多接口。 应…

C++011-C++循环+枚举

文章目录C011-C循环枚举枚举枚举思想枚举举例题目描述 统计因数题目描述 质数判定错误方法一&#xff1a;优化方法1&#xff1a; 用break实现优化优化方法2&#xff1a; sqrt(n)题目描述 水仙花数题目描述 7744问题实现方法1优化方法2题目描述 余数相同问题题目描述 特殊自然数…

视频投票和图文投票之间的差异投票链接制作平台微擎投票

“我的舞台我的梦”网络评选投票_线上小程序的投票方式_视频投票的功能_在线投票程序用户在使用微信投票的时候&#xff0c;需要功能齐全&#xff0c;又快捷方便的投票小程序。而“活动星投票”这款软件使用非常的方便&#xff0c;用户可以随时使用手机微信小程序获得线上投票服…

嵌入物理(PINN)还是基于物理(AD)?

文章目录1. 传统"反演问题"1.1 反演问题是什么1.2 常见反演问题1.3 传统反演问题的困境2. 深度学习优势3. AD inversion 例子3.1 ADsurf3.2 ADseismic关于PINN的内容大家可以直接google PINN (Physical-informed neural network),其主要的目的是用一个神经网络拟合物…

Docker--------Day1

1.简介 您要如何确保应用能够在这些环境中运行和通过质量检测&#xff1f;并且在部署过程中不出现令人头疼的版本、配置问题&#xff0c;也无需重新编写代码和进行故障修复&#xff1f; Docker之所以发展如此迅速&#xff0c;也是因为它对此给出了一个标准化的解决方案-----…

Linux进程概念讲解

1、进程的基本概念在给进程下定义之前&#xff0c;我们先了解一下进程&#xff1a;我们在编写完代码并运行起来时&#xff0c;在我们的磁盘中会形成一个可执行文件&#xff0c;当我们双击这个可执行文件时&#xff08;程序时&#xff09;&#xff0c;这个程序会加载到内存中&am…

从全局变量寻找到Tomcat回显方式

前言 对于回显的获取主要是在ApplicationFilterChain类的lastServicedRequest / lastServicedResponse两个属性&#xff0c;是使用的ThreadLocal进行修饰的&#xff0c;并且&#xff0c;在执行请求的过程中&#xff0c;通过反射修改属性值&#xff0c;能够记录下当前线程的req…

camera 硬件基本知识

参考博客&#xff1a;1.【Camera专题】Qcom-你应该掌握的Camera调试技巧2_c枫_撸码的日子的博客-CSDN博客_outputpixelclock 2.浩瀚之水_csdn的博客_CSDN博客-深度学习,嵌入式Linux相关知识汇总,Caffe框架领域博主 3.一个早起的程序员的博客_CSDN博客-FPGA,PCIe应用实战,PCI-E…