当前位置: 首页 > news >正文

百万数据excel导出功能如何实现?

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

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

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

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

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

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

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

  1. 如果同步导数据,接口很容易超时。

  2. 如果把所有数据一次性装载到内存,很容易引起OOM。

  3. 数据量太大sql语句必定很慢。

  4. 相同商品编号的数据要放到一起。

  5. 如果走异步,如何通知用户导出结果?

  6. 如果excel文件太大,目标用户打不开怎么办?

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

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自带了失败重试功能,如果失败次数超过了一定的阀值,则会将该消息自动放入死信队列

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包:

<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.0.2</version>
</dependency>

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

读excel数据非常方便:

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

写excel数据也非常方便:

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

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

3.分页查询

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

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

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

public Page<User> searchUser(SearchModel searchModel) {List<User> userList = userMapper.searchUser(searchModel);Page<User> pageResponse = Page.create(userList, searchModel);pageResponse.setTotal(userMapper.searchUserCount(searchModel));return pageResponse;
}

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

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

limit #{pageStart}, #{pageSize}

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

pageStart = (pageNo - 1) * pageSize;

4.多个sheet

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

invalid row number (1048576) outside allowable range (0..1048575)

如果你想导出一百万以上的数据,excel的一个sheet肯定是存放不下的。

因此我们需要把数据保存到多个sheet中。

5.计算limit的起始位置

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

limit #{pageStart}, #{pageSize}

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

pageStart = (pageNo - 1) * pageSize;

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

例如:

ExcelWriter excelWriter = EasyExcelFactory.write(out).build();
int totalPage = searchUserTotalPage(searchModel);if(totalPage > 0) {Page<User> page = Page.create(searchModel);int sheet = (totalPage % maxSheetCount == 0) ? totalPage / maxSheetCount: (totalPage / maxSheetCount) + 1;for(int i=0;i<sheet;i++) {WriterSheet writeSheet = buildSheet(i,"sheet"+i);int startPageNo = i*(maxSheetCount/pageSize)+1;int endPageNo = (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包:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

使用起来挺方便的。

我们可以加一张专门的通知表,记录通过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导数据:

如果是使用job导数据:

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

我们按照这套方案的开发了代码,发到了pre环境,原本以为会非常顺利,但后面却还是出现了性能问题。

后来,我们用了两招轻松解决了性能问题。

相关文章:

百万数据excel导出功能如何实现?

最近我做过一个MySQL百万级别数据的excel导出功能&#xff0c;已经正常上线使用了。 这个功能挺有意思的&#xff0c;里面需要注意的细节还真不少&#xff0c;现在拿出来跟大家分享一下&#xff0c;希望对你会有所帮助。 原始需求&#xff1a;用户在UI界面上点击全部导出按钮…...

华为OD机试题,用 Java 解【合规数组】问题

最近更新的博客 华为OD机试 - 猴子爬山 | 机试题算法思路 【2023】华为OD机试 - 分糖果(Java) | 机试题算法思路 【2023】华为OD机试 - 非严格递增连续数字序列 | 机试题算法思路 【2023】华为OD机试 - 消消乐游戏(Java) | 机试题算法思路 【2023】华为OD机试 - 组成最大数…...

SAP ABAP中的数据类型 Data Types

简单来说分两种&#xff1a; 数据字典里定义的在ABAP程序里定义的 文章目录1. ABAP数据字典里的1.1 数字型的1.2 字符型1.3 字节型1.4 特殊类型2. 预定义的ABAP数据类型2.1 预定义数字型2.2 预定义字符型2.3 预定义字节型1. ABAP数据字典里的 1.1 数字型的 用在数学计算里的…...

HashMap~

HashMap&#xff1a; HashMap是面试中经常被问到的一个内容&#xff0c;以下两个经常被问到的问题&#xff0c; Question1&#xff1a;底层数据结构&#xff0c;1.7和1.8有何不同&#xff1f; 答&#xff1a;1.7数组&#xff0b;链表&#xff0c;1.8数组&#xff0b;(链表|红…...

EasyNLP集成K-Global Pointer算法,支持中文信息抽取

作者&#xff1a;周纪咏、汪诚愚、严俊冰、黄俊 导读 信息抽取的三大任务是命名实体识别、关系抽取、事件抽取。命名实体识别是指识别文本中具有特定意义的实体&#xff0c;包括人名、地名、机构名、专有名词等&#xff1b;关系抽取是指识别文本中实体之间的关系&#xff1b;…...

mysql lesson3

DQL查找语句续集.............................. 分组函数&#xff08;也叫多行处理函数&#xff09; 1&#xff1a; select sum(sal) from emp;select min(sal)from emp;select max(sal)from emp;select avg(sal)from emp;select count(ename)from emp;2&#xff1a;分组函…...

python源码保护

文章目录代码混淆打包exe编译为字节码源码加密项目发布部署时&#xff0c;为防止python源码泄漏&#xff0c;可以通过几种方式进行处理代码混淆 修改函数、变量名 打包exe 通过pyinstaller 将项目打包为exe可执行程序&#xff0c;不过容易被反编译。 编译为字节码 py_comp…...

第51讲:SQL优化之COUNT查询的优化

文章目录 1.COUNT查询优化的概念2.COUNT函数的用法1.COUNT查询优化的概念 在很多的业务场景下可能需要统计一张表中的总数据量,当表的数据量很大时,使用COUNT统计表数据量时,也是非常耗时的。 MyISAM引擎会把一个表的总行记录在磁盘中,当执行count(*)的时候会直接从磁盘中…...

ArrayBlockingQueue

同步队列超出长度时&#xff0c;不同的返回形式可以分为以下四种。 会抛异常不会抛异常&#xff0c;有返回值死等&#xff0c;直到可以插入值或者取到值设置等待超时时间添加方法add()offfer()put()offer(E e,long timeout, TimeUnit unit)删除方法remove()poll()take()poll(l…...

DeepLabV3+:对预测处理的详解

相信大家对于这一部分才是最感兴趣的&#xff0c;能够实实在在的看到效果。这里我们就只需要两个.py文件&#xff08;deeplab.py、predict_img.py&#xff09;。 创建DeeplabV3类 deeplab.py的作用是为了创建一个DeeplabV3类&#xff0c;提供一个检测图片的方法&#xff0c;而…...

【Git】与“三年经验”就差个分支操作的距离

前言 Java之父于胜军说过&#xff0c;曾经一位“三年开发经验”的程序员粉丝朋友&#xff0c;刚入职因为不会解决分支问题而被开除&#xff0c;这是不是在警示我们什么呢&#xff1f; 针对一些Git的不常用操作&#xff0c;我们通过例子来演示一遍 1.版本回退 1.1已提交但未p…...

【经验】win10设置自启动

方法一&#xff1a;自启动文件夹 按下winr快捷键&#xff0c;弹出运行窗口&#xff0c;输入&#xff1a;shell:startup&#xff0c;弹出自启动文件夹窗口&#xff0c;将要开机自启的程序或快捷方式复制到此窗口中即可。 自启动文件夹路径&#xff1a;C:\Users\【用户名】\Ap…...

Linux SPI-NAND 驱动开发指南

文章目录Linux SPI-NAND 驱动开发指南1 概述1.1 编写目的1.2 适用范围1.3 相关人员3 流程设计3.1 体系结构3.2 源码结构3.3 关键数据定义3.3.1 flash 设备信息数据结构3.3.2 flash chip 数据结构3.3.3 aw_spinand_chip_request3.3.4 ubi_ec_hdr3.3.5 ubi_vid_hdr3.4 关键接口说…...

【THREE.JS学习(3)】使用THREEJS加载GeoJSON地图数据

本文接着系列文章&#xff08;2&#xff09;进行介绍&#xff0c;以VUE2为开发框架&#xff0c;该文涉及代码存放在HelloWorld.vue中。相较于上一篇文章对div命名class等&#xff0c;该文简洁许多。<template> <div></div> </template>接着引入核心库i…...

在windows搭建Redis集群并整合入Springboot项目

搭建集群配置规划Redis集群编写bat来启动每个redis服务安装Ruby安装Redis的Ruby驱动出现错误镜像过期SSL证书过期安装集群脚本redis-trib启动每个节点并执行集群构建脚本测试搭建是否成功配置springboot项目中配置规划Redis集群 我们搭建三个节点的集群&#xff0c;每个节点有…...

C++【内存管理】

文章目录C内存管理一、C/C内存分布1.1.C/C内存区域划分图解&#xff1a;1.2.根据代码进行内存区域分析二、C内存管理方式2.1.new/delete操作内置类型2.2.new和delete操作自定义类型三、operator new与operator delete函数四、new和delete的实现原理4.1.内置类型4.2.自定义类型4…...

Spring Cloud Nacos源码讲解(六)- Nacos客户端服务发现

Nacos客户端服务发现源码分析 总体流程 首先我们先通过一个图来直观的看一下&#xff0c;Nacos客户端的服务发现&#xff0c;其实就是封装参数、调用服务接口、获得返回实例列表。 ​ 但是如果我们要是细化这个流程&#xff0c;会发现不仅包括了通过NamingService获取服务列表…...

华为OD机试题,用 Java 解【计算最大乘积】问题

最近更新的博客 华为OD机试 - 猴子爬山 | 机试题算法思路 【2023】华为OD机试 - 分糖果(Java) | 机试题算法思路 【2023】华为OD机试 - 非严格递增连续数字序列 | 机试题算法思路 【2023】华为OD机试 - 消消乐游戏(Java) | 机试题算法思路 【2023】华为OD机试 - 组成最大数…...

蓝牙运动耳机哪个好,比较好的运动蓝牙耳机

很多想选择蓝牙运动耳机的朋友都不知道应该如何选择&#xff0c;运动首先需要注意的就是耳机的防水能力以及耳机佩戴舒适度&#xff0c;在运动当中会排出大量的汗水&#xff0c;耳机防水等级做到越高&#xff0c;可以更好地保护耳机不受汗水浸湿&#xff0c;下面就分享五款适合…...

苹果设计可变色Apple Watch表带,智能穿戴玩法多

苹果最新技术专利显示&#xff0c;苹果正在为 Apple Watch 设计一款可变色的表带&#xff0c;可以根据佩戴者所穿着的服装、所在的环境等自动改变颜色。据介绍&#xff0c;这款表带里的灯丝具有电致变色功能&#xff0c;可以通过施加不同的电压&#xff0c;来实现显示多种颜色或…...

浅谈 React Hooks

React Hooks 是 React 16.8 引入的一组 API&#xff0c;用于在函数组件中使用 state 和其他 React 特性&#xff08;例如生命周期方法、context 等&#xff09;。Hooks 通过简洁的函数接口&#xff0c;解决了状态与 UI 的高度解耦&#xff0c;通过函数式编程范式实现更灵活 Rea…...

装饰模式(Decorator Pattern)重构java邮件发奖系统实战

前言 现在我们有个如下的需求&#xff0c;设计一个邮件发奖的小系统&#xff0c; 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式&#xff08;Decorator Pattern&#xff09;允许向一个现有的对象添加新的功能&#xff0c;同时又不改变其…...

(十)学生端搭建

本次旨在将之前的已完成的部分功能进行拼装到学生端&#xff0c;同时完善学生端的构建。本次工作主要包括&#xff1a; 1.学生端整体界面布局 2.模拟考场与部分个人画像流程的串联 3.整体学生端逻辑 一、学生端 在主界面可以选择自己的用户角色 选择学生则进入学生登录界面…...

线程与协程

1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指&#xff1a;像函数调用/返回一样轻量地完成任务切换。 举例说明&#xff1a; 当你在程序中写一个函数调用&#xff1a; funcA() 然后 funcA 执行完后返回&…...

家政维修平台实战20:权限设计

目录 1 获取工人信息2 搭建工人入口3 权限判断总结 目前我们已经搭建好了基础的用户体系&#xff0c;主要是分成几个表&#xff0c;用户表我们是记录用户的基础信息&#xff0c;包括手机、昵称、头像。而工人和员工各有各的表。那么就有一个问题&#xff0c;不同的角色&#xf…...

高危文件识别的常用算法:原理、应用与企业场景

高危文件识别的常用算法&#xff1a;原理、应用与企业场景 高危文件识别旨在检测可能导致安全威胁的文件&#xff0c;如包含恶意代码、敏感数据或欺诈内容的文档&#xff0c;在企业协同办公环境中&#xff08;如Teams、Google Workspace&#xff09;尤为重要。结合大模型技术&…...

网络编程(UDP编程)

思维导图 UDP基础编程&#xff08;单播&#xff09; 1.流程图 服务器&#xff1a;短信的接收方 创建套接字 (socket)-----------------------------------------》有手机指定网络信息-----------------------------------------------》有号码绑定套接字 (bind)--------------…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

Aspose.PDF 限制绕过方案:Java 字节码技术实战分享(仅供学习)

Aspose.PDF 限制绕过方案&#xff1a;Java 字节码技术实战分享&#xff08;仅供学习&#xff09; 一、Aspose.PDF 简介二、说明&#xff08;⚠️仅供学习与研究使用&#xff09;三、技术流程总览四、准备工作1. 下载 Jar 包2. Maven 项目依赖配置 五、字节码修改实现代码&#…...

NXP S32K146 T-Box 携手 SD NAND(贴片式TF卡):驱动汽车智能革新的黄金组合

在汽车智能化的汹涌浪潮中&#xff0c;车辆不再仅仅是传统的交通工具&#xff0c;而是逐步演变为高度智能的移动终端。这一转变的核心支撑&#xff0c;来自于车内关键技术的深度融合与协同创新。车载远程信息处理盒&#xff08;T-Box&#xff09;方案&#xff1a;NXP S32K146 与…...