SpringBoot utiliza EasyExcel para exportar 5 millones de datos en lotes

ilustrar

Registre el aprendizaje sobre cómo SpringBoot usa EasyExcel para exportar millones de datos en lotes. Guárdalo para después.
El entorno local mysql está instalado con la versión 5.7, el proyecto usa la versión jdk1.8 y la versión del controlador mysql utilizada por el proyecto es la versión 8.0.
El código de contenido de este blog se basa en mi blog:
SpringBoot usa mybatis para agregar por lotes 5 millones de datos a la demostración de la base de datos mysql y realiza cambios en el código de mi blog para escribir una función de exportación por lotes.

comparación de versiones de excel

Versión de Excel 03: HSSFWorkbook es la versión anterior al 2003, con la extensión .xls. Cada página de hoja tiene un máximo de 65536 filas y un máximo de 256 columnas. Cuando los datos del escenario de la aplicación tienen menos de 65536 filas de datos, puede usar Excel versión 03 para exportar y el rendimiento de la exportación es muy alto.

Excel versión 07: XSSFWorkbook es una versión posterior a 2007, con una extensión .xlsx. Cada página de hoja tiene un máximo de 1.048.576 filas. Si se excede este número de filas, el programa backend generará una excepción al exportar.

Cuando el volumen de datos excede las 65536 filas, puede usar Excel versión 07 para exportar grandes cantidades de datos. Cuando usa POI para exportar millones de datos, consumirá mucha memoria y es probable que cause OOM (desbordamiento de memoria) en el programa Para mejorar la eficiencia de XSSF Workbook Mejorado, se utiliza la tercera clase de implementación de Workbook, SXSSFWorkbook. SXSSFWorkbook se utiliza para manejar la exportación de volúmenes de datos grandes y extremadamente grandes, pero SXSSFWorkbook solo admite el formato .xlsx y no admite el formato .xls.

Introducción a EasyExcel

EasyExcel es una herramienta de procesamiento de Excel rápida y concisa basada en Java que resuelve el desbordamiento de memoria de archivos grandes.
Le permite completar rápidamente la lectura, escritura y otras funciones de Excel sin considerar el rendimiento, la memoria y otros factores.

Los marcos más famosos para el análisis y generación de Excel de Java incluyen Apache poi y jxl. Pero todos tienen un problema grave, que es que consumen mucha memoria. POI tiene un conjunto de API en modo SAX que pueden resolver algunos problemas de desbordamiento de memoria hasta cierto punto, pero POI todavía tiene algunos defectos, como la descompresión y El almacenamiento de la versión 07 de Excel se realiza en la memoria y el consumo de memoria sigue siendo muy grande.

easyexcel reescribe el análisis de poi de la versión 07 de Excel. Un excel de 3M todavía requiere alrededor de 100M de memoria para analizar con POI sax. Usar easyexcel puede reducirlo a unos pocos MB, y no importa cuán grande sea el excel, no habrá memoria desbordamiento; la versión 03 se basa en el modo sax de POI que encapsula la conversión del modelo en la capa superior, haciéndolo más simple y conveniente para los usuarios.

Documentación oficial de EasyExcel: https://easyexcel.opensource.alibaba.com/

Directorio de proyectos

Insertar descripción de la imagen aquí

declaración de creación de tabla correspondiente de 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>

clase de configuración application.yml

server:
  port: 8080

mybatis:
  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: debug

spring:
  datasource:
    #mysql批量新增需要在url后面添加rewriteBatchedStatements=true才能生效
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
    driverClassName: com.mysql.cj.jdbc.Driver #mysql8.0驱动,mysql5.7驱动是com.mysql.jdbc.Driver
    username: 你自己的数据库用户名
    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.html
        enabled: true
        login-username: druid
        login-password: druid


Código de clase de inicio

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);
    }

}

Clase de entidad 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;//每页所返回行数
}

Clase de título de plantilla de Excel OrderInfoExcel (EasyExcel necesita usar esto)

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;//修改人
    @ExcelIgnore
    private int pageNum;//页数
    @ExcelIgnore
    private int pageSize;//每页所返回行数
}

Capa de control TestController

Para conocer los métodos de exportación por lotes, consulte método de exportación por lotes.

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 {
    
    

    @Autowired
    private 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个sheet
            int 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();
        }
    }
}

Servicio de prueba de capa de interfaz

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);
}

Capa de implementación 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 {
    
    

    @Resource
    private TestDao testDao;

    @Resource
    private SqlSessionFactory sqlSessionFactory;

    @Override
    public 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();
        }
    }

    @Override
    public int batchExportCount() {
    
    
        return testDao.batchExportCount();
    }

    @Override
    public List<OrderInfo> batchExport(int pageNum, int pageSize) {
    
    
        return testDao.batchExport(pageNum, pageSize);
    }
}

Capa de interfaz de datos 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);
}

la capa dao corresponde a la declaración SQL personalizada de mapper.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">
<!-- 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,modifier
        from order_info
        order by id
        limit #{
    
    pageNum},#{
    
    pageSize}
    </select>
</mapper>

Los resultados de la prueba son los siguientes.

La interfaz de llamada del navegador muestra lo siguiente:
Insertar descripción de la imagen aquí
La consola en segundo plano imprime lo siguiente:
Insertar descripción de la imagen aquí
Cuanto más se pasa la página, más tarda la consulta.
Insertar descripción de la imagen aquí
De 22:33:01 a 22:36:52, tomó casi 4 minutos completar todos los datos Exportar.
Entonces, el cuello de botella en el rendimiento aquí es la optimización de SQL. ¿Cómo podemos mejorar el rendimiento de SQL al pasar las páginas profundamente, de modo que se
puedan exportar 5 millones de datos más rápido?

Los resultados del archivo de Excel son los siguientes:
la identificación aumenta de 1 a 5.000.000 y cada página de la hoja tiene 1 millón de datos.
Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí

Supongo que te gusta

Origin blog.csdn.net/weixin_48040732/article/details/131545051
Recomendado
Clasificación