0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

SpringBoot实现Excel导入导出,百万数据量,性能爆表!

jf_ro2CN3Fa 来源:芋道源码 2023-02-16 09:50 次阅读


前言

最近我做过一个MySQL百万级别数据的excel导出功能,已经正常上线使用了。

这个功能挺有意思的,里面需要注意的细节还真不少,现在拿出来跟大家分享一下,希望对你会有所帮助。

原始需求:用户在UI界面上点击全部导出按钮,就能导出所有商品数据。

咋一看,这个需求挺简单的。

但如果我告诉你,导出的记录条数,可能有一百多万,甚至两百万呢?

这时你可能会倒吸一口气。

因为你可能会面临如下问题:

  1. 如果同步导数据,接口很容易超时。
  2. 如果把所有数据一次性装载到内存,很容易引起OOM。
  3. 数据量太大sql语句必定很慢。
  4. 相同商品编号的数据要放到一起。
  5. 如果走异步,如何通知用户导出结果?
  6. 如果excel文件太大,目标用户打不开怎么办?

我们要如何才能解决这些问题,实现一个百万级别的excel数据快速导出功能呢?

ec618bc4-ad3d-11ed-bfe3-dac502259ad0.png

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

1.异步处理

做一个MySQL百万数据级别的excel导出功能,如果走接口同步导出,该接口肯定会非常容易超时

因此,我们在做系统设计的时候,第一选择应该是接口走异步处理。

说起异步处理,其实有很多种,比如:使用开启一个线程,或者使用线程池,或者使用job,或者使用mq等。

为了防止服务重启时数据的丢失问题,我们大多数情况下,会使用job或者mq来实现异步功能。

1.1 使用job

如果使用job的话,需要增加一张执行任务表,记录每次的导出任务。

用户点击全部导出按钮,会调用一个后端接口,该接口会向表中写入一条记录,该记录的状态为:待执行

有个job,每隔一段时间(比如:5分钟),扫描一次执行任务表,查出所有状态是待执行的记录。

然后遍历这些记录,挨个执行。

需要注意的是:如果用job的话,要避免重复执行的情况。比如job每隔5分钟执行一次,但如果数据导出的功能所花费的时间超过了5分钟,在一个job周期内执行不完,就会被下一个job执行周期执行。

所以使用job时可能会出现重复执行的情况。

为了防止job重复执行的情况,该执行任务需要增加一个执行中的状态。

具体的状态变化如下:

  1. 执行任务被刚记录到执行任务表,是待执行状态。
  2. 当job第一次执行该执行任务时,该记录再数据库中的状态改为:执行中
  3. 当job跑完了,该记录的状态变成:完成失败

这样导出数据的功能,在第一个job周期内执行不完,在第二次job执行时,查询待处理状态,并不会查询出执行中状态的数据,也就是说不会重复执行。

此外,使用job还有一个硬伤即:它不是立马执行的,有一定的延迟。

如果对时间不太敏感的业务场景,可以考虑使用该方案。

1.2 使用mq

用户点击全部导出按钮,会调用一个后端接口,该接口会向mq服务端,发送一条mq消息

有个专门的mq消费者,消费该消息,然后就可以实现excel的数据导出了。

相较于job方案,使用mq方案的话,实时性更好一些。

对于mq消费者处理失败的情况,可以增加补偿机制,自动发起重试

RocketMQ自带了失败重试功能,如果失败次数超过了一定的阀值,则会将该消息自动放入死信队列

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

2.使用easyexcel

我们知道在Java中解析和生成Excel,比较有名的框架有Apache POIjxl

但它们都存在一个严重的问题就是:非常耗内存,POI有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。

百万级别的excel数据导出功能,如果使用传统的Apache POI框架去处理,可能会消耗很大的内存,容易引发OOM问题。

easyexcel重写了POI对07版Excel的解析,之前一个3M的excel用POI sax解析,需要100M左右内存,如果改用easyexcel可以降低到几M,并且再大的Excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便。

需要在mavenpom.xml文件中引入easyexcel的jar包:


com.alibaba
easyexcel
3.0.2

之后,使用起来非常方便。

读excel数据非常方便:

