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のバージョン比較
Excel バージョン 03: HSSFWorkbook は 2003 より前のバージョンで、拡張子は .xls です。各シート ページには最大 65536 行と最大 256 列があります。アプリケーション シナリオ データが 65536 行未満の場合、Excel バージョン 03 を使用してエクスポートでき、エクスポート パフォーマンスは非常に高くなります。
Excel バージョン 07: XSSFWorkbook は 2007 以降のバージョンで、拡張子は .xlsx です。各シート ページの最大行数は 1,048,576 行です。この行数を超えると、エクスポート時にバックエンド プログラムによって例外がスローされます。
データ量が 65536 行を超える場合、Excel バージョン 07 を使用して大量のデータをエクスポートできます。POI を使用して数百万のデータをエクスポートすると、大量のメモリが占有され、OOM (メモリ オーバーフロー) が発生する可能性があります。 XSSFWorkbook を効率化するために、Workbook の 3 番目の実装クラス SXSSFWorkbook を使用するように改良しました。SXSSFWorkbook は、大規模および非常に大規模なデータ ボリュームのエクスポートを処理するために使用されますが、SXSSFWorkbook は .xlsx 形式のみをサポートし、.xls 形式はサポートしません。
EasyExcel の概要
EasyExcel は、大きなファイルのメモリ オーバーフローを解決する、Java ベースの高速かつ簡潔な Excel 処理ツールです。
これにより、パフォーマンス、メモリ、その他の要素を考慮することなく、Excel の読み取り、書き込み、その他の機能を迅速に完了できます。
Java の解析と Excel 生成のためのより有名なフレームワークには、Apache poi と jxl があります。しかし、それらはすべて大量のメモリを消費するという深刻な問題を抱えています。POI にはメモリ オーバーフローの問題をある程度解決できる一連の SAX モード API がありますが、POI にはまだ、 07 バージョンの Excel。メモリ内で実行されるため、メモリの消費量は依然として非常に大きくなります。
easyexcel は、Excel の 07 バージョンの poi の分析を書き換えます。3M Excel でも、POI sax で解析するには約 100M のメモリが必要です。easyexcel を使用すると、それを数 MB に削減でき、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: 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
スタートアップクラスコード
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;//修改人
@ExcelIgnore
private int pageNum;//页数
@ExcelIgnore
private 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 {
@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();
}
}
}
インターフェイス層 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 {
@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);
}
}
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,modifier
from order_info
order by id
limit #{
pageNum},#{
pageSize}
</select>
</mapper>
テスト結果は次のとおりです
ブラウザ呼び出しインターフェイスは次のように返されます:
バックグラウンド コンソールは次のように出力されます:
ページが深くなるほど、クエリにかかる時間は長くなります。22
:33:01 から 22:36:52 まで、すべてのクエリが完了するまでに 4 分近くかかりました。データをエクスポートします。ここでのパフォーマンスのボトルネックは SQL の最適化です。500 万のデータをより速くエクスポートできる
ように、ページを深くめくるときの SQL パフォーマンスを向上させるにはどうすればよいでしょうか?
Excel ファイルの結果は次のようになります
。ID は 1 から 5,000,000 まで増加し、各シート ページには 100 万件のデータがあります。