找回密码
 立即注册
首页 业界区 业界 SpringBoot系列之集成EasyExcel实现百万级别的数据导入 ...

SpringBoot系列之集成EasyExcel实现百万级别的数据导入导出实践

寇秀娟 2025-8-4 09:41:07
Spring Boot 系列:集成 EasyExcel 实现百万级数据导入导出实战


  • SpringBoot与微服务实践系列博客
本文基于开源项目 springboot-easyexcel-batch 进行解析与扩展,手把手教你如何在 Spring Boot 2.2.1 中集成 Alibaba EasyExcel,轻松实现 百万级数据的导入与导出
目录


  • 项目结构概览
  • 核心依赖
  • 百万级导出实战
  • 百万级导入实战
  • 性能优化技巧
  • 常见问题 & 解决方案
  • 总结
项目结构概览
  1. springboot-easyexcel-batch
  2. ├── src/main/java/com/example/easyexcel
  3. │   ├── controller/      # 导入导出接口
  4. │   ├── listener/        # 导入监听器
  5. │   ├── model/           # 实体类
  6. │   ├── service/         # 业务逻辑
  7. │   └── Application.java # 启动类
  8. └── src/main/resources
  9.     ├── application.yml  # 线程池配置
  10.     └── templates/       # 前端demo
复制代码
核心依赖
  1. <parent>
  2.     <groupId>org.springframework.boot</groupId>
  3.     spring-boot-starter-parent</artifactId>
  4.     <version>2.2.1.RELEASE</version>
  5. </parent>
  6. <dependency>
  7.     <groupId>com.alibaba</groupId>
  8.     easyexcel</artifactId>
  9.     <version>2.2.11</version>
  10. </dependency>
复制代码
百万级导出实战

1️⃣ 场景

