SpringBoot使用EasyExcel批量导出500万数据

SpringBoot使用EasyExcel批量导出500万数据

  • 说明
  • excel版本比较
  • EasyExcel介绍
  • 项目目录
  • mysql对应表建表语句
  • pom.xml
  • application.yml配置类
  • 启动类代码
  • OrderInfo 实体类
  • OrderInfoExcel excel模版标题类(EasyExcel需要使用这个)
  • TestController控制层
  • 接口层TestService
  • TestServiceImpl实现层
  • TestDao数据接口层
  • dao层对应mapper.xml自定义sql语句
  • 测试结果如下

说明

记录下学习SpringBoot使用EasyExcel批量导出百万数据。留着以后备用。
本地环境mysql安装的5.7版本,项目使用jdk1.8版本,项目使用的mysql驱动版本为8.0版本。
这一篇博客内容代码基于我的这篇博客:
SpringBoot使用mybatis批量新增500万数据到mysql数据库Demo,在自己这篇博客的代码上做改动写批量导出功能。

excel版本比较

03版excel:HSSFWorkbook为2003之前的版本,扩展名为.xls。每个sheet页最多65536行,最大列数是256列。当应用场景数据小于65536行数据时,可以使用03版excel来导出,导出性能很高。

07版excel:XSSFWorkbook为2007之后的版本,扩展名为.xlsx。每个sheet页最多1048576行,超过这个行数,导出的时候后端程序就会抛异常。

当数据量超过65536行进行大数据量导出时可以用07版excel,当用POI进行百万数据导出时,会很占用内存,很有可能导致程序出现OOM(内存溢出),而且为了让XSSFWorkbook效率提升,用了Workbook第三个实现类SXSSFWorkbook。SXSSFWorkbook用来处理大数据量以及超大数据量的导出,只是 SXSSFWorkbook只支持.xlsx格式,不支持.xls格式。

EasyExcel介绍

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

Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。

easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便。

EasyExcel官方文档:https://easyexcel.opensource.alibaba.com/

项目目录

在这里插入图片描述

mysql对应表建表语句

