【实战】EasyExcel实现百万级数据导入导出

文章目录

    • 前言
    • 技术积累
    • 实战演示
      • 实现思路
      • 模拟代码
      • 测试结果

在这里插入图片描述

前言

最近接到一个百万级excel数据导入导出的需求,大概就是我们在进行公众号API群发的时候,需要支持500w以上的openid进行群发,并且可以提供发送openid数据的导出功能。可能有的同学会说,这么大的数据量发送为啥不用标签发送呢。哈哈,标签发送需要提前打标签微信限制50个一批,我们开10个线程也是需要3个小时左右才能打完,这样肯定不能满足客户需求。如果用openid群发就不一样了,微信支持10000个每批,基本上我开5个线程同时发送差不多几分钟搞定。所以,问题就来到了百万级excel数据的导入与导出啦。

技术积累

EasyExcel是什么
EasyExcel是一个基于Java的、快速、简洁、解决大文件内存溢出的Excel处理工具。他能让你在不用考虑性能、内存的等因素的情况下,快速完成Excel的读、写等功能。

作为一个资深的搬砖人,秉承着能够用CV大法,绝不自己造轮子的原则,我肯定选择这个阿里开源的excel读写工具来开发功能。

使用案例
对于excel的读、写、填充都有简单的案例,有兴趣的同学可以自己去看,这里不再重复叙述。
https://easyexcel.opensource.alibaba.com/docs/current/quickstart/read

实战演示

相信有很多的同学都使用过Easyexcel这个开源中间件,基本上很多的管理系统项目做到导出导入功能都会使用这个中间件。按照目前使用的情况来看,这个中间件还是比较稳定的,而且这个开源社区的活跃度也是比较高,基本上很难遇到不能解决的问题。

对于简单的excel导入导出我们直接安装Easyexcel提供的Demo就能够完美搞定,但是遇到比较大的数据量的时候就需要我们特殊处理下业务逻辑了。比如今天的重点百万级数据的导入导出。

实现思路

1、由于Easyexcel读功能是对excel一行一行进行读的,这是为了保证不过多占用我们内存。如果我们系统需要对数据进行入库的话则需要对数据进行缓存,比如1w每批次入库。虽然会损失一定的内存,但是写库时间大大降低了呀;
2、如果传入的excel有多个sheet,可以考虑开启多个线程进行读excel。比如每个sheet一个线程,但是线程需要做好管理,如使用线程池等等。但是,一般大数据量都不使用excel来保存,而是使用csv来储存数据,因为这个格式简单、体积小、易于使用、可被多种软件打开和编辑。当然,Easyexel也是可以读取csv文件的,但因为要兼容csv文件就不采用多sheet的方式,因为csv没有sheet。

EasyExcel.read(filePath, Object.class, new PageReadListener<Object>(dataList ->{//TODO 数据处理,默认读取0号sheet
}, 10000)).sheet().doRead();

3、excel导出目前Easyexcel最新版本是不支持多线程写数据的,只能单线程进行写excel。为了保证写excel效率,我采用20w数据一批一次写入excel。
4、由于excel数据行数超过100w打开时间特别长,所以我们在导出的时候对数据进行切割,每个sheet最多只保存100w数据,其他数据写入下个sheet。
5、为了保证我们每批次可以写入20w数据到excel,那么,我必须保证能够用最短的时间从数据库抓取20w数据。这里我们可以采用多线程每个线程去拉5w条,开启4个线程足够,然后用countdownlatch进行多线程处理。当然,如果内存足够可以一次从数据库拉取20w数据,其实也不大最多也才几兆而已。