@Test
publicvoidsimpleRead(){
StringfileName=TestFileUtil.getPath()+"demo"+File.separator+"demo.xlsx";
//这里需要指定读用哪个class去读,然后读取第一个sheet文件流会自动关闭
EasyExcel.read(fileName,DemoData.class,newDemoDataListener()).sheet().doRead();
}

写excel数据也非常方便:

@Test
publicvoidsimpleWrite(){
StringfileName=TestFileUtil.getPath()+"write"+System.currentTimeMillis()+".xlsx";
//这里需要指定写用哪个class去读,然后写到第一个sheet,名字为模板然后文件流会自动关闭
//如果这里想使用03则传入excelType参数即可
EasyExcel.write(fileName,DemoData.class).sheet("模板").doWrite(data());
}

easyexcel能大大减少占用内存的主要原因是:在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析。

3.分页查询

百万级别的数据,从数据库一次性查询出来,是一件非常耗时的工作。

即使我们可以从数据库中一次性查询出所有数据,没出现连接超时问题,这么多的数据全部加载到应用服务的内存中,也有可能会导致应用服务出现OOM问题。

因此,我们从数据库中查询数据时,有必要使用分页查询。比如:每页5000条记录,分为200页查询。

publicPagesearchUser(SearchModelsearchModel){
ListuserList=userMapper.searchUser(searchModel);
PagepageResponse=Page.create(userList,searchModel);
pageResponse.setTotal(userMapper.searchUserCount(searchModel));
returnpageResponse;
}

每页大小pageSize和页码pageNo,是SearchModel类中的成员变量,在创建searchModel对象时,可以设置设置这两个参数。

然后在Mybatis的sql文件中,通过limit语句实现分页功能:

limit#{pageStart},#{pageSize}

其中的pagetStart参数,是通过pageNo和pageSize动态计算出来的,比如:

pageStart=(pageNo-1)*pageSize;

4.多个sheet

我们知道,excel对一个sheet存放的最大数据量,是有做限制的,一个sheet最多可以保存1048576行数据。否则在保存数据时会直接报错:

invalidrownumber(1048576)outsideallowablerange(0..1048575)

如果你想导出一百万以上的数据,excel的一个sheet肯定是存放不下的。ec7f1d7e-ad3d-11ed-bfe3-dac502259ad0.png

因此我们需要把数据保存到多个sheet中。ec967a78-ad3d-11ed-bfe3-dac502259ad0.png

5.计算limit的起始位置

我之前说过,我们一般是通过limit语句来实现分页查询功能的:

limit#{pageStart},#{pageSize}

其中的pagetStart参数,是通过pageNo和pageSize动态计算出来的,比如:

pageStart=(pageNo-1)*pageSize;

如果只有一个sheet可以这么玩,但如果有多个sheet就会有问题。因此,我们需要重新计算limit的起始位置。

例如:

ExcelWriterexcelWriter=EasyExcelFactory.write(out).build();
inttotalPage=searchUserTotalPage(searchModel);

if(totalPage>0){
Pagepage=Page.create(searchModel);
intsheet=(totalPage%maxSheetCount==0)?totalPage/maxSheetCount:(totalPage/maxSheetCount)+1;
for(inti=0;i"sheet"+i);
intstartPageNo=i*(maxSheetCount/pageSize)+1;
intendPageNo=(i+1)*(maxSheetCount/pageSize);
while(page.getPageNo()>=startPageNo&&page.getPageNo()<=endPageNo) {
        page = searchUser(searchModel);
        if(CollectionUtils.isEmpty(page.getList())){
break;
}

excelWriter.write(page.getList(),writeSheet);
page.setPageNo(page.getPageNo()+1);
}
}
}

这样就能实现分页查询,将数据导出到不同的excel的sheet当中。

6.文件上传到OSS

由于现在我们导出excel数据的方案改成了异步,所以没法直接将excel文件,同步返回给用户。

因此我们需要先将excel文件存放到一个地方,当用户有需要时,可以访问到。

这时,我们可以直接将文件上传到OSS文件服务器上。

通过OSS提供的上传接口,将excel上传成功后,会返回文件名称访问路径

我们可以将excel名称和访问路径保存到中,这样的话,后面就可以直接通过浏览器,访问远程excel文件了。