需求数据量策略导出用户表100万+分Sheet + 分批查询 + 边查边写2️⃣ 核心代码
  1. package com.example.easyexcel.service;
  2. import com.alibaba.excel.EasyExcel;
  3. import com.alibaba.excel.ExcelWriter;
  4. import com.alibaba.excel.write.metadata.WriteSheet;
  5. import com.example.easyexcel.model.User;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
  8. import org.springframework.stereotype.Service;
  9. import javax.servlet.http.HttpServletResponse;
  10. import java.io.IOException;
  11. import java.io.OutputStream;
  12. import java.net.URLEncoder;
  13. import java.util.ArrayList;
  14. import java.util.List;
  15. import java.util.concurrent.CompletableFuture;
  16. @Service
  17. @Slf4j
  18. public class ExcelExportService {
  19.     private final ThreadPoolTaskExecutor excelExecutor;
  20.     private final UserService userService;
  21.     // 每个Sheet的数据量
  22.     private static final int DATA_PER_SHEET = 100000;
  23.     // 每次查询的数据量
  24.     private static final int QUERY_BATCH_SIZE = 10000;
  25.     public ExcelExportService(ThreadPoolTaskExecutor excelExecutor, UserService userService) {
  26.         this.excelExecutor = excelExecutor;
  27.         this.userService = userService;
  28.     }
  29.     /**
  30.      * 导出百万级用户数据(优化内存版本)
  31.      */
  32.     public void exportMillionUsers(HttpServletResponse response, long totalCount) throws IOException {
  33.         // 设置响应头
  34.         response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
  35.         response.setCharacterEncoding("utf-8");
  36.         String fileName = URLEncoder.encode("百万用户数据", "UTF-8").replaceAll("\\+", "%20");
  37.         response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
  38.         response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
  39.         response.setHeader("Pragma", "no-cache");
  40.         response.setDateHeader("Expires", 0);
  41.         // 计算总Sheet数
  42.         int sheetCount = (int) (totalCount / DATA_PER_SHEET + (totalCount % DATA_PER_SHEET > 0 ? 1 : 0));
  43.         log.info("需要生成的Sheet总数:{}", sheetCount);
  44.         try (OutputStream os = response.getOutputStream()) {
  45.             // 创建ExcelWriter,直接写入响应输出流
  46.             ExcelWriter excelWriter = EasyExcel.write(os, User.class).build();
  47.             // 用于保证Sheet写入顺序的前一个Future
  48.             CompletableFuture<Void> previousFuture = CompletableFuture.completedFuture(null);
  49.             for (int sheetNo = 0; sheetNo < sheetCount; sheetNo++) {
  50.                 final int currentSheetNo = sheetNo;
  51.                 long start = currentSheetNo * (long) DATA_PER_SHEET;
  52.                 long end = Math.min((currentSheetNo + 1) * (long) DATA_PER_SHEET, totalCount);
  53.                 // 每个Sheet的处理依赖于前一个Sheet完成,保证顺序
  54.                 previousFuture = previousFuture.thenRunAsync(() -> {
  55.                     try {
  56.                         log.info("开始处理Sheet {} 的数据({} - {})", currentSheetNo, start, end);
  57.                         writeSheetData(excelWriter, currentSheetNo, start, end);
  58.                         log.info("完成处理Sheet {} 的数据", currentSheetNo);
  59.                     } catch (Exception e) {
  60.                         log.error("处理Sheet {} 数据失败", currentSheetNo, e);
  61.                         throw new RuntimeException("处理Sheet " + currentSheetNo + " 数据失败", e);
  62.                     }
  63.                 }, excelExecutor);
  64.             }
  65.             // 等待所有Sheet处理完成
  66.             previousFuture.join();
  67.             // 完成写入
  68.             excelWriter.finish();
  69.             log.info("所有Sheet写入完成");
  70.         } catch (Exception e) {
  71.             log.error("Excel导出失败", e);
  72.             throw e;
  73.         }
  74.     }
  75.     /**
  76.      * 写入单个Sheet的数据
  77.      */
  78.     private void writeSheetData(ExcelWriter excelWriter, int sheetNo, long start, long end) {
  79.         String sheetName = "用户数据" + (sheetNo + 1);
  80.         WriteSheet writeSheet = EasyExcel.writerSheet(sheetNo, sheetName).build();
  81.         long totalToQuery = end - start;
  82.         int totalWritten = 0;
  83.         // 分批查询并写入,每批查询后立即写入,不缓存大量数据
  84.         for (long i = 0; i < totalToQuery; i += QUERY_BATCH_SIZE) {
  85.             long currentStart = start + i;
  86.             long currentEnd = Math.min(start + i + QUERY_BATCH_SIZE, end);
  87.             // 调用UserService查询数据
  88.             List<User> batchData = userService.findUsersByRange(currentStart, currentEnd);
  89.             if (batchData == null || batchData.isEmpty()) {
  90.                 log.info("{} - {} 范围没有数据", currentStart, currentEnd);
  91.                 break; // 没有更多数据,提前退出
  92.             }
  93.             // 直接写入这一批数据
  94.             excelWriter.write(batchData, writeSheet);
  95.             totalWritten += batchData.size();
  96.             log.info("Sheet {} 已写入 {} - {} 范围的数据,累计 {} 条",
  97.                     sheetName, currentStart, currentEnd, totalWritten);
  98.             // 清除引用,帮助GC
  99.             batchData = new ArrayList<>();
  100.         }
  101.         log.info("Sheet {} 写入完成,共 {} 条数据", sheetName, totalWritten);
  102.     }
  103. }
复制代码
3️⃣ 效果

指标优化前优化后内存峰值1.2GB100MB耗时45s18s百万级导入实战

1️⃣ 场景