//需要导出的总数量
int total = count(*)
//每次读20w数据
int readNum = 20 * 10000;
//每个sheet总数据
int sheetDataNum = 100 * 10000;
//需要写入sheetNum
int sheetNum = total % sheetDataNum == 0 ? total / sheetDataNum : (total / sheetDataNum)+1;
//计算每个线程查询数据库次数
int queryNum = sheetDataNum / readNum;
//最后一个线程查询数据库次数
int lastQueryNum = total % sheetDataNum == 0 ? queryNum: (total % sheetDataNum % readNum == 0 ? (total % sheetDataNum / readNum) : (total % sheetDataNum / readNum + 1));
//导出逻辑
for (int i = 0; i < sheetNum; i++) {final int finalI = i;new Runnable() {@Overridepublic void run() {//查询数据数据for (int j = 0; j < ((finalI < sheetNum -1) ? queryNum : lastQueryNum); j++) {//查询数据库int page = j+1+finalI * sheetDataNum;int pageSize = readNum;//TODO 调用数据库查询//TODO 写excel}}};
}

模拟代码

数据库创建一个公众号用户表

-- 创建一个缓存openid的数据库
drop table if exists mp_user;
create table mp_user(id bigint   not null auto_increment comment 'ID',openid varchar(64) not null comment 'openid',deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',primary key (`id`) using btree
) engine = innodb default charset=utf8mb4 comment '公众号粉丝';

引入maven依赖

<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.3.3</version>
</dependency>
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.0</version>
</dependency>
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.33</version>
</dependency>

application配置数据库

spring:datasource:url: jdbc:mysql://localhost:3306/cce-demo?serverTimezone=GMT%2B8&autoReconnect=false&useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=trueusername: rootpassword: 12345678driver-class-name: com.mysql.cj.jdbc.Driver# MyBatis Plus 的配置项
mybatis-plus:configuration:map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启sql日志global-config:db-config:id-type: NONElogic-delete-value: 1 # 逻辑已删除值(默认为 1)logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)banner: false # 关闭控制台的 Banner 打印type-aliases-package: com.example.ccedemo.entitymapper-locations: classpath:/mapper/*.xml    

MpUser实体和excel类

/*** MpUser* @author senfel* @version 1.0* @date 2024/7/1 16:17*/
@TableName("mp_user")
@KeySequence("mp_user_seq")
@Data
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MpUser implements Serializable {@TableIdprivate Long id;/*** openid*/private String openid;private Boolean deleted;}
/*** openId Excel 导入 VO* @author senfel*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = false)
public class OpenIdImportExcelVo {/*** 用户OpenId*/@ExcelProperty(index = 0)private String openid;
}MpUserMapper
/*** MpUserMapper* @author senfel* @version 1.0* @date 2024/7/1 16:23*/
@Mapper
public interface MpUserMapper extends BaseMapper<MpUser> {/*** insertBatch* @param list* @author senfel* @date 2024/7/1 17:16* @return int*/int insertBatch(List<OpenIdImportExcelVo> list);/*** selectDataByPage* @param offset* @param size* @author senfel* @date 2024/7/1 17:16* @return java.util.List<java.lang.String>*/List<OpenIdImportExcelVo> selectDataByPage(int offset, int size);
}

MpUserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.ccedemo.mapper.MpUserMapper"><!--批量新增openid--><insert id="insertBatch" parameterType="com.example.ccedemo.excel.OpenIdImportExcelVo">insert into mp_user (openid) values<foreach collection="list" item="item" separator=",">(#{item.openid})</foreach></insert><!--查询分页数据--><select id="selectDataByPage" parameterType="map" resultType="com.example.ccedemo.excel.OpenIdImportExcelVo">select openid from mp_userlimit #{offset},#{size}</select></mapper>

提供测试类

/*** EasyExcelTest* @author senfel* @version 1.0* @date 2024/7/1 16:03*/
@SpringBootTest
public class EasyExcelTest {@Resourceprivate MpUserMapper mpUserMapper;/*** readExcel* @author senfel* @date 2024/7/1 17:17* @return void*/@Testpublic void readExcel(){//800w+的csv文件,每批次读取10000条long startTime = System.currentTimeMillis();System.err.println("readExcel开始执行时间:"+startTime);String filePath = "D:\\blank\\工作簿1.csv";EasyExcel.read(filePath, OpenIdImportExcelVo.class, new PageReadListener<OpenIdImportExcelVo>(dataList ->{if(!CollectionUtils.isEmpty(dataList)){//数据存储mpUserMapper.insertBatch(dataList);}}, 10000)).sheet().doRead();System.err.println("readExcel结束执行时间:"+(System.currentTimeMillis()-startTime));}/*** exportExcel* @author senfel* @date 2024/7/1 17:19* @return void*/@Testpublic void exportExcel(){long startTime = System.currentTimeMillis();System.err.println("exportExcel:"+startTime);String excelName = "测试导出openid";String exportPath = "D:\\blank\\";boolean isRun = true;int size = 20 * 10000;int page = 0;int sheetDataSize = 0;int sheetNo = 0;FileOutputStream outputStream = null;try {outputStream = new FileOutputStream(exportPath + excelName + ".xlsx");ExcelWriter excelWriter = EasyExcel.write(outputStream).build();do {page++;List<OpenIdImportExcelVo> openList = mpUserMapper.selectDataByPage((page - 1) * size, size);if(CollectionUtils.isEmpty(openList)){isRun = false;break;}sheetDataSize += openList.size();if(sheetDataSize > 1000000){sheetNo++;sheetDataSize = openList.size();}//写入文件流WriteSheet writeSheet = EasyExcel.writerSheet(sheetNo, "openid"+sheetNo).head(OpenIdImportExcelVo.class).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build();excelWriter.write(openList, writeSheet);}while (isRun);excelWriter.finish();outputStream.flush();}catch (IOException e){e.printStackTrace();}finally {if (outputStream != null) {try {outputStream.close();} catch (IOException e) {e.printStackTrace();}}System.err.println("exportExcel结束执行时间:"+(System.currentTimeMillis()-startTime));}}
}

测试结果

导入结果
在这里插入图片描述

83s导入存入数据库 838w数据,如果改为原生的JDBC操作入库会更快。

导出结果
在这里插入图片描述

77s导出写入excel 838w数据,写excel不建议多线程。如果受到内存限制查询条数低于20w可以考虑多线程执行,但是写excel必须单线程。

如果需要导出到响应头HttpServletResponse

public void exportExcel2(HttpServletResponse response){long startTime = System.currentTimeMillis();String excelName = "测试导出openid";boolean isRun = true;int size = 20 * 10000;int page = 0;int sheetDataSize = 0;int sheetNo = 0;OutputStream outputStream = null;try {outputStream = response.getOutputStream();ExcelWriter excelWriter = EasyExcel.write(outputStream).build();do {page++;List<OpenIdImportExcelVo> openList = mpUserMapper.selectDataByPage((page - 1) * size, size);if(CollectionUtils.isEmpty(openList)){isRun = false;break;}sheetDataSize += openList.size();if(sheetDataSize > 1000000){sheetNo++;sheetDataSize = openList.size();}//写入文件流WriteSheet writeSheet = EasyExcel.writerSheet(sheetNo, "openid"+sheetNo).head(OpenIdImportExcelVo.class).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build();excelWriter.write(openList, writeSheet);}while (isRun);// 下载EXCELresponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setCharacterEncoding("utf-8");String fileName = URLEncoder.encode(excelName, "UTF-8").replaceAll("\\+", "%20");response.setHeader("Content-disposition", "attachment;filename*=" + fileName + ".xlsx");excelWriter.finish();outputStream.flush();}catch (IOException e){e.printStackTrace();throw new RuntimeException("exportExcel异常,具体信息为:"+e.getMessage());}finally {if (outputStream != null) {try {outputStream.close();} catch (IOException e) {e.printStackTrace();}}System.err.println("exportExcel结束执行时间:"+(System.currentTimeMillis()-startTime));}
}

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

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

相关文章

002-基于Sklearn的机器学习入门:基本概念

本节将继续介绍与机器学习有关的一些基本概念&#xff0c;包括机器学习的分类&#xff0c;性能指标等。同样&#xff0c;如果你对本节内容很熟悉&#xff0c;可直接跳过。 2.1 机器学习概述 2.1.1 什么是机器学习 常见的监督学习方法 2.1.2 机器学习的分类 机器学习一般包括监…

C++初学者指南-3.自定义类型(第一部分)-析构函数

C初学者指南-3.自定义类型(第一部分)-析构函数 文章目录 C初学者指南-3.自定义类型(第一部分)-析构函数特殊的成员函数用户定义的构造函数和析构函数RAII示例&#xff1a;资源处理示例&#xff1a;RAII记录零规则 特殊的成员函数 T::T()默认构造函数当创建新的 T 对象时运行。…

配置WLAN 示例

规格 仅AR129CVW、AR129CGVW-L、AR109W、AR109GW-L、AR161W、AR161EW、AR161FGW-L、AR161FW、AR169FVW、AR169JFVW-4B4S、AR169JFVW-2S、AR169EGW-L、AR169EW、AR169FGW-L、AR169W-P-M9、AR1220EVW和AR301W支持WLAN-FAT AP功能。 组网需求 如图1所示&#xff0c;企业使用WLAN…

C++(第一天-----命名空间和引用)

一、C/C的区别 1、与C相比   c语言面向过程&#xff0c;c面向对象。   c能够对函数进行重载&#xff0c;可使同名的函数功能变得更加强大。   c引入了名字空间&#xff0c;可以使定义的变量名更多。   c可以使用引用传参&#xff0c;引用传参比起指针传参更加快&#…

基于YOLOv9+pyside的安检仪x光危险物物品检测(有ui)

安全检查在公共场所确保人身安全的关键环节&#xff0c;不可或缺。X光安检机作为必要工具&#xff0c;在此过程中发挥着重要作用。然而&#xff0c;其依赖人工监控和判断成像的特性限制了其应用效能。本文以此为出发点&#xff0c;探索了基于Torch框架的YOLO算法在安检X光图像中…

Xcode安装Simulator失败问题解决方法

Xcode安装Simulator_Runtime失败&#xff0c;安装包离线安装保姆级教程 Xcode更新之后有时候会提示要安装模拟器运行时环境&#xff0c;但是用Xcode更新会因为网络原因&#xff0c;我觉得基本上就是因为苹果服务器的连接不稳定导致的&#xff0c;更可气的是不支持断点续…

【论文阅读】--Popup-Plots: Warping Temporal Data Visualization

弹出图&#xff1a;扭曲时态数据可视化 摘要1 引言2 相关工作3 弹出图3.1 椭球模型3.1.1 水平轨迹3.1.2 垂直轨迹3.1.3 组合轨迹 3.2 视觉映射与交互 4 实施5 结果6 评估7 讨论8 结论和未来工作致谢参考文献 期刊: IEEE Trans. Vis. Comput. Graph.&#xff08;发表日期: 2019&…

DICOM灰度图像、彩色图像的窗宽、窗位与像素的最大最小值的换算关系?

图像可以调整窗宽、窗位 dicom图像中灰度图像可以调整窗宽、窗位&#xff0c;RGB图像调整亮度或对比度&#xff1f;_灰度 图 调节窗宽-CSDN博客 窗宽、窗位与像素的最大最小值的换算关系? 换算公式 max-minWindowWidth; (maxmin)/2WindowCenter; 详细解释 窗宽&#xff0…

视频太大怎么压缩变小?6款视频压缩软件免费版分享

视频太大怎么压缩得又小又清晰呢&#xff1f;无论是视频文件传输、视频文件存储&#xff0c;还是进行自媒体视频上传&#xff0c;都对视频文件的大小有一定的限制。高质量的视频文件往往伴随着文件占据大量存储空间&#xff0c;导致文件传输速度变慢。今天教大家6种视频压缩软件…

试用笔记之-汇通来电显示软件

首先汇通来电显示软件下载 http://www.htsoft.com.cn/download/httelephone.rar

IP白名单及其作用解析

在网络安全领域&#xff0c;IP白名单是一项至关重要的策略&#xff0c;它允许特定的IP地址或地址范围访问网络资源&#xff0c;从而确保只有受信任的终端能够连接。下面&#xff0c;我们将深入探讨IP白名单的定义、作用以及实施时的关键考虑因素。 一、IP白名单的定义 IP白名单…

深度学习21-30

1.池化层作用&#xff08;筛选、过滤、压缩&#xff09; h和w变为原来的1/2&#xff0c;64是特征图个数保持不变。 每个位置把最大的数字取出来 用滑动窗口把最大的数值拿出来&#xff0c;把44变成22 2.卷积神经网络 &#xff08;1&#xff09;conv&#xff1a;卷积进行特征…

stm32学习笔记---USART串口协议(理论部分)

目录 通信 通信的目的 通信协议 STM32的通信协议 各种协议的通信引脚介绍 通信空间和时间 时钟特性 电平特性 设备特性 串口通信 硬件电路 电平标准 串口参数及时序 时序 串口的参数 串口通信的实际波形 声明&#xff1a;本专栏是本人跟着B站江科大的视频的学习…

Vue 项目部署为 HTTPS 站点

&#x1f9d1;‍&#x1f393; 个人主页&#xff1a;爱蹦跶的大A阿 &#x1f525;当前正在更新专栏&#xff1a;《JavaScript保姆级教程》、《VUE》、《Krpano》 ✨ 前言 在将 Vue 项目部署为 HTTPS 站点时&#xff0c;你需要配置 HTTPS 证书和服务器。以下是一个基本的步骤和…

深入解析 androidx.databinding.BaseObservable

在现代 Android 开发中&#xff0c;数据绑定 (Data Binding) 是一个重要的技术&#xff0c;它简化了 UI 和数据之间的交互。在数据绑定框架中&#xff0c;androidx.databinding.BaseObservable 是一个关键类&#xff0c;用于实现可观察的数据模型。本文将详细介绍 BaseObservab…

java 代码块

Java中的代码块主要有三种类型&#xff1a;普通代码块、静态代码块、构造代码块。它们的用途和执行时机各不相同。 普通代码块&#xff1a;在方法内部定义&#xff0c;使用一对大括号{}包围的代码片段。它的作用域限定在大括号内&#xff0c;每当程序执行到该代码块时就会执行其…

Pikachu 不安全的文件下载(Unsafe file download)概述 附漏洞利用案例

目录 获取下载链接 修改链接 重新构造链接 拓展 不安全的文件下载概述 文件下载功能在很多web系统上都会出现&#xff0c;一般我们当点击下载链接&#xff0c;便会向后台发送一个下载请求&#xff0c;一般这个请求会包含一个需要下载的文件名称&#xff0c;后台在收到请求…

初识Java(复习版)

一. 什么是Java Java是一种面向对象的编程语言&#xff0c;和C语言有所不同&#xff0c;C语言是一门面向过程的语言。偏底层实现&#xff0c;比较注重底层的逻辑实现。不能一味的说某一种语言特别好&#xff0c;每一种语言都是在特定的情况下有自己的优势。 二.Java语言发展史…

Docker Compose 一键快速部署 RocketMQ

Apache RocketMQ是一个开源的分布式消息中间件系统&#xff0c;最初由阿里巴巴开发并贡献给Apache软件基金会。RocketMQ提供了高性能、高可靠性、高扩展性和低延迟的消息传递服务&#xff0c;适用于构建大规模分布式系统中的消息通信和数据同步。 RocketMQ支持多种消息模型&am…

武汉星起航:无锡跨境电商加速“出海”,物流升级助品牌全球布局

随着全球化的不断深入&#xff0c;跨境电商作为数字外贸的新业态&#xff0c;正逐渐成为无锡企业拓展海外市场的重要渠道。武汉星起航关注到&#xff0c;近年来&#xff0c;无锡市通过积极推进国际物流枢纽建设&#xff0c;完善海外仓布局&#xff0c;以及各特色产业带的积极参…