【实战】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));}
}
相关文章:

【实战】EasyExcel实现百万级数据导入导出
文章目录 前言技术积累实战演示实现思路模拟代码测试结果 前言 最近接到一个百万级excel数据导入导出的需求,大概就是我们在进行公众号API群发的时候,需要支持500w以上的openid进行群发,并且可以提供发送openid数据的导出功能。可能有的同学…...
Graalvm配置文件与Feature和Substitute机制介绍
GraalVM介绍 GraalVM提前将Java应用程序编译成独立与机器码二进制文件(可执行文件、动态库文件),如windows系统中的exe文件和dll文件。与在Java虚拟机(JVM)上运行的应用程序相比,这些二进制文件更小,启动速…...

Appium adb 获取appActivity
方法一(最简单有效的方法) 通过cmd命令,前提是先打开手机中你要获取包名的APP adb devices -l 获取连接设备详细信息 adb shell dumpsys activity | grep mFocusedActivity 有时获取到的不是真实的Activity 方法二 adb shell monkey -p …...

调整分区失败致盘无法访问:深度解析与数据恢复全攻略
调整分区失败盘打不开的困境 在计算机的日常维护与管理中,调整磁盘分区是常见的操作之一,旨在优化存储空间布局、提升系统性能或满足特定应用需求。然而,当这一操作未能如预期般顺利进行,反而导致分区调整失败,进而使…...

试用笔记之-汇通计算机等级考试软件一级Windows
首先下载汇通计算机等级考试软件一级Windows http://www.htsoft.com.cn/download/htwork.rar...

Java的NIO体系
目录 NIO1、操作系统级别下的IO模型有哪些?2、Java语言下的IO模型有哪些?3、Java的NIO应用场景?相比于IO的优势在哪?4、Java的IO、NIO、AIO 操作文件读写5、NIO的核心类 :Buffer(缓冲区)、Channelÿ…...
自下而上的选股与自上而下的选股
一起学习了《战胜华尔街》,不知道大家有没有这么一种感受:林奇的选股方法是典型的自下而上的选股方法。虽然这一点没有单独拎出来讨论过,但在《从低迷中寻找卓越》《如何通过财务指标筛选股票?》《边逛街边选股?》《好…...

Tech Talk:智能电视eMMC存储的五问五答
智能电视作为搭载操作系统的综合影音载体,以稳步扩大的市场规模走入越来越多的家庭,成为人们生活娱乐的重要组成部分。存储部件是智能电视不可或缺的组成部分,用于保存操作系统、应用程序、多媒体文件和用户数据等信息。智能电视使用eMMC作为…...

scikit-learn教程
scikit-learn(通常简称为sklearn)是Python中最受欢迎的机器学习库之一,它提供了各种监督和非监督学习算法的实现。下面是一个基本的教程,涵盖如何使用sklearn进行数据预处理、模型训练和评估。 1. 安装和导入包 首先确保安装了…...

CentOS 7 搭建rsyslog日志服务器
CentOS 7 搭建rsyslog日志服务器 前言一、IP地址及主机名称规划1.修改主机名 二、配置rsyslog日志服务器1.安装rsyslog服务2.编辑/etc/rsyslog.conf 文件3.启动并启用rsyslog服务4.验证端口是否侦听 三、在rsyslog日志服务器上配置firewalld防火墙四、配置rsyslog日志客户端1.编…...
使用Spring Boot Actuator监控应用健康状态
使用Spring Boot Actuator监控应用健康状态 大家好,我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿!今天我们将探讨如何利用Spring Boot Actuator来监控和管理应用程序的…...

leetcode刷题:vector刷题
🔥个人主页:guoguoqiang. 🔥专栏:leetcode刷题 1.只出现一次的数字 这道题很简单,我们只需要遍历一次数组即可通过异或运算实现。(一个数与自身异或结果为0,任何数与0异或还是它本身) class Solut…...
CGI面试题及参考答案
什么是CGI?它在Web服务器与应用程序之间扮演什么角色? CGI(Common Gateway Interface) 是一种标准协议,它定义了Web服务器与运行在服务器上的外部程序(通常是脚本或应用程序)之间的通信方式。简单来说,CGI充当了一个桥梁,使得Web服务器能够将用户的请求传递给后端程序…...
论文调研_物联网漏洞检测综述
A Review of IoT Firmware Vulnerabilities and Auditing Techniques 研究背景:物联网设备在工业、消费类等各个领域得到了广泛应用,实现了更高的自动化和生产率。然而,这些连网设备的高度依赖也带来了一系列网络安全威胁,特别是…...

Java学习【IO流:深入理解与应用(上)】
Java学习【IO流:深入理解与应用(上)】 🍃1.IO流体系结构🍃2.FileOutputStream🍁2.1FileOutputStream写数据的三种方式🍁2.2换行和续写 🍃3.FileInputStream🍁3.1每次读取…...
干货系列:SpringBoot3第三方接口调用10种方式
环境:SpringBoot.3.3.0 1、简介 在项目中调用第三方接口是日常开发中非常常见的。调用方式的选择通常遵循公司既定的技术栈和架构规范,以确保项目的一致性和可维护性。无论是RESTful API调用、Feign声明式HTTP客户端、Apache HttpClient等调用方式&…...