需求数据量策略导入用户表100万+分Sheet + 监听器 + 批量插入2️⃣ 监听器和Service(核心)
  1. package com.example.easyexcel.listener;
  2. import com.alibaba.excel.context.AnalysisContext;
  3. import com.alibaba.excel.event.AnalysisEventListener;
  4. import com.example.easyexcel.model.User;
  5. import com.example.easyexcel.service.UserService;
  6. import lombok.extern.slf4j.Slf4j;
  7. import java.util.ArrayList;
  8. import java.util.List;
  9. import java.util.concurrent.atomic.AtomicLong;
  10. /**
  11. * 用户数据导入监听器(独立类实现)
  12. */
  13. @Slf4j
  14. public class UserImportListener extends AnalysisEventListener<User> {
  15.     // 批量保存阈值(可根据内存调整)
  16.     private static final int BATCH_SIZE = 5000;
  17.    
  18.     // 临时存储批次数据
  19.     private final List<User> batchList = new ArrayList<>(BATCH_SIZE);
  20.    
  21.     // 导入结果统计
  22.     private final AtomicLong successCount = new AtomicLong(0);
  23.     private final AtomicLong failCount = new AtomicLong(0);
  24.    
  25.     // 业务服务(通过构造器注入)
  26.     private final UserService userService;
  27.     public UserImportListener(UserService userService) {
  28.         this.userService = userService;
  29.     }
  30.     /**
  31.      * 每读取一行数据触发
  32.      */
  33.     @Override
  34.     public void invoke(User user, AnalysisContext context) {
  35.         // 数据验证
  36.         if (validateUser(user)) {
  37.             batchList.add(user);
  38.             successCount.incrementAndGet();
  39.             
  40.             // 达到批次大小则保存
  41.             if (batchList.size() >= BATCH_SIZE) {
  42.                 saveBatchData();
  43.                 // 清空列表释放内存
  44.                 batchList.clear();
  45.             }
  46.         } else {
  47.             failCount.incrementAndGet();
  48.             log.warn("数据验证失败: {}", user);
  49.         }
  50.     }
  51.     /**
  52.      * 所有数据读取完成后触发
  53.      */
  54.     @Override
  55.     public void doAfterAllAnalysed(AnalysisContext context) {
  56.         // 处理剩余数据
  57.         if (!batchList.isEmpty()) {
  58.             saveBatchData();
  59.             batchList.clear();
  60.         }
  61.         log.info("当前Sheet导入结束,成功: {}, 失败: {}", successCount.get(), failCount.get());
  62.     }
  63.     /**
  64.      * 批量保存数据
  65.      */
  66.     private void saveBatchData() {
  67.         try {
  68.             // 调用业务层批量保存(带事务)
  69.             userService.batchSaveUsers(batchList);
  70.             log.debug("批量保存成功,数量: {}", batchList.size());
  71.         } catch (Exception e) {
  72.             log.error("批量保存失败,数量: {}", batchList.size(), e);
  73.             // 失败处理:可记录失败数据到文件或数据库
  74.             handleSaveFailure(batchList);
  75.         }
  76.     }
  77.     /**
  78.      * 数据验证逻辑
  79.      */
  80.     private boolean validateUser(User user) {
  81.         // 基础字段验证(根据实际业务调整)
  82.         if (user == null) return false;
  83.         if (user.getId() == null) return false;
  84.         if (user.getName() == null || user.getName().trim().isEmpty()) return false;
  85.         return true;
  86.     }
  87.     /**
  88.      * 处理保存失败的数据
  89.      */
  90.     private void handleSaveFailure(List<User> failedData) {
  91.         // 实现失败数据的处理逻辑(例如写入失败日志表)
  92.         // userService.saveFailedData(failedData);
  93.     }
  94.     // Getter方法用于统计结果
  95.     public long getSuccessCount() {
  96.         return successCount.get();
  97.     }
  98.     public long getFailCount() {
  99.         return failCount.get();
  100.     }
  101. }