而如果将excel文件保存到应用服务器,可能会占用比较多的磁盘空间

一般建议将应用服务器文件服务器分开,应用服务器需要更多的内存资源或者CPU资源,而文件服务器需要更多的磁盘资源

7.通过WebSocket推送通知

通过上面的功能已经导出了excel文件,并且上传到了OSS文件服务器上。

接下来的任务是要本次excel导出结果,成功还是失败,通知目标用户。

有种做法是在页面上提示:正在导出excel数据,请耐心等待

然后用户可以主动刷新当前页面,获取本地导出excel的结果。

但这种用户交互功能,不太友好。

还有一种方式是通过webSocket建立长连接,进行实时通知推送。

如果你使用了SpringBoot框架,可以直接引入webSocket的相关jar包:


org.springframework.boot
spring-boot-starter-websocket

使用起来挺方便的。

我们可以加一张专门的通知表,记录通过webSocket推送的通知的标题、用户、附件地址、阅读状态、类型等信息

能更好的追溯通知记录。

webSocket给客户端推送一个通知之后,用户的右上角的收件箱上,实时出现了一个小窗口,提示本次导出excel功能是成功还是失败,并且有文件下载链接。

当前通知的阅读状态是未读

用户点击该窗口,可以看到通知的详细内容,然后通知状态变成已读

8.总条数可配置

我们在做导百万级数据这个需求时,是给用户用的,也有可能是给运营同学用的。

其实我们应该站在实际用户的角度出发,去思考一下,这个需求是否合理。

用户拿到这个百万级别的excel文件,到底有什么用途,在他们的电脑上能否打开该excel文件,电脑是否会出现太大的卡顿了,导致文件使用不了。

如果该功能上线之后,真的发生发生这些情况,那么导出excel也没有啥意义了。

因此,非常有必要把记录的总条数,做成可配置的,可以根据用户的实际情况调整这个配置。

比如:用户发现excel中有50万的数据,可以正常访问和操作excel,这时候我们可以将总条数调整成500000,把多余的数据截取掉。

其实,在用户的操作界面,增加更多的查询条件,用户通过修改查询条件,多次导数据,可以实现将所有数据都导出的功能,这样可能更合理一些。

此外,分页查询时,每页的大小,也建议做成可配置的。

通过总条数和每页大小,可以动态调整记录数量和分页查询次数,有助于更好满足用户的需求。

9.order by商品编号

之前的需求是要将相同商品编号的数据放到一起。

例如:

编号 商品名称 仓库名称 价格
1 笔记本 北京仓 7234
1 笔记本 上海仓 7235
1 笔记本 武汉仓 7236
2 平板电脑 成都仓 7236
2 平板电脑 大连仓 3339

但我们做了分页查询的功能,没法将数据一次性查询出来,直接在Java内存中分组或者排序。

因此,我们需要考虑在sql语句中使用order by 商品编号,先把数据排好顺序,再查询出数据,这样就能将相同商品编号,仓库不同的数据放到一起。

此外,还有一种情况需要考虑一下,通过配置的总记录数将全部数据做了截取。

但如果最后一个商品编号在最后一页中没有查询完,可能会导致导出的最后一个商品的数据不完整。

因此,我们需要在程序中处理一下,将最后一个商品删除。

但加了order by关键字进行排序之后,如果查询sql中join了很多张表,可能会导致查询性能变差。

那么,该怎么办呢?

总结

最后用两张图,总结一下excel异步导数据的流程。

如果是使用mq导数据:ecaf41de-ad3d-11ed-bfe3-dac502259ad0.png

如果是使用job导数据:ecc3278a-ad3d-11ed-bfe3-dac502259ad0.png

这两种方式都可以,可以根据实际情况选择使用。



审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • Excel
    +关注

    关注

    4

    文章

    218

    浏览量

    55443
  • MySQL
    +关注

    关注

    1

    文章

    801

    浏览量

    26433
  • 线程
    +关注

    关注

    0

    文章

    504

    浏览量

    19646
  • SpringBoot
    +关注

    关注

    0

    文章

    173

    浏览量

    167

