【实战】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.mzph.cn/pingmian/39279.shtml

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

相关文章

Android项目框架

Android项目基于Android Studio开发&#xff0c;Android Studio使用Gradle作为项目构建工具。新建工程后可以看到如图所示目录结构&#xff0c;将Android切成Project可以看到完整的Android工程目录结构&#xff0c;如图所示。 图1-2 Android项目目录结构 app目录是一个典型的…

Profibus转Modbus网关在智能化水处理系统优化改造的应用

一、背景 在现代水处理行业中&#xff0c;智能化系统的应用已经成为提高效率和降低成本的关键。特别是在水厂中&#xff0c;罐内压载水处理系统的自动化和监控对于保障水质安全至关重要。而在这一过程中需要将水泵、阀门、传感器等设备连接到中控系统上。 二、方案 在控制器与…

计算机专业课面试常见问题-编程语言篇

目录 1. 程序的编译执行流程&#xff1f; 2. C浅拷贝和深拷贝的区别&#xff1f; 3. C虚函数&#xff1f; …

教你点出现安装错误 - 0x80070643的修复方法

错误代码 0x80070643 通常与 Windows 更新失败有关&#xff0c;但也可能出现在安装其他 Microsoft 软件&#xff08;例如 Microsoft Office 或 Microsoft .NET Framework&#xff09;时。这个错误可能由多种原因造成&#xff0c;比如权限问题、系统文件损坏、以前安装的残留或防…

thinksboard新建table表格

html文件 <div fxFlex fxLayoutAlign"left top" style"display: block"> <!-- <mat-card appearance"raised" style"max-height: 80vh; overflow-y: auto;">--><div><button mat-raised-button (click)&…

6个操作简单又好用的实用办公工具

分享6个操作简单又好用的实用办公工具&#xff0c;手机和电脑上的都有&#xff0c;好好使用可以让工作效率翻倍&#xff01; 1.方方格子 一个大型的的【Excel工具箱】&#xff0c;支持32位和64位Office&#xff0c;可直接作为插件使用&#xff0c;功能覆盖非常全面&#xff0c…

Jmeter 入门指南:从零开始学习

JMeter 是一个非常流行的开源工具&#xff0c;用于进行负载测试。它支持多种网络协议&#xff0c;包括 HTTP、FTP、SMTP、JMS、SOAP、JDBC 等&#xff0c;使其成为在多种应用环境中检测性能瓶颈的理想选择。本文将详细介绍如何利用 JMeter 进行高效的接口自动化测试。 创建和执…

黄金小程序开发的市场分析

在当今数字化时代&#xff0c;互联网技术与传统行业的深度融合正催生出一系列新兴商业模式和市场机遇。黄金行业&#xff0c;作为传统贵金属市场的代表&#xff0c;也在这场变革中迎来了新的发展机遇。黄金小程序的开发&#xff0c;正是这一趋势下的重要产物&#xff0c;它不仅…

Wireshark抓包工具使用

Wireshark抓包工具使用 1. Wireshark工具下载2. Wireshark工具基本配置3. Wireshark过滤语法3.1. 根据源IP过滤3.2. 针对特定的域名进行包过滤3.3. 针对特定的图片格式进行包过滤3.4. 针对特定的Host字段进行过滤4. Wireshark抓包文件保存1. Wireshark工具下载 Windows系统下载…

echarts用pictorialBar实现3D柱状图

先看下效果 实现思路 描绘一个普通的柱状图通过象形柱图&#xff08;pictorialBar&#xff09;在柱状图的顶部添加一个图形类型&#xff08;symbol&#xff09;菱形 代码实现 <template><div id"symbolBar"></div> </template> <scrip…

chunk-vendors.js 优化

问题背景 在 App.vue 加入 web-vitals 性能监控指标并打印 import {onLCP, onINP, onCLS, onTTFB} from web-vitals/attribution;// Measure and log LCP as soon as its available. onLCP(console.log); onINP(console.log); onCLS(console.log); onTTFB(console.log);网页的…