复制代码
导入Service类
  1. package com.example.easyexcel.service;
  2. import com.alibaba.excel.EasyExcel;
  3. import com.alibaba.excel.support.ExcelTypeEnum;
  4. import com.example.easyexcel.listener.SheetCountListener;
  5. import com.example.easyexcel.listener.UserImportListener;
  6. import com.example.easyexcel.model.User;
  7. import lombok.extern.slf4j.Slf4j;
  8. import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
  9. import org.springframework.stereotype.Service;
  10. import org.springframework.web.multipart.MultipartFile;
  11. import java.io.IOException;
  12. import java.io.InputStream;
  13. import java.util.ArrayList;
  14. import java.util.List;
  15. import java.util.concurrent.CompletableFuture;
  16. import java.util.concurrent.atomic.AtomicLong;
  17. /**
  18. * 百万级Excel数据导入服务
  19. */
  20. @Service
  21. @Slf4j
  22. public class ExcelImportService {
  23.     private final ThreadPoolTaskExecutor excelExecutor;
  24.     private final UserService userService;
  25.     public ExcelImportService(ThreadPoolTaskExecutor excelExecutor, UserService userService) {
  26.         this.excelExecutor = excelExecutor;
  27.         this.userService = userService;
  28.     }
  29.     /**
  30.      * 多线程导入百万级用户数据(每个Sheet一个线程)
  31.      */
  32.     public void importMillionUsers(MultipartFile file) throws IOException {
  33.         // 1. 保存成临时文件,避免多线程共用 InputStream
  34.         java.io.File tmpFile = java.io.File.createTempFile("excel_", ".xlsx");
  35.         file.transferTo(tmpFile);          // Spring 提供的零拷贝
  36.         tmpFile.deleteOnExit();            // JVM 退出时自动清理
  37.         ExcelTypeEnum excelType = getExcelType(file.getOriginalFilename());
  38.         // 2. 拿 sheet 数量
  39.         int sheetCount;
  40.         try (InputStream in = new java.io.FileInputStream(tmpFile)) {
  41.             sheetCount = getSheetCount(in);
  42.         }
  43.         log.info("开始导入,总 Sheet 数: {}", sheetCount);
  44.         // 3. 并发读,每个 Sheet 独立 FileInputStream
  45.         AtomicLong totalSuccess = new AtomicLong(0);
  46.         AtomicLong totalFail    = new AtomicLong(0);
  47.         List<CompletableFuture<Void>> futures = new ArrayList<>(sheetCount);
  48.         for (int sheetNo = 0; sheetNo < sheetCount; sheetNo++) {
  49.             final int idx = sheetNo;
  50.             futures.add(CompletableFuture.runAsync(() -> {
  51.                 try (InputStream in = new java.io.FileInputStream(tmpFile)) {
  52.                     UserImportListener listener = new UserImportListener(userService);
  53.                     EasyExcel.read(in, User.class, listener)
  54.                             .excelType(excelType)
  55.                             .sheet(idx)
  56.                             .doRead();
  57.                     totalSuccess.addAndGet(listener.getSuccessCount());
  58.                     totalFail.addAndGet(listener.getFailCount());
  59.                     log.info("Sheet {} 完成,成功: {}, 失败: {}", idx, listener.getSuccessCount(), listener.getFailCount());
  60.                 } catch (IOException e) {
  61.                     throw new RuntimeException("Sheet " + idx + " 读取失败", e);
  62.                 }
  63.             }, excelExecutor));
  64.         }
  65.         CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
  66.         log.info("全部导入完成,总成功: {},总失败: {}", totalSuccess.get(), totalFail.get());
  67.     }
  68.     /**
  69.      * 获取Excel中的Sheet数量
  70.      */
  71.     private int getSheetCount(InputStream inputStream) {
  72.         SheetCountListener countListener = new SheetCountListener();
  73.         EasyExcel.read(inputStream)
  74.                 .registerReadListener(countListener)
  75.                 .doReadAll();
  76.         return countListener.getSheetCount();
  77.     }
  78.     /**
  79.      * 获取Excel文件类型
  80.      *
  81.      */
  82.     public ExcelTypeEnum getExcelType(String fileName) {
  83.         if (fileName == null) return null;
  84.         if (fileName.toLowerCase().endsWith(".xlsx")) {
  85.             return ExcelTypeEnum.XLSX;
  86.         } else if (fileName.toLowerCase().endsWith(".xls")) {
  87.             return ExcelTypeEnum.XLS;
  88.         }
  89.         return null;
  90.     }
  91. }
复制代码
3️⃣ Controller
  1. @PostMapping("/import")
  2. @ApiOperation("导入用户数据")
  3. public ResponseEntity<String> importUsers(@RequestParam("file") MultipartFile file) {
  4.     try {
  5.         if (file.isEmpty()) {
  6.             return ResponseEntity.badRequest().body("请选择要导入的文件");
  7.         }
  8.         String fileName = file.getOriginalFilename();
  9.         ExcelTypeEnum excelType = importService.getExcelType(fileName);
  10.         if (excelType == null) {
  11.             return ResponseEntity.badRequest().body("不支持的文件类型,文件名:" +  fileName);
  12.         }
  13.         importService.importMillionUsers(file);
  14.         return ResponseEntity.ok("文件导入成功,正在后台处理数据");
  15.     } catch (Exception e) {
  16.         log.error("导入用户数据失败", e);
  17.         return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
  18.                 .body("导入失败:" + e.getMessage());
  19.     }
  20. }
复制代码
性能优化技巧

技巧说明分批查询避免一次性加载全表分批写入每5k条批量插入临时文件并发读时先 MultipartFile.transferTo(tmp)线程池配置专用线程池,隔离业务线程
  1. # application.yml
  2. spring:
  3.   task:
  4.     execution:
  5.       pool:
  6.         core-size: 10
  7.         max-size: 30
  8.         queue-capacity: 1000
复制代码
常见问题 & 解决方案

问题解决方案Can not create temporary file!并发读时先保存临时文件,再独立流读取Stream Closed每个任务独立 InputStreamOutOfMemoryError分批处理 + 及时 clear()总结


Spring Boot + EasyExcel零侵入 的情况下即可完成百万级数据的导入导出。
✅ 通过 分批、并发、顺序写 等技巧,内存占用降低 90% 以上。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册