SpringBoot utilise EasyExcel pour exporter 5 millions de données par lots

illustrer

Enregistrez comment SpringBoot utilise EasyExcel pour exporter des millions de données par lots. Gardez-le pour plus tard.
L'environnement local mysql est installé avec la version 5.7, le projet utilise la version jdk1.8 et la version du pilote mysql utilisée par le projet est la version 8.0.
Le code du contenu de ce blog est basé sur mon blog :
SpringBoot utilise mybatis pour ajouter par lots 5 millions de données à la base de données mysql Demo et apporte des modifications au code de mon blog pour écrire une fonction d'exportation par lots.

comparaison des versions Excel

Excel version 03 : HSSFWorkbook est la version antérieure à 2003, avec l'extension .xls. Chaque page de feuille comporte un maximum de 65 536 lignes et un maximum de 256 colonnes. Lorsque les données du scénario d'application comportent moins de 65 536 lignes de données, vous pouvez utiliser Excel version 03 pour exporter et les performances d'exportation sont très élevées.

Excel version 07 : XSSFWorkbook est une version postérieure à 2007, avec une extension .xlsx. Chaque page de feuille comporte un maximum de lignes 1 048 576. Si ce nombre de lignes est dépassé, le programme backend lèvera une exception lors de l'exportation.

Lorsque le volume de données dépasse 65 536 lignes, vous pouvez utiliser Excel version 07 pour exporter de grandes quantités de données. Lorsque vous utilisez POI pour exporter des millions de données, cela prendra beaucoup de mémoire et est susceptible de provoquer un MOO (débordement de mémoire) dans le Afin de rendre XSSFWorkbook efficace, amélioré, en utilisant la troisième classe d'implémentation de Workbook, SXSSFWorkbook. SXSSFWorkbook est utilisé pour gérer l'exportation de volumes de données importants et extrêmement volumineux. Cependant, SXSSFWorkbook ne prend en charge que le format .xlsx et ne prend pas en charge le format .xls.

Introduction à EasyExcel

EasyExcel est un outil de traitement Excel rapide et concis basé sur Java qui résout le débordement de mémoire des fichiers volumineux.
Il vous permet d'exécuter rapidement la lecture, l'écriture et d'autres fonctions d'Excel sans tenir compte des performances, de la mémoire et d'autres facteurs.

Les frameworks les plus connus pour l'analyse Java et la génération d'Excel incluent Apache poi et jxl. Mais ils ont tous un sérieux problème : ils consomment beaucoup de mémoire. POI dispose d'un ensemble d'API en mode SAX qui peuvent résoudre dans une certaine mesure certains problèmes de débordement de mémoire, mais POI a encore quelques défauts, comme la décompression et le stockage des fichiers. 07 d'Excel. Cela se fait en mémoire, et la consommation de mémoire est encore très importante.

easyexcel réécrit l'analyse de poi de la version 07 d'Excel. Un Excel 3M nécessite encore environ 100 Mo de mémoire pour être analysé avec POI sax. L'utilisation d'easyexcel peut la réduire à quelques Mo, et quelle que soit la taille d'Excel, il n'y aura pas de mémoire. overflow ; la version 03 s'appuie sur le mode sax de POI et encapsule la conversion de modèle dans la couche supérieure, la rendant plus simple et plus pratique pour les utilisateurs.

Documentation officielle EasyExcel : https://easyexcel.opensource.alibaba.com/

Répertoire du projet

Insérer la description de l'image ici

instruction de création de table correspondante 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>

classe de configuration 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


Code de classe de démarrage

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

}

Classe d'entité 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;//每页所返回行数
}

Classe de titre du modèle Excel OrderInfoExcel (EasyExcel doit l'utiliser)

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

Couche de contrôle TestController

Pour les méthodes d’exportation par lots, voir méthode 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 {
    
    

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

Service de test de la couche d'interface

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

Couche d'implémentation 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);
    }
}

Couche d'interface de données 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 couche dao correspond à l'instruction SQL personnalisée 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>

Les résultats des tests sont les suivants

L'interface d'appel du navigateur renvoie ce qui suit :
Insérer la description de l'image ici
La console d'arrière-plan s'imprime comme suit :
Insérer la description de l'image ici
Plus la page est tournée en profondeur, plus la requête prend du temps.
Insérer la description de l'image ici
De 22:33:01 à 22:36:52, il a fallu près de 4 minutes pour terminer toutes les requêtes. données.Exporter.
Le goulot d'étranglement des performances ici est donc l'optimisation SQL. Comment pouvons-nous améliorer les performances SQL lors de la rotation profonde des pages, afin que 5 millions de
données puissent être exportées plus rapidement ?

Les résultats du fichier Excel sont les suivants :
l'identifiant passe de 1 à 5 000 000, et chaque page de feuille contient 1 million de données.
Insérer la description de l'image ici
Insérer la description de l'image ici
Insérer la description de l'image ici
Insérer la description de l'image ici
Insérer la description de l'image ici
Insérer la description de l'image ici
Insérer la description de l'image ici
Insérer la description de l'image ici
Insérer la description de l'image ici
Insérer la description de l'image ici

Je suppose que tu aimes

Origine blog.csdn.net/weixin_48040732/article/details/131545051
conseillé
Classement