Redis 7.x 系列【14】数据类型之流(Stream)

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 本系列Redis 版本 7.2.5 源码地址&#xff1a;https://gitee.com/pearl-organization/study-redis-demo 文章目录 1. 概述2. 常用命令2.1 XADD2.2 XRANGE2.3 XREVRANGE2.4 XDEL2.5 XLEN2.6 XREAD2.7 XG…

【自用】CentOS7.6 安装 node-RED 4.0.2 教程(各种坑都摆脱的版本)

步骤总览 1.下载安装 nodejs 2.安装并配置 node-RED 3.重启服务器&#xff0c;验证 node-RED 是否安装 and 配置成功 一、下载安装 nodejs 1.下载 nodejs 18 为什么要下载 nodejs 18 呢&#xff1f; 因为 node-RED 4.0.1 支持的最低 nodejs 版本就是 nodejs 18。 当然了&a…

实在智能对话钉钉:宜搭+实在Agent,AI时代的工作方式

比起一个需求需要等产品、技术排期&#xff0c;越来越多的人开始追求把自己武装成「全能战士」&#xff0c;通过低代码工具一搭&#xff0c;一个高效的工作平台便产生了。 宜搭是钉钉自研的低代码应用构建平台&#xff0c;无论是专业开发者还是没有代码基础的业务人员&#xf…

不知几DAY的Symfony---RCE复现

感谢红队大佬老流氓的供稿&#xff0c;此篇文章是针对Symfony框架的一个RCE漏洞复现 ​框架简介 Symfony是一个开源的PHP Web框架&#xff0c;它现在是许多知名 CMS 的核心组件&#xff0c;例如Drupal、Joomla!、eZPlatform&#xff08;以前称为 eZPublish&#xff09;或Bolt。…

和鲸“101”计划领航!和鲸科技携手北中医,共话医学+AI 实验室建设及创新人才培养

为进一步加强医学院校大数据管理与应用、信息管理与信息系统&#xff0c;医学信息工程等专业建设&#xff0c;交流实验室建设、专业发展与人才培养经验&#xff0c;6 月 22 日&#xff0c;由北京中医药大学&#xff08;简称“北中医”&#xff09;主办&#xff0c;上海和今信息…

短剧系统开发:如何让你的创意变成现实

短剧系统开发是一个将创意转化为现实的过程&#xff0c;它涉及多个方面&#xff0c;包括需求分析、系统设计、开发环境搭建、前后端开发、测试与发布等。 1. 需求分析 &#xff08;1&#xff09;明确目标&#xff1a;首先&#xff0c;明确短剧系统的目标和定位&#xff0c;包括…

APP逆向 day9 安卓开发基础1

一.前言 app逆向当然要学安卓基础啦&#xff01;今天我们来教安卓基础当然&#xff0c;安卓基础不会教的很多&#xff0c;比java还要少&#xff0c;还是那句话&#xff0c;了解就好。 二.安卓环境搭建 2.1 安卓介绍 如果做安卓开发 需要会java代码安卓SDK(安卓提供的内置…

Hack The Box-Blazorized

总体思路 Blazor JWT->SPN劫持->登录脚本劫持->DCSync 信息收集&端口利用 nmap -sSVC blazorized.htbStarting Nmap 7.94SVN ( https://nmap.org ) at 2024-07-01 02:37 EDT Nmap scan report for blazorized.htb (10.10.11.22) Host is up (0.30s latency). N…

编译调试swift5.7源码

环境&#xff1a; 电脑&#xff1a;apple m1 pro系统&#xff1a;macOS13Xcode: 14.2Cmake: 3.25.1Ninja: 1.11.1sccache: 0.3.3python: 3.10 (如果你的mac不是这个版本&#xff0c;可以通过 brew install python3.10下载&#xff0c;然后看这篇文章切换到该python版本)swift代…