原文标题:SpringBoot 实现 Excel 导入导出,百万数据量,性能爆表!

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    百万级别excel导出功能如何实现

    最近我做过一个MySQL 百万级别 数据excel 导出功能,已经正常上线使用了。 这个功能挺有意思的,里面需要注意的细节还真不少,现在拿出来跟大家分享一下,希望对你会有所帮助。
    的头像 发表于 09-25 11:38 1026次阅读
    <b class='flag-5'>百万</b>级别<b class='flag-5'>excel</b><b class='flag-5'>导出</b>功能如何<b class='flag-5'>实现</b>

    在LabVIEW中导入导出Excel文件程序设计

    LabVIEW Report Generation工具包的基础上,以示例方式描述了在LabVIEW开发环境中利用报表生成器函数,实现导入Excel文件和导出
    发表于 10-25 14:58

    labview如何导入导出Excel,希望能给个示例或者模板参考!

    labview如何导入导出Excel,希望能给个示例或者模板参考!比较急在做课设!!
    发表于 11-12 16:57

    列表存储数据以及导出Excel表格中

    想做一个列表,显示存储的数据,然后还能导入Excel表格里想要实现如下功能:1. 20个数据存成一行,一列代表一个变量。第一列为存储时的时
    发表于 03-15 09:56

    数据怎么导入excel

    数据库信息导入excel表格,以及excel导入数据
    发表于 11-08 09:23

    有个excel数据表导入鸿蒙数据库,求sqlite数据使用文档?

    有个excel 数据表导入鸿蒙数据库, 求sqlite数据使用文档?
    发表于 06-09 10:10

    基于Java反射机制的Excel文件导出实现_杨敏煜

    基于Java反射机制的Excel文件导出实现_杨敏煜
    发表于 03-18 09:46 1次下载

    组态王历史数据导出EXCEL表格的方法

    怎么把组态王数据导出变成excel格式
    发表于 03-13 17:29 15次下载

    如何用一个公用工具来进行Excel导入导出

    日常在做后台系统的时候会很频繁的遇到Excel导入导出的问题,正好这次在做一个后台系统,就想着写一个公用工具来进行Excel导入
    的头像 发表于 08-20 09:33 2692次阅读
    如何用一个公用工具来进行<b class='flag-5'>Excel</b>的<b class='flag-5'>导入</b><b class='flag-5'>导出</b>

    MACSV数据导出导入的方法

    MACSV数据导出导入的方法(现代电源技术期末考试)-文档为MACSV数据导出导入的方法
    发表于 09-17 15:41 2次下载
    MACSV<b class='flag-5'>数据</b>库<b class='flag-5'>导出</b>、<b class='flag-5'>导入</b>的方法

    如何写一个公用工具来进行Excel导入导出

    日常在做后台系统的时候会很频繁的遇到Excel导入导出的问题,正好这次在做一个后台系统,就想着写一个公用工具来进行Excel导入
    的头像 发表于 10-09 14:19 1438次阅读

    百万数据导入导出解决方案

    前景 1 传统POI的的版本优缺点比较 2 使用方式哪种看情况 3 百万数据导入导出(正菜) 4 总结 前景 在项目开发中往往需要使用到数据
    的头像 发表于 10-11 17:19 1281次阅读

    数据从Arduino导出Excel工作

    电子发烧友网站提供《将数据从Arduino导出Excel工作.zip》资料免费下载
    发表于 12-07 09:19 1次下载
    将<b class='flag-5'>数据</b>从Arduino<b class='flag-5'>导出</b>到<b class='flag-5'>Excel</b>工作<b class='flag-5'>表</b>

    SpringBoot实现MySQL百万数据量导出并避免OOM的解决方案

    加载不可行,那我们的目标就是如何实现数据的分批加载了。实事上,Mysql本身支持Stream查询,我们可以通过Stream流获取数据,然后将数据
    的头像 发表于 03-16 13:50 2443次阅读

    excel导出功能如何实现

    最近我做过一个MySQL`百万级别`数据的`excel`导出功能,已经正常上线使用了。 这个功能挺有意思的,里面需要注意的细节还真不少,现在拿出来跟大家分享一下,希望对你会有所帮
    的头像 发表于 05-11 18:17 1184次阅读
    <b class='flag-5'>excel</b><b class='flag-5'>导出</b>功能如何<b class='flag-5'>实现</b>?