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

news2024/7/7 15:11:30

文章目录

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

在这里插入图片描述

前言

最近接到一个百万级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() {
        @Override
        public 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=true
    username: root
    password: 12345678
    driver-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: NONE
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
    banner: false # 关闭控制台的 Banner 打印
  type-aliases-package: com.example.ccedemo.entity
  mapper-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 {

    @TableId
    private 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_user
        limit #{offset},#{size}
    </select>

</mapper>

提供测试类

/**
 * EasyExcelTest
 * @author senfel
 * @version 1.0
 * @date 2024/7/1 16:03
 */
@SpringBootTest
public class EasyExcelTest {

    @Resource
    private MpUserMapper mpUserMapper;
    /**
     * readExcel
     * @author senfel
     * @date 2024/7/1 17:17
     * @return void
     */
    @Test
    public 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
     */
    @Test
    public 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);
        // 下载EXCEL
        response.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.coloradmin.cn/o/1886390.html

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

相关文章

电脑录歌用什么软件好?分享电脑录音软件:6款

短视频普遍的今天&#xff0c;越来越多的人喜欢通过电脑进行音乐创作和录制。然而&#xff0c;面对市面上琳琅满目的电脑录音软件&#xff0c;很多人可能会感到困惑&#xff1a;电脑录歌用什么软件好呢&#xff1f;本文将为大家分享六款精选的录音软件&#xff0c;帮助大家找到…

主从同步binlog

主从同步的原理是怎样的 提到主从同步的原理&#xff0c;我们就需要了解在数据库中的一个重要日志文件&#xff0c;那就是 Binlog 二 进制日志&#xff0c;它记录了对数据库进行更新的事件。实际上主从同步的原理就是基于 Binlog 进 行数据同步的。在主从复制过程中&#xff…

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是不同的线程&#xff0c;而不同的线程是跑在不同的cpu上&#xff0c;一般情况&#xff0c;虚拟机在运行时自身会点用3个cpus&#xff0c;为保证生产环…

第二篇——始计篇:“计”是最早的SWOT分析

目录 一、背景介绍二、思路&方案三、过程1.思维导图2.文章中经典的句子理解3.学习之后对于投资市场的理解4.通过这篇文章结合我知道的东西我能想到什么&#xff1f; 四、总结五、升华 一、背景介绍 第二次详读孙子兵法&#xff0c;当初听讲解的时候&#xff0c;就觉得自己…

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

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

某智能装备公司如何实现多个工程师共用1台图形工作站

在当今快速发展的科技领域&#xff0c;资源共享和高效利用已成为企业提升竞争力的关键&#xff0c;特别是在工程设计和研发领域。如何最大化地利用有限的资源&#xff0c;如工作站&#xff0c;成为了许多公司面临的挑战。某智能装备公司便是在这样的背景下&#xff0c;通过云飞…

【如何使用RSA签名验签】python语言

文章目录 签名方法异步同步通知数据验签生活号响应数据验签同步响应数据验签 &#x1f308;你好呀&#xff01;我是 山顶风景独好 &#x1f388;欢迎踏入我的博客世界&#xff0c;能与您在此邂逅&#xff0c;真是缘分使然&#xff01;&#x1f60a; &#x1f338;愿您在此停留的…

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

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

玉林师范学院宿舍管理系统的设计与实现19633

玉林师范学院宿舍管理系统设计与实现 摘要&#xff1a;随着大学生人数的增加&#xff0c;宿舍管理成为高校管理中的重要问题。本论文旨在研究玉林师范学院宿舍管理系统&#xff0c;探讨其优势和不足&#xff0c;并提出改进建议。通过对相关文献的综述和实地调研&#xff0c;我们…

【操作系统】进程管理——线程管理(个人笔记)

学习日期&#xff1a;2024.7.2 内容摘要&#xff1a;线程的概念、存在的意义、线程的属性&#xff0c;线程的实现方式&#xff0c;线程的状态与组织。 线程的概念 拿QQ来说&#xff0c;QQ既可以打视频电话&#xff0c;也可以在这同时进行文字聊天或传送文件&#xff0c;进程是…

java基于ssm+jsp 二手交易平台网站

1商家能模块 商家首页&#xff0c;在商家首页页面可以查看个人中心、商品分类管理、商品信息管理、订单信息管理、订单配送管理信息&#xff0c;如图1所示。 图1商家首页界面图 个人中心&#xff0c;用户通过个人中心可以查看用户名、用户姓名、头像、性别、手机号码、邮箱等信…

MySQL数据库设计作业 ——《网上书店系统》数据库设计实验报告

数据库设计作业——《网上书店系统》数据库设计 一、功能需求 普通用户&#xff1a;可以进行最基础的登陆操作&#xff0c;可浏览图书、按类别查询图书、查看 图书的详细信息&#xff0c;还可以注册成为会员。会员&#xff1a;需要填写详细信息&#xff08;真实姓名、性别、手…

移动端 UI 风格简约而不凡

移动端 UI 风格简约而不凡

vue 中使用element-ui实现锚点定位表单

效果图&#xff1a; 代码&#xff1a; html代码&#xff1a; <div class"content-left"><el-tabs :tab-position"left" tab-click"goAnchor"><el-tab-pane v-for"(item,index) in anchorNameList"v-anchor-scroll:ke…

HTTP2.0如何优化HTTP1.1

目录 HTTP1.1什么缺点 如何解决头部压缩 静态字典 动态字典 如何解决队头阻塞 Stream 排序 服务器主动推送资源 HTTP1.1什么缺点 队头阻塞问题 同一连接只能在完成一个 HTTP 事务&#xff08;请求和响应&#xff09;后&#xff0c;才能处理下一个事务&#xff1b;(…

力扣每日一题 7/2 数学、数论、数组/双指针

博客主页&#xff1a;誓则盟约系列专栏&#xff1a;IT竞赛 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ 3115.质数的最大距离【中等】 题目&#xff1a; 给你一个整数数组 nums。…

视频字幕提取在线工具有哪些?总结5个字幕提取工具

平时在沉浸式追剧的时候&#xff0c;我们常常都会被影视剧中的各种金句爆梗而逗得开怀大笑~而真正要用到时候却总是一片头脑空白。其实要记住它们最好的办法便是将其提取留档下来&#xff0c;每次有需要的时候打开就能一下子回顾到~ 今天就来带大家盘一盘视频字幕提取的软件好…

[C++]——同步异步日志系统(1)

同步异步日志系统 一、项⽬介绍二、开发环境三、核心技术四、环境搭建五、日志系统介绍5.1 为什么需要日志系统5.2 日志系统技术实现5.2.1 同步写日志5.2.2 异步写日志 日志系统&#xff1a; 日志&#xff1a;程序在运行过程中&#xff0c;用来记录程序运行状态信息。 作用&…

图片转pdf,图片转pdf在线转换,在线图片转pdf,图片转pdf格式怎么弄

在数字化时代&#xff0c;图片和PDF文件成为了我们日常生活中不可或缺的元素。有时候&#xff0c;我们需要将图片转换成PDF格式以便更好地分享、存储或打印。那么&#xff0c;图片格式怎么转换成PDF呢&#xff1f;本文将为您详细介绍简单实用的转换方法。 方法一、使用pdf转换软…

Python容器 之 字典--字典的常用操作方法

1.增加和修改 字典[键] 值 键 存在, 修改 键 不存在, 添加 # 定义非空字典, 姓名, 年龄, 身高, 性别 my_dict {"name": "小明", "age": 18, "height": 1.78, "isMen": True} print(my_dict) # {name: 小明, age: 18, h…