CREATE TABLE `order_info` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',`period` int(11) NOT NULL COMMENT '账期月份',`amount` decimal(20,2) NOT NULL COMMENT '金额',`user_name` varchar(20) NOT NULL COMMENT '下单人',`phone` varchar(11) NOT NULL COMMENT '手机号',`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`creator` varchar(20) NOT NULL COMMENT '创建人',`modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',`modifier` varchar(20) NOT NULL COMMENT '修改人',PRIMARY KEY (`id`) USING BTREE,KEY `idx_period` (`period`),KEY `idx_modified` (`modified`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='订单信息表';

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.3</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.example</groupId><artifactId>batching</artifactId><version>0.0.1-SNAPSHOT</version><name>batching</name><description>batching</description><properties><java.version>1.8</java.version><!--下列版本都是2022/04/16最新版本,都是父项目的基本依赖,用来子项目继承父项目依赖--><pagehelper-starter.version>1.4.2</pagehelper-starter.version><mybatis.version>3.5.9</mybatis.version><mysql-connector.version>8.0.28</mysql-connector.version><druid.version>1.2.9</druid.version><lombok.version>1.18.22</lombok.version><easyexcel.version>3.2.1</easyexcel.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--MyBatis分页插件1.4.2版本才支持spring-boot2.6.6--><!--pagehelper分页官网:https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper-spring-boot-starter/--><dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>${pagehelper-starter.version}</version></dependency><!-- MyBatis就是用来创建数据库连接进行增删改查等操作,提供了原生JDBC,Connection,Statement,ResultSet这些底层--><!-- MyBatis官网:https://mybatis.org/mybatis-3/zh/dependency-info.html--><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>${mybatis.version}</version></dependency><!--Mysql数据库驱动--><!--Mysql驱动官网:https://mvnrepository.com/artifact/mysql/mysql-connector-java/--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${mysql-connector.version}</version></dependency><!--集成druid连接池--><!--druid版本官网:https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter--><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>${druid.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--lombok--><!--lombok官网:https://mvnrepository.com/artifact/org.projectlombok/lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version></dependency><!--集成阿里巴巴EasyExcel用于大批量数据导出--><!--官网地址:https://easyexcel.opensource.alibaba.com/--><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>${easyexcel.version}</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

application.yml配置类

server:port: 8080mybatis:mapper-locations:- classpath:mapper/*.xml #找到mybatis位置,自定义sql语句#当查询语句中resultType="java.util.HashMap"时,如果返回的字段值为null时,设置如下参数为true,让它返回configuration:call-setters-on-nulls: true#打印sql语句
logging:level:com.example.batching.dao: debugspring:datasource:#mysql批量新增需要在url后面添加rewriteBatchedStatements=true才能生效url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=truedriverClassName: com.mysql.cj.jdbc.Driver #mysql8.0驱动,mysql5.7驱动是com.mysql.jdbc.Driverusername: 你自己的数据库用户名password: 你自己的数据库密码druid:initial-size: 3 #连接池初始大小min-idle: 5 #最小空闲连接数max-active: 20 #最大空闲连接数web-stat-filter:exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" #不统计这些请求数据stat-view-servlet: #访问监控网页的用户名和密码#默认为true,内置监控页面首页/druid/index.htmlenabled: truelogin-username: druidlogin-password: druid

启动类代码

package com.example.batching;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class BatchingApplication {public static void main(String[] args) {SpringApplication.run(BatchingApplication.class, args);}}

OrderInfo 实体类

package com.example.batching.entity;import lombok.Data;import java.math.BigDecimal;@Data
public class OrderInfo {private int id;private int period;//账期月份private BigDecimal amount;//金额private String userName;//下单人private String phone;//手机号private String created;//创建时间private String creator;//创建人private String modified;//修改时间private String modifier;//修改人private int pageNum;//页数private int pageSize;//每页所返回行数
}

OrderInfoExcel excel模版标题类(EasyExcel需要使用这个)

package com.example.batching.excelmode;import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;import java.math.BigDecimal;public class OrderInfoExcel {@ExcelProperty(value="id")private int id;@ExcelProperty(value="账期月份")private int period;//账期月份@ExcelProperty(value="金额")private BigDecimal amount;//金额@ExcelProperty(value="下单人")private String userName;//下单人@ExcelProperty(value="手机号")private String phone;//手机号@ExcelProperty(value="创建时间")private String created;//创建时间@ExcelProperty(value="创建人")private String creator;//创建人@ExcelProperty(value="修改时间")private String modified;//修改时间@ExcelProperty(value="修改人")private String modifier;//修改人@ExcelIgnoreprivate int pageNum;//页数@ExcelIgnoreprivate int pageSize;//每页所返回行数
}

TestController控制层

批量导出方法看batchExport方法。

package com.example.batching.controller;import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.example.batching.entity.OrderInfo;
import com.example.batching.excelmode.OrderInfoExcel;
import com.example.batching.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;@RestController
@RequestMapping(value = "/order")
public class TestController {@Autowiredprivate TestService testService;//批量新增数据处理@PostMapping(value = "/batchSave")public String batchSave() {//随机生成电话号码String[] start = {"130", "131", "132", "133", "134", "150", "151", "155", "158", "166", "180", "181", "184", "185", "188"};List<OrderInfo> orderInfoList=new ArrayList<>();//生成500万数据批量新增到mysql数据库里面for(int i=1;i<=5000000;i++){OrderInfo orderInfo=new OrderInfo();orderInfo.setPeriod(202206);orderInfo.setAmount(new BigDecimal(i));orderInfo.setUserName("用户"+i);orderInfo.setPhone(start[(int) (Math.random() * start.length)]+(10000000+(int)(Math.random()*(99999999-10000000+1))));orderInfo.setCreator("用户"+i);orderInfo.setModifier("用户"+i);orderInfoList.add(orderInfo);//每一万条数据进行批量新增if(i%10000==0){testService.batchSave(orderInfoList);//新增完成后清空list集合防止内存溢出orderInfoList.clear();System.out.println("当前已新增完数据:"+i+"行");}}return "成功";}//批量导出数据到excel@GetMapping(value = "/batchExport")public void batchExport(HttpServletResponse response) {try{OutputStream outputStream =response.getOutputStream();//查询出总数据量大小,这里为500万int count=testService.batchExportCount();System.out.println("count="+count);//根据总数得到总页数int totalPage=(count-1)/100000+1;//总页数,每页10万行数据System.out.println("totalPage="+totalPage);//xlsx每个sheet页最大数据行为1048576,超过这个数值就会报错,所以这里将500万数据分5个导出到5个sheet页//根据总数得到每页sheet应该分几个sheet,每个sheet导入100万数据,500万就是5个sheetint totalSheet=(count-1)/1000000+1;//总sheet页数,每个sheet100万行数据System.out.println("totalSheet="+totalSheet);//设置初始页数OrderInfo orderInfo=new OrderInfo();orderInfo.setPageNum(0);//初始页,从0开始orderInfo.setPageSize(100000);//每页返回数据//文件名String fileName="批量测试导出.xlsx";//使用EasyExcel进行导出ExcelWriter excelWriter = EasyExcel.write(outputStream, OrderInfoExcel.class).build();//这里最终会写到5个sheet里面for (int i = 0; i < totalSheet; i++) {//writerSheet第一个参数表示往几个sheet开始写数据,从0开始表示第一个WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).build();//分页去数据库查询数据for(int j = orderInfo.getPageNum(); j < totalPage; j++){//页数,对应后端数据库来说是索引int pageNum=j*100000;//每页要查询的行数int pageSize=orderInfo.getPageSize();//根据分页参数去查询每页数据List<OrderInfo> data = testService.batchExport(pageNum,pageSize);excelWriter.write(data, writeSheet);System.out.println("已导出数据:"+(pageNum+100000));if((pageNum+100000)%1000000==0){//记录当前页数j并加1,并跳出这个for循环,往下一个sheet页写入数据orderInfo.setPageNum(j+1);break;}}}//下载response.reset();response.setContentType("application/octet-stream; charset=utf-8");//以流的形式对文件进行下载response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));//对文件名编码,防止文件名乱码excelWriter.finish();outputStream.flush();outputStream.close();}catch (Exception e){e.printStackTrace();}}
}

接口层TestService

package com.example.batching.service;import com.example.batching.entity.OrderInfo;import java.util.List;public interface TestService {void batchSave(List<OrderInfo> orderInfoList);int batchExportCount();List<OrderInfo> batchExport(int pageNum, int pageSize);
}

TestServiceImpl实现层

package com.example.batching.service.impl;import com.example.batching.dao.TestDao;
import com.example.batching.entity.OrderInfo;
import com.example.batching.service.TestService;
import com.github.pagehelper.PageHelper;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import java.util.List;@Service
public class TestServiceImpl implements TestService {@Resourceprivate TestDao testDao;@Resourceprivate SqlSessionFactory sqlSessionFactory;@Overridepublic void batchSave(List<OrderInfo> orderInfoList) {//批量新增处理,需要在jdbc连接那里添加rewriteBatchedStatements=true属性,批量新增才能生效// ExecutorType.SIMPLE: 这个执行器类型不做特殊的事情。它为每个语句的执行创建一个新的预处理语句。自动提交不关闭的前提下,默认设置是这个// ExecutorType.REUSE: 这个执行器类型会复用预处理语句。// ExecutorType.BATCH: 这个执行器会批量执行所有更新语句,如果 SELECT 在它们中间执行还会标定它们是 必须的,来保证一个简单并易于理解的行为。//如果自动提交设置为true,将无法控制提交的条数,改为最后统一提交SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false);try {TestDao testMapper = sqlSession.getMapper(TestDao.class);orderInfoList.stream().forEach(orderInfo -> testMapper.batchSave(orderInfo));//提交数据sqlSession.commit();} catch (Exception e) {sqlSession.rollback();} finally {sqlSession.close();}}@Overridepublic int batchExportCount() {return testDao.batchExportCount();}@Overridepublic List<OrderInfo> batchExport(int pageNum, int pageSize) {return testDao.batchExport(pageNum, pageSize);}
}

TestDao数据接口层

package com.example.batching.dao;import com.example.batching.entity.OrderInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.List;@Mapper
public interface TestDao {void batchSave(OrderInfo orderInfo);int batchExportCount();List<OrderInfo> batchExport(@Param("pageNum") int pageNum, @Param("pageSize") int pageSize);
}

dao层对应mapper.xml自定义sql语句

<?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">
<!-- namespace必须指向Dao接口 -->
<mapper namespace="com.example.batching.dao.TestDao"><insert id="batchSave" parameterType="com.example.batching.entity.OrderInfo">INSERT INTO order_info<trim prefix="(" suffix=")" suffixOverrides=","><if test="period != null">period,</if><if test="amount != null">amount,</if><if test="userName != null">user_name,</if><if test="phone != null">phone,</if><if test="creator != null">creator,</if><if test="modifier != null">modifier,</if></trim><trim prefix="values (" suffix=")" suffixOverrides=","><if test="period != null">#{period},</if><if test="amount != null">#{amount},</if><if test="userName != null">#{userName},</if><if test="phone != null">#{phone},</if><if test="creator != null">#{creator},</if><if test="modifier != null">#{modifier},</if></trim></insert><select id="batchExportCount" resultType="java.lang.Integer">select count(id) num from order_info</select><select id="batchExport" parameterType="java.lang.Integer" resultType="com.example.batching.entity.OrderInfo">select id,period,amount,user_name userName,phone,created,creator,modified,modifierfrom order_infoorder by idlimit #{pageNum},#{pageSize}</select>
</mapper>

测试结果如下

浏览器调用接口返回如下:
在这里插入图片描述
后台控制台打印如下:
在这里插入图片描述
越往后进行深翻页,查询耗时越长
在这里插入图片描述
从22时33分01秒到22时36分52秒耗时接近4分钟才将所有数据进行导出。
所以这里的性能瓶颈在sql优化那一块,怎么才能深翻页的时候提高sql性能,让500万
数据更快导出。

excel文件结果如下:
id自增从1到5000000,每个sheet页100万数据。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

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

相关文章

行为型模式-策略模式(一)

今天就说一说设计模式中的策略模式&#xff0c;从名字来讲&#xff0c;意思就是&#xff0c;对应不同的情况&#xff0c;就有一种解决问题的办法&#xff0c;不同的情况&#xff0c;就有不同的应对方法&#xff0c;这就是策略模式&#xff0c;非常的智能化。 也可以参考菜鸟 …

阿里云服务器扩容记录

先在控制台选择云盘扩容&#xff0c;然后才能在服务器操作&#xff1a; blkidlsblk -n df -Thapt-get updatetype growpart || apt-get install -y cloud-guest-utilstype sgdisk || apt-get install -y gdiskLC_ALLen_US.UTF-8 growpart /dev/vda 3 resize2fs /dev/vda3

svn下载及使用

下载 链接&#xff1a;https://pan.baidu.com/s/1RV6vAujA1anHhXJuKbItUQ?pwdflzx 提取码&#xff1a;flzx 这是svn安装包以及中文安装包&#xff0c;放在百度网盘了&#xff0c;有需要可以下载 注意&#xff1a;svn中文语言包安装的版本需要和svn的版本一致 他有两种安装…

计算机视觉 3D点云极简概述

一、概述 点云是表示 3D 坐标系中的数字 3D 物理对象或空间的点数据库。它由数百万或者更多个单独的测量点组成,具有 x、y 和 z 坐标。3D点云是物体的高精度数字记录。点云用于生成用于 3D 建模的 3D 网格和其他模型。包括医学成像、3D 打印、制造、建筑、3D 游戏和虚拟现实 (…

RAID5重建失败的服务器数据恢复案例

服务器数据恢复环境&#xff1a; 一台IBM某型号服务器&#xff0c;4块SAS磁盘组建了一组RAID5磁盘阵列。服务器安装的windows server操作系统&#xff0c;上面运行了一个Oracle单节点&#xff0c;数据存储为文件系统&#xff0c;无归档。该oracle数据库的数据量不大&#xff0c…

UG NX二次开发(C#)-建模-根据特征获取体(或者关联体)

文章目录 1、前言2、创建块特征与UIStyler2.1 创建两个长方体2.2 创建一个UIStyler3、添加获取体的代码1、前言 UG NX的建模是基于特征建模的,比如块、圆柱体、合并操作等等都是特征,都有特征对象,对于具有体的特征,其可以通过NXOpen二次开发技术实现二者的转换,也可以通…

网络编程套接字

目录 网络编程基础 为什么需要网络编程&#xff1f; 什么是网络编程 网络编程中的基本概念 发送端和接收端 请求和响应 客户端和服务端 常见的客户端服务端模型 Socket套接字 概念 分类 Java数据报套接字通信模型 Java流套接字通信模型 Socket编程注意事项 UDP数…

List移除元素的四种方式

List 移除某个元素 四种方式&#xff1a; 方式一&#xff0c;使用 Iterator &#xff0c;顺序向下&#xff0c;如果找到元素&#xff0c;则使用 remove 方法进行移除。方式二&#xff0c;倒序遍历 List &#xff0c;如果找到元素&#xff0c;则使用 remove 方法进行移除。方式…

括号生成(力扣)递归 JAVA

目录 题目描述&#xff1a;纯递归解法&#xff1a;递归 回溯&#xff1a; 题目描述&#xff1a; 数字 n 代表生成括号的对数&#xff0c;请你设计一个函数&#xff0c;用于能够生成所有可能的并且 有效的 括号组合。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a…

spring重点标签有哪些

<bean>标签 id属性:在容器中Bean实例的唯一标识&#xff0c;不允许重复 class属性:要实例化的Bean的全限定名 scope属性:Bean的作用范围&#xff0c;常用是singleton(默认)和prototype <property>标签:属性注入 name属性:属性名称 value属性:注入的普通属性值 ref属…

Netty核心技术九--TCP 粘包和拆包及解决方案

1. TCP 粘包和拆包基本介绍 **TCP是面向连接的&#xff0c;面向流的&#xff0c;提供高可靠性服务。收发两端&#xff08;客户端和服务器端&#xff09;都要有一一成对的socket&#xff0c;因此&#xff0c;发送端为了将多个发给接收端的包&#xff0c;更有效的发给对方&#x…

爆肝整理,Docker容器测试-常见问题+解决(汇总)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 问题1&#xff1a…