KVM性能优化之CPU优化
1、查看kvm虚拟机vCPU的QEMU线程 ps -eLo ruser,pid,ppid,lwp,psr,args |awk /^qemu/{print $1,$2,$3,$4,$5,$6,$8} 注:vcpu是不同的线程,而不同的线程是跑在不同的cpu上,一般情况,虚拟机在运行时自身会点用3个cpus,为保证生产环…...

lua中判断2个表是否相等
当我们获取 table 长度的时候无论是使用 # 还是 table.getn 其都会在索引中断的地方停止计数,而导致无法正确取得 table 的长度,而且还会出现奇怪的现象。例如:t里面有3个元素,但是因为最后一个下表是5和4,却表现出不一…...

uni-app 自定义支付密码键盘
1.新建组件 payKeyboard .vue <template><view class"page-total" v-show"isShow"><view class"key-list"><view class"list" v-for"(item,index) in keyList" :class"{special:item.keyCode190…...

抖音微短剧小程序源码搭建:实现巨量广告数据高效回传
在数字化营销日益盛行的今天,抖音微短剧小程序已成为品牌与观众互动的新渠道。这些短小精悍的剧目不仅能迅速抓住用户的注意力,还能有效提升品牌的知名度和用户黏性。然而,想要充分利用这一营销工具,关键在于如何高效地追踪广告数…...
Android Wi-Fi 连接失败日志分析
1. Android wifi 关键日志总结 (1) Wi-Fi 断开 (CTRL-EVENT-DISCONNECTED reason3) 日志相关部分: 06-05 10:48:40.987 943 943 I wpa_supplicant: wlan0: CTRL-EVENT-DISCONNECTED bssid44:9b:c1:57:a8:90 reason3 locally_generated1解析: CTR…...
2024年赣州旅游投资集团社会招聘笔试真
2024年赣州旅游投资集团社会招聘笔试真 题 ( 满 分 1 0 0 分 时 间 1 2 0 分 钟 ) 一、单选题(每题只有一个正确答案,答错、不答或多答均不得分) 1.纪要的特点不包括()。 A.概括重点 B.指导传达 C. 客观纪实 D.有言必录 【答案】: D 2.1864年,()预言了电磁波的存在,并指出…...
基于Uniapp开发HarmonyOS 5.0旅游应用技术实践
一、技术选型背景 1.跨平台优势 Uniapp采用Vue.js框架,支持"一次开发,多端部署",可同步生成HarmonyOS、iOS、Android等多平台应用。 2.鸿蒙特性融合 HarmonyOS 5.0的分布式能力与原子化服务,为旅游应用带来…...
相机Camera日志分析之三十一:高通Camx HAL十种流程基础分析关键字汇总(后续持续更新中)
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:有对最普通的场景进行各个日志注释讲解,但相机场景太多,日志差异也巨大。后面将展示各种场景下的日志。 通过notepad++打开场景下的日志,通过下列分类关键字搜索,即可清晰的分析不同场景的相机运行流程差异…...
Element Plus 表单(el-form)中关于正整数输入的校验规则
目录 1 单个正整数输入1.1 模板1.2 校验规则 2 两个正整数输入(联动)2.1 模板2.2 校验规则2.3 CSS 1 单个正整数输入 1.1 模板 <el-formref"formRef":model"formData":rules"formRules"label-width"150px"…...

Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...
深入理解Optional:处理空指针异常
1. 使用Optional处理可能为空的集合 在Java开发中,集合判空是一个常见但容易出错的场景。传统方式虽然可行,但存在一些潜在问题: // 传统判空方式 if (!CollectionUtils.isEmpty(userInfoList)) {for (UserInfo userInfo : userInfoList) {…...

【LeetCode】算法详解#6 ---除自身以外数组的乘积
1.题目介绍 给定一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法,且在 O…...

数学建模-滑翔伞伞翼面积的设计,运动状态计算和优化 !
我们考虑滑翔伞的伞翼面积设计问题以及运动状态描述。滑翔伞的性能主要取决于伞翼面积、气动特性以及飞行员的重量。我们的目标是建立数学模型来描述滑翔伞的运动状态,并优化伞翼面积的设计。 一、问题分析 滑翔伞在飞行过程中受到重力、升力和阻力的作用。升力和阻力与伞翼面…...

认识CMake并使用CMake构建自己的第一个项目
1.CMake的作用和优势 跨平台支持:CMake支持多种操作系统和编译器,使用同一份构建配置可以在不同的环境中使用 简化配置:通过CMakeLists.txt文件,用户可以定义项目结构、依赖项、编译选项等,无需手动编写复杂的构建脚本…...