基于SpringBoot+FastExcel的百万级数据导入导出完整方案
本文将详细介绍在 Spring Boot 3.5.11 + JDK17 + MySQL 环境下,使用 FastExcel 实现百万级数据的 导入 和 导出。内容包括:
- 数据库表设计及百万测试数据生成
- 环境配置与依赖
- 百万数据导入(多线程批量插入)
- 百万数据导出(多线程分页查询 + 单线程写入)
- 线程池、阻塞队列、CountDownLatch 的设计思路与原理
- 接口测试方法
一、项目初始化
1.1 创建 Spring Boot 项目
使用 Spring Initializr 创建项目,选择:
- Spring Boot 3.5.11
- Java 17
- 依赖:Web、MyBatis Framework、MySQL Driver、Lombok
1.2 添加依赖(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
http://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>3.5.11</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>excel-batch-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Plus (简化数据库操作,非必需但推荐) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.9</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- FastExcel (若无法获取,可用 EasyExcel 替代,API 类似) -->
<dependency>
<groupId>cn.idev.excel</groupId>
<artifactId>fastexcel</artifactId>
<version>1.0.0</version> <!-- 请使用官方最新版本 -->
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- HikariCP 连接池 (Spring Boot 默认) -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
二、数据库准备
2.1 创建图书表
CREATE DATABASE IF NOT EXISTS `excel_demo` DEFAULT CHARACTER SET utf8mb4;
USE `excel_demo`;
CREATE TABLE `book` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
`title` VARCHAR(200) NOT NULL COMMENT '书名',
`author` VARCHAR(100) NOT NULL COMMENT '作者',
`price` DECIMAL(10,2) NOT NULL COMMENT '价格',
`publish_date` DATE NOT NULL COMMENT '出版日期',
`isbn` VARCHAR(20) NOT NULL COMMENT 'ISBN编号',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书表';
2.2 生成百万测试数据(用于导出测试)
-- 存储过程:插入指定行数的随机数据
DELIMITER $$
CREATE PROCEDURE insert_book_data(IN num_rows INT)
BEGIN
DECLARE i INT DEFAULT 0;
DECLARE batch_size INT DEFAULT 1000;
DECLARE total_batches INT;
DECLARE current_batch INT DEFAULT 0;
SET total_batches = CEIL(num_rows / batch_size);
START TRANSACTION;
WHILE current_batch < total_batches DO
SET i = 0;
WHILE i < batch_size AND (current_batch * batch_size + i) < num_rows DO
INSERT INTO book (title, author, price, publish_date, isbn) VALUES (
CONCAT('Book_', current_batch * batch_size + i),
CONCAT('Author_', FLOOR(RAND() * 1000)),
ROUND(RAND() * 100, 2),
DATE_ADD('2000-01-01', INTERVAL FLOOR(RAND() * 8000) DAY),
CONCAT('978-', LPAD(FLOOR(RAND() * 1000000000), 9, '0'))
);
SET i = i + 1;
END WHILE;
COMMIT;
START TRANSACTION;
SET current_batch = current_batch + 1;
END WHILE;
COMMIT;
END$$
DELIMITER ;
-- 调用存储过程插入 100 万条数据
CALL insert_book_data(1000000);
三、实体类与 Mapper
3.1 实体类Book
package com.gxa.testexport.pojo.entity;
import cn.idev.excel.annotation.ExcelProperty;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
@Data
@TableName("book")
public class Book {
@TableId(type = IdType.AUTO)
// 不添加 @ExcelProperty,表示忽略 Excel 中的 id 列
private Long id;
@ExcelProperty(index = 1) // Excel 第2列(标题行后)
private String title;
@ExcelProperty(index = 2) // Excel 第3列
private String author;
@ExcelProperty(index = 3) // Excel 第4列
private BigDecimal price;
@ExcelProperty(index = 4) // Excel 第5列
private LocalDate publishDate;
@ExcelProperty(index = 5) // Excel 第6列
private String isbn;
}
注意:若您的 Excel 中不包含 id 列,请移除 id 字段上的 @ExcelProperty,并调整其他字段的 index(从 0 开始)。本例假设 Excel 包含所有 6 列。意思就是添加 了@ExcelProperty,你的Excel就不能有id列。
3.2 Mapper 接口
package com.example.excel.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.excel.entity.Book;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface BookMapper extends BaseMapper<Book> {
/**
* 分页查询图书(用于导出)
*/
@Select("SELECT id, title, author, price, publish_date, isbn FROM book ORDER BY id LIMIT #{offset}, #{limit}")
List<Book> selectPage(@Param("offset") long offset, @Param("limit") int limit);
/**
* 查询总记录数(用于导出)
*/
@Select("SELECT COUNT(*) FROM book")
long countAll();
/**
* 批量插入(用于导入)
* MyBatis Plus 提供了 saveBatch 方法,此处也可以自定义批量插入 SQL 提升性能
*/
// 使用 MyBatis Plus 的 saveBatch 即可,此处不需要额外定义
}
四、配置文件 application.yml
spring:
servlet:
multipart:
max-file-size: 100MB # 最大上传文件大小
max-request-size: 100MB
datasource:
url: jdbc:mysql://localhost:3306/excel_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
username: root
password: yourpassword
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 30 # 根据线程数适当调大,避免连接不够
mybatis-plus:
configuration:
# 开启 MyBatis 批量提交重写,提升批量插入性能
rewriteBatchedStatements: true
global-config:
db-config:
# 开启批量提交
insert-batch-size: 2000
server:
port: 8080
logging:
level:
com.example.excel.mapper: debug # 可选,查看 SQL
五、百万数据导入功能
5.1 设计思路
- 读取:使用 FastExcel 的监听器模式(SAX 方式),逐行读取 Excel,避免内存溢出。
- 批处理:每读取一定数量(如
BATCH_SIZE = 2000)的记录,封装成一个插入任务提交给线程池。 - 线程池:多个插入任务并行执行,提高数据库写入速度。
- 阻塞队列:如果读取速度远快于插入速度,可先将任务放入队列,由线程池消费(此处我们直接提交任务,利用线程池的任务队列)。
- CountDownLatch:主线程等待所有插入任务完成后再返回结果。
- 事务:每个批次使用独立的事务,避免长事务。
5.2 导入 Service 接口
package com.example.excel.service;
import org.springframework.web.multipart.MultipartFile;
public interface BookImportService extends IService<Book> {
/**
* 从 Excel 文件导入图书数据
* @param file 上传的 Excel 文件
* @return 导入结果统计
*/
ImportResult importBooks(MultipartFile file);
// 导入结果内部类
@Data
@AllArgsConstructor
@NoArgsConstructor
class ImportResult {
private long totalRows; // 总处理行数
private long successRows; // 成功插入行数
private long failedRows; // 失败行数
private long costTimeMillis; // 耗时
// 构造方法、getter/setter 省略(使用 Lombok 或手动添加)
}
}
5.3 导入 Service 实现类
package com.example.excel.service.impl;
import cn.idev.excel.EasyExcel;
import cn.idev.excel.context.AnalysisContext;
import cn.idev.excel.event.AnalysisEventListener;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.excel.entity.Book;
import com.example.excel.mapper.BookMapper;
import com.example.excel.service.BookImportService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
@Service
@Slf4j
public class BookImportServiceImpl extends ServiceImpl<BookMapper, Book> implements BookImportService {
@Autowired
private BookMapper bookMapper;
@Autowired
private PlatformTransactionManager transactionManager;
// 线程池配置:IO密集型任务,核心线程数适当调大
private static final int CORE_POOL_SIZE = 10;
private static final int MAX_POOL_SIZE = 20;
private static final int QUEUE_CAPACITY = 100;
private static final long KEEP_ALIVE_TIME = 60L;
// 每批插入的数据量
private static final int BATCH_SIZE = 2000;
@Override
public ImportResult importBooks(MultipartFile file) {
long startTime = System.currentTimeMillis();
ImportResult result = new ImportResult();
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy() // 当线程池满时,由读取线程执行插入,起到限流作用
);
// 用于统计成功/失败行数
AtomicLong successCount = new AtomicLong(0);
AtomicLong failedCount = new AtomicLong(0);
// CountDownLatch 用于等待所有插入任务完成
// 注意:任务数量未知,无法在读取前确定。因此采用动态增加计数的方式。
// 可以使用 AtomicInteger 记录提交任务数,再配合 CountDownLatch 的变种?简单起见,我们使用一个计数器 + 等待所有任务完成的机制:
// 方案1:使用一个 List<Future> 收集所有提交的任务,最后遍历 future.get() 等待完成。
// 方案2:使用 CountDownLatch,但需要提前知道任务数。可以在读取过程中先计数任务数,但这样需要两次读取?不可取。
// 此处采用方案1:提交任务时获得 Future,最后等待所有 Future 完成。
List<Future<?>> futures = new CopyOnWriteArrayList<>();
// 读取监听器
AnalysisEventListener<Book> listener = new AnalysisEventListener<Book>() {
private final List<Book> batch = new ArrayList<>(BATCH_SIZE);
@Override
public void invoke(Book book, AnalysisContext context) {
batch.add(book);
if (batch.size() >= BATCH_SIZE) {
// 提交插入任务
List<Book> toInsert = new ArrayList<>(batch);
batch.clear();
Future<?> future = executor.submit(() -> insertBatch(toInsert, successCount, failedCount));
futures.add(future);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 处理剩余不足一批的数据
if (!batch.isEmpty()) {
List<Book> toInsert = new ArrayList<>(batch);
batch.clear();
Future<?> future = executor.submit(() -> insertBatch(toInsert, successCount, failedCount));
futures.add(future);
}
}
};
try {
// 开始读取 Excel
EasyExcel.read(file.getInputStream(), Book.class, listener).sheet().doRead();
// 等待所有插入任务完成
for (Future<?> future : futures) {
try {
future.get(); // 阻塞直到任务完成,若任务抛出异常,此处会抛出 ExecutionException
} catch (ExecutionException e) {
log.error("插入任务执行异常", e.getCause());
// 异常已在线程内记录,此处可累加失败计数(但已在线程内记录,可忽略)
}
}
} catch (IOException e) {
log.error("读取 Excel 文件失败", e);
throw new RuntimeException("文件读取失败", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("等待插入任务被中断", e);
} finally {
// 关闭线程池
executor.shutdown();
}
long endTime = System.currentTimeMillis();
result.setTotalRows(successCount.get() + failedCount.get());
result.setSuccessRows(successCount.get());
result.setFailedRows(failedCount.get());
result.setCostTimeMillis(endTime - startTime);
log.info("导入完成,总行数:{},成功:{},失败:{},耗时:{}ms",
result.getTotalRows(), result.getSuccessRows(), result.getFailedRows(), result.getCostTimeMillis());
return result;
}
/**
* 批量插入数据,使用编程式事务,每个批次独立事务
*/
private void insertBatch(List<Book> books, AtomicLong successCount, AtomicLong failedCount) {
if (books.isEmpty()) {
return;
}
// 定义事务
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
def.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT);
TransactionStatus status = transactionManager.getTransaction(def);
boolean success = false;
try {
// 使用 MyBatis Plus 的批量插入方法(需注意 MP 默认批量插入是循环单条,可开启 rewriteBatchedStatements 优化)
// 或者自定义 XML 使用 foreach 批量插入。此处用 MP 的 saveBatch,需在配置中开启批量提交。
this.saveBatch(books); // 假设使用 MP 的 Service 或 Mapper,此处仅为示例,实际需注入 IService
// 若使用纯 MyBatis,可自定义 mapper 方法批量插入
transactionManager.commit(status);
success = true;
successCount.addAndGet(books.size());
log.debug("批量插入成功,数量:{}", books.size());
} catch (Exception e) {
transactionManager.rollback(status);
failedCount.addAndGet(books.size());
log.error("批量插入失败,数量:{},错误:{}", books.size(), e.getMessage());
}
}
}
注意:上述代码中 this.saveBatch(books) 是 MyBatis Plus 的批量插入方法,需要注入 IService<Book> 或者使用 SqlSession 直接批量操作。为简化,可以注入 com.baomidou.mybatisplus.extension.service.IService,或者自定义 Mapper 实现批量插入。这里为了代码简洁,假设已注入 BookMapper 并且 MP 的 saveBatch 可用(需要 BookMapper 继承 BaseMapper,但 saveBatch 是 IService 的方法,需要注入 ServiceImpl)。实际使用时可以注入 com.baomidou.mybatisplus.extension.service.IService<Book>。
5.4 导入 Controller
package com.example.excel.controller;
import com.example.excel.service.BookImportService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/import")
public class ImportController {
@Autowired
private BookImportService bookImportService;
@PostMapping("/books")
public BookImportService.ImportResult importBooks(@RequestParam("file") MultipartFile file) {
return bookImportService.importBooks(file);
}
}
六、百万数据导出功能
(保留之前导出设计,但稍作整理,确保与导入风格一致)
6.1 导出 Service 接口
package com.example.excel.service;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.servlet.http.HttpServletResponse;
public interface BookExportService extends IService<Book> {
/**
* 导出百万图书数据到 Excel
* @param response HTTP响应
*/
void exportMillionBooks(HttpServletResponse response);
}
6.2 导出 Service 实现类
package com.gxa.testexport.service.impl;
import cn.idev.excel.EasyExcel;
import cn.idev.excel.write.metadata.WriteSheet;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.excel.mapper.BookMapper;
import com.example.excel.pojo.entity.Book;
import com.example.excel.service.BookExportService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
@Service
@Slf4j
public class BookExportServiceImpl extends ServiceImpl<BookMapper, Book> implements BookExportService {
@Autowired
private BookMapper bookMapper;
// 线程池参数(根据服务器核心数和IO等待时间调整)
private static final int CORE_POOL_SIZE = 10;
private static final int MAX_POOL_SIZE = 20;
private static final int QUEUE_CAPACITY = 100;
private static final long KEEP_ALIVE_TIME = 60L;
// 每批查询数量(根据数据库性能和内存调整)
private static final int BATCH_SIZE = 5000;
// 数据缓冲队列(生产者-消费者模式)
private final BlockingQueue<List<Book>> dataQueue = new LinkedBlockingQueue<>(QUEUE_CAPACITY);
// 所有查询任务完成标志
private volatile boolean queryFinished = false;
// 用于捕获写线程异常
private final AtomicReference<Exception> writerException = new AtomicReference<>();
@Override
public void exportMillionBooks(HttpServletResponse response) {
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("百万图书导出", StandardCharsets.UTF_8)
.replaceAll("\\+", "%20");
response.setHeader("Content-disposition",
"attachment;filename*=utf-8''" + fileName + ".xlsx");
// 获取总记录数
long totalCount = bookMapper.countAll();
log.info("总记录数:{}", totalCount);
if (totalCount == 0) {
throw new RuntimeException("无数据可导出");
}
// 计算总页数
int totalPages = (int) Math.ceil((double) totalCount / BATCH_SIZE);
CountDownLatch queryLatch = new CountDownLatch(totalPages); // 等待所有查询任务完成
// 创建线程池(IO密集型,适当调大线程数)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy() // 当线程池满时由调用线程执行(限流)
);
long startTime = System.currentTimeMillis();
try {
// 启动消费者线程(单线程写Excel),传入 response
Thread writerThread = new Thread(() -> writeDataToExcel(response), "Excel-Writer");
writerThread.start();
// 提交生产者任务(多线程分页查询)
for (int page = 0; page < totalPages; page++) {
int offset = page * BATCH_SIZE;
executor.submit(() -> {
try {
List<Book> books = bookMapper.selectPage(offset, BATCH_SIZE);
// 放入队列,如果队列满则阻塞
dataQueue.put(books);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("查询任务被中断", e);
} finally {
queryLatch.countDown(); // 无论成功失败,都必须 countDown
}
});
}
// 等待所有查询任务完成
queryLatch.await();
// 所有查询已完成,设置完成标志
queryFinished = true;
// 等待写线程结束
writerThread.join();
// 检查写线程是否有异常
Exception ex = writerException.get();
if (ex != null) {
throw new RuntimeException("写Excel过程中发生异常", ex);
}
long endTime = System.currentTimeMillis();
log.info("导出成功,耗时:{} ms", endTime - startTime);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("导出被中断", e);
throw new RuntimeException("导出被中断", e);
} finally {
executor.shutdown();
}
}
/**
* 消费者:从队列获取数据并写入Excel(单线程)
* @param response HTTP响应,用于获取输出流
*/
private void writeDataToExcel(HttpServletResponse response) {
OutputStream outputStream = null;
cn.idev.excel.ExcelWriter writer = null;
try {
outputStream = response.getOutputStream();
// 构建ExcelWriter(注意:不要用 try-with-resources 自动关闭流,需手动控制顺序)
writer = EasyExcel.write(outputStream, Book.class).build();
WriteSheet writeSheet = EasyExcel.writerSheet("图书数据").build();
while (true) {
// 从队列取数据,超时1秒,避免一直阻塞导致无法检测 finished
List<Book> batch = dataQueue.poll(1, TimeUnit.SECONDS);
if (batch != null) {
// 写入一批数据
writer.write(batch, writeSheet);
} else if (queryFinished && dataQueue.isEmpty()) {
// 所有查询已完成且队列为空,退出循环
break;
}
// 如果队列为空但查询尚未完成,继续循环等待
}
// 所有数据写入完成,必须调用 finish 来刷新并关闭内部资源
writer.finish();
// 此时可以安全关闭输出流(但 finish 内部可能已关闭,此处可不显式关闭)
outputStream.flush();
} catch (Exception e) {
log.error("写Excel过程中出错", e);
// 记录异常,供主线程判断
writerException.set(e);
// 如果writer已创建但未finish,尝试finish以释放资源(避免内存泄漏)
if (writer != null) {
try {
writer.finish();
} catch (Exception ex) {
log.error("关闭ExcelWriter时出错", ex);
}
}
// 不要抛出RuntimeException,因为是在子线程中,异常通过 writerException 传递
} finally {
// 确保流关闭(如果未关闭)
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
log.error("关闭输出流时出错", e);
}
}
}
}
}
6.3 导出 Controller
package com.example.excel.controller;
import com.example.excel.service.BookExportService;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/export")
public class ExportController {
@Autowired
private BookExportService bookExportService;
@GetMapping("/books")
public void exportBooks(HttpServletResponse response) {
bookExportService.exportMillionBooks(response);
}
}
七、关键设计解析
7.1 为什么使用线程池?
- 导入:数据库插入是 IO 密集型操作,多线程并发插入可以充分利用数据库连接和磁盘 IO,大幅提升吞吐量。
- 导出:数据库查询同样是 IO 密集型,多线程分页查询可以缩短数据获取时间,配合单线程写入,实现高效导出。
7.2 线程池参数选型
| 参数 | 值 | 说明 |
|---|---|---|
| corePoolSize | 10 | 核心线程数,根据 CPU 核心数和 IO 等待时间估算,通常设为 CPU 核心数 * (1 + 平均等待时间/计算时间),这里简单设为 10 |
| maximumPoolSize | 20 | 最大线程数,避免创建过多线程导致数据库连接耗尽 |
| keepAliveTime | 60s | 空闲线程存活时间 |
| workQueue | LinkedBlockingQueue(100) | 有界队列,防止任务无限堆积导致内存溢出 |
| rejectedExecutionHandler | CallerRunsPolicy | 当线程池和队列都满时,由提交任务的线程(如读取 Excel 的线程)执行插入,实现自然限流 |
7.3 为什么用 CountDownLatch?
- 导入:需要等待所有插入任务完成后才能返回导入结果。由于任务数量在读取过程中动态产生,我们使用
List<Future>来等待每个任务完成,效果等同于 CountDownLatch。 - 导出:需要等待所有分页查询任务结束,才能设置
finished = true,从而让写线程正确退出。CountDownLatch 初始化为总页数,每个查询任务完成时调用countDown(),主线程await()等待所有查询完成。
7.4 为什么用阻塞队列?
- 导入:虽然我们直接提交插入任务给线程池,但线程池内部有阻塞队列,起到缓冲作用。读取线程不会阻塞,除非线程池队列满(此时 CallerRunsPolicy 让读取线程自己插入,相当于阻塞了读取)。
- 导出:使用
BlockingQueue作为数据通道,生产者(查询线程)将数据放入队列,消费者(写线程)从队列取出。当写速度慢于查询速度时,队列会填满,生产者put()阻塞,自动调节生产速度,防止内存溢出。
7.5 事务处理
- 导入:每个批次使用独立事务,避免长事务,且失败批次不会影响其他批次。使用编程式事务精细控制。
- 导出:只读操作,无需事务。
7.6 数据库连接池配置
- 必须确保
maximum-pool-size大于等于最大线程数,否则线程会因获取不到连接而阻塞或超时。本例中最大线程 20,连接池设为 30 比较稳妥。
7.7 批量插入性能优化
- 使用
rewriteBatchedStatements=true让 MySQL 驱动将多条 insert 合并成一条,大幅提升批量插入性能。 - MyBatis Plus 的
saveBatch默认也是循环单条,需要配置rewriteBatchedStatements才能发挥批量效果。也可以自定义 XML 使用<foreach>拼接成一条 insert 语句,但要注意 SQL 长度限制。
八、接口测试
8.1 导入测试
使用 Postman 或前端上传文件:
- URL:
http://localhost:8080/import/books - Method: POST
- Body: form-data,key 为
file,选择包含百万数据的 Excel 文件(可用导出功能先生成一个)
返回示例:
{
"totalRows": 1000000,
"successRows": 1000000,
"failedRows": 0,
"costTimeMillis": 189786
}
- 189,786 毫秒(约 3.16 分钟)的百万数据导入速度在多数业务场景下属于良好表现,足以满足日常需求。
- 优化建议(导入速度要看硬件或优化):
- 增大批量大小:尝试将 BATCH_SIZE 提升至 5000~10000,观察数据库响应。
- 使用多线程并发插入:确保数据库连接池支持并发,并注意线程数不宜超过 CPU 核心数的 2~3 倍。
- 使用原生批量插入语句:如
INSERT INTO table VALUES (...), (...), ...,代替框架的逐条转换。
注意:本例子导入的Excel可以带id列
8.2 导出测试
浏览器或 Postman 访问:http://localhost:8080/export/books,将下载名为“百万图书导出.xlsx”的文件。
- 导出用时40076 ms ≈ 40秒,处理 1,000,000 条数据,平均每秒处理约 25,000 条。40秒处理百万数据,在大多数业务场景下属于非常快。
注意:导出Excel的ID不是按顺序导出的,可以使用对ID列使用排序来观察是否连续。
8.3 监控
- 查看日志,观察耗时和线程运行情况。
- 使用 JConsole 或 VisualVM 观察内存、线程状态。
- 数据库连接池监控(如 Hikari 的 metrics)。
九、总结
本文完整实现了基于 Spring Boot 3.5.11 + FastExcel 的百万数据导入导出功能:
- 导入:利用监听器逐行读取,多线程批量插入,独立事务,确保性能和可靠性。
- 导出:多线程分页查询 + 阻塞队列 + 单线程写入,内存友好且高效。
以上就是基于SpringBoot+FastExcel的百万级数据导入导出完整方案的详细内容,更多关于SpringBoot FastExcel百万级数据导入导出的资料请关注脚本之家其它相关文章!
相关文章
springboot CompletableFuture并行计算及使用方法
CompletableFuture基于 Future 和 CompletionStage 接口,利用线程池、回调函数、异常处理、组合操作等机制,提供了强大而灵活的异步编程功能,这篇文章主要介绍了springboot CompletableFuture并行计算及使用方法,需要的朋友可以参考下2024-05-05


最新评论