<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12
      Fork me on GitHub

      SpringBoot系列之集成EasyExcel實(shí)現(xiàn)百萬級別的數(shù)據(jù)導(dǎo)入導(dǎo)出實(shí)踐

      Spring Boot 系列:集成 EasyExcel 實(shí)現(xiàn)百萬級數(shù)據(jù)導(dǎo)入導(dǎo)出實(shí)戰(zhàn)

      本文基于開源項(xiàng)目 springboot-easyexcel-batch 進(jìn)行解析與擴(kuò)展,手把手教你如何在 Spring Boot 2.2.1 中集成 Alibaba EasyExcel,輕松實(shí)現(xiàn) 百萬級數(shù)據(jù)的導(dǎo)入與導(dǎo)出


      目錄

      1. 項(xiàng)目結(jié)構(gòu)概覽
      2. 核心依賴
      3. 百萬級導(dǎo)出實(shí)戰(zhàn)
      4. 百萬級導(dǎo)入實(shí)戰(zhàn)
      5. 性能優(yōu)化技巧
      6. 常見問題 & 解決方案
      7. 總結(jié)

      項(xiàng)目結(jié)構(gòu)概覽

      springboot-easyexcel-batch
      ├── src/main/java/com/example/easyexcel
      │   ├── controller/      # 導(dǎo)入導(dǎo)出接口
      │   ├── listener/        # 導(dǎo)入監(jiān)聽器
      │   ├── model/           # 實(shí)體類
      │   ├── service/         # 業(yè)務(wù)邏輯
      │   └── Application.java # 啟動類
      └── src/main/resources
          ├── application.yml  # 線程池配置
          └── templates/       # 前端demo
      

      核心依賴

      <!-- Spring Boot 2.2.1 -->
      <parent>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-parent</artifactId>
          <version>2.2.1.RELEASE</version>
      </parent>
      
      <!-- EasyExcel 2.2.11(穩(wěn)定版) -->
      <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>easyexcel</artifactId>
          <version>2.2.11</version>
      </dependency>
      

      百萬級導(dǎo)出實(shí)戰(zhàn)

      1?? 場景

      需求 數(shù)據(jù)量 策略
      導(dǎo)出用戶表 100萬+ 分Sheet + 分批查詢 + 邊查邊寫

      2?? 核心代碼

      package com.example.easyexcel.service;
      
      import com.alibaba.excel.EasyExcel;
      import com.alibaba.excel.ExcelWriter;
      import com.alibaba.excel.write.metadata.WriteSheet;
      import com.example.easyexcel.model.User;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
      import org.springframework.stereotype.Service;
      
      import javax.servlet.http.HttpServletResponse;
      import java.io.IOException;
      import java.io.OutputStream;
      import java.net.URLEncoder;
      import java.util.ArrayList;
      import java.util.List;
      import java.util.concurrent.CompletableFuture;
      
      @Service
      @Slf4j
      public class ExcelExportService {
      
          private final ThreadPoolTaskExecutor excelExecutor;
          private final UserService userService;
      
          // 每個(gè)Sheet的數(shù)據(jù)量
          private static final int DATA_PER_SHEET = 100000;
      
          // 每次查詢的數(shù)據(jù)量
          private static final int QUERY_BATCH_SIZE = 10000;
      
          public ExcelExportService(ThreadPoolTaskExecutor excelExecutor, UserService userService) {
              this.excelExecutor = excelExecutor;
              this.userService = userService;
          }
      
          /**
           * 導(dǎo)出百萬級用戶數(shù)據(jù)(優(yōu)化內(nèi)存版本)
           */
          public void exportMillionUsers(HttpServletResponse response, long totalCount) throws IOException {
              // 設(shè)置響應(yīng)頭
              response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
              response.setCharacterEncoding("utf-8");
              String fileName = URLEncoder.encode("百萬用戶數(shù)據(jù)", "UTF-8").replaceAll("\\+", "%20");
              response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
              response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
              response.setHeader("Pragma", "no-cache");
              response.setDateHeader("Expires", 0);
      
              // 計(jì)算總Sheet數(shù)
              int sheetCount = (int) (totalCount / DATA_PER_SHEET + (totalCount % DATA_PER_SHEET > 0 ? 1 : 0));
              log.info("需要生成的Sheet總數(shù):{}", sheetCount);
      
              try (OutputStream os = response.getOutputStream()) {
                  // 創(chuàng)建ExcelWriter,直接寫入響應(yīng)輸出流
                  ExcelWriter excelWriter = EasyExcel.write(os, User.class).build();
      
                  // 用于保證Sheet寫入順序的前一個(gè)Future
                  CompletableFuture<Void> previousFuture = CompletableFuture.completedFuture(null);
      
                  for (int sheetNo = 0; sheetNo < sheetCount; sheetNo++) {
                      final int currentSheetNo = sheetNo;
                      long start = currentSheetNo * (long) DATA_PER_SHEET;
                      long end = Math.min((currentSheetNo + 1) * (long) DATA_PER_SHEET, totalCount);
      
                      // 每個(gè)Sheet的處理依賴于前一個(gè)Sheet完成,保證順序
                      previousFuture = previousFuture.thenRunAsync(() -> {
                          try {
                              log.info("開始處理Sheet {} 的數(shù)據(jù)({} - {})", currentSheetNo, start, end);
                              writeSheetData(excelWriter, currentSheetNo, start, end);
                              log.info("完成處理Sheet {} 的數(shù)據(jù)", currentSheetNo);
                          } catch (Exception e) {
                              log.error("處理Sheet {} 數(shù)據(jù)失敗", currentSheetNo, e);
                              throw new RuntimeException("處理Sheet " + currentSheetNo + " 數(shù)據(jù)失敗", e);
                          }
                      }, excelExecutor);
                  }
      
                  // 等待所有Sheet處理完成
                  previousFuture.join();
      
                  // 完成寫入
                  excelWriter.finish();
                  log.info("所有Sheet寫入完成");
      
              } catch (Exception e) {
                  log.error("Excel導(dǎo)出失敗", e);
                  throw e;
              }
          }
      
          /**
           * 寫入單個(gè)Sheet的數(shù)據(jù)
           */
          private void writeSheetData(ExcelWriter excelWriter, int sheetNo, long start, long end) {
              String sheetName = "用戶數(shù)據(jù)" + (sheetNo + 1);
              WriteSheet writeSheet = EasyExcel.writerSheet(sheetNo, sheetName).build();
      
              long totalToQuery = end - start;
              int totalWritten = 0;
      
              // 分批查詢并寫入,每批查詢后立即寫入,不緩存大量數(shù)據(jù)
              for (long i = 0; i < totalToQuery; i += QUERY_BATCH_SIZE) {
                  long currentStart = start + i;
                  long currentEnd = Math.min(start + i + QUERY_BATCH_SIZE, end);
      
                  // 調(diào)用UserService查詢數(shù)據(jù)
                  List<User> batchData = userService.findUsersByRange(currentStart, currentEnd);
      
                  if (batchData == null || batchData.isEmpty()) {
                      log.info("{} - {} 范圍沒有數(shù)據(jù)", currentStart, currentEnd);
                      break; // 沒有更多數(shù)據(jù),提前退出
                  }
      
                  // 直接寫入這一批數(shù)據(jù)
                  excelWriter.write(batchData, writeSheet);
                  totalWritten += batchData.size();
      
                  log.info("Sheet {} 已寫入 {} - {} 范圍的數(shù)據(jù),累計(jì) {} 條",
                          sheetName, currentStart, currentEnd, totalWritten);
      
                  // 清除引用,幫助GC
                  batchData = new ArrayList<>();
              }
      
              log.info("Sheet {} 寫入完成,共 {} 條數(shù)據(jù)", sheetName, totalWritten);
          }
      }
      
      
      

      3?? 效果

      指標(biāo) 優(yōu)化前 優(yōu)化后
      內(nèi)存峰值 1.2GB 100MB
      耗時(shí) 45s 18s

      百萬級導(dǎo)入實(shí)戰(zhàn)

      1?? 場景

      需求 數(shù)據(jù)量 策略
      導(dǎo)入用戶表 100萬+ 分Sheet + 監(jiān)聽器 + 批量插入

      2?? 監(jiān)聽器和Service(核心)

      package com.example.easyexcel.listener;
      
      import com.alibaba.excel.context.AnalysisContext;
      import com.alibaba.excel.event.AnalysisEventListener;
      import com.example.easyexcel.model.User;
      import com.example.easyexcel.service.UserService;
      import lombok.extern.slf4j.Slf4j;
      
      import java.util.ArrayList;
      import java.util.List;
      import java.util.concurrent.atomic.AtomicLong;
      
      /**
       * 用戶數(shù)據(jù)導(dǎo)入監(jiān)聽器(獨(dú)立類實(shí)現(xiàn))
       */
      @Slf4j
      public class UserImportListener extends AnalysisEventListener<User> {
      
          // 批量保存閾值(可根據(jù)內(nèi)存調(diào)整)
          private static final int BATCH_SIZE = 5000;
          
          // 臨時(shí)存儲批次數(shù)據(jù)
          private final List<User> batchList = new ArrayList<>(BATCH_SIZE);
          
          // 導(dǎo)入結(jié)果統(tǒng)計(jì)
          private final AtomicLong successCount = new AtomicLong(0);
          private final AtomicLong failCount = new AtomicLong(0);
          
          // 業(yè)務(wù)服務(wù)(通過構(gòu)造器注入)
          private final UserService userService;
      
          public UserImportListener(UserService userService) {
              this.userService = userService;
          }
      
          /**
           * 每讀取一行數(shù)據(jù)觸發(fā)
           */
          @Override
          public void invoke(User user, AnalysisContext context) {
              // 數(shù)據(jù)驗(yàn)證
              if (validateUser(user)) {
                  batchList.add(user);
                  successCount.incrementAndGet();
                  
                  // 達(dá)到批次大小則保存
                  if (batchList.size() >= BATCH_SIZE) {
                      saveBatchData();
                      // 清空列表釋放內(nèi)存
                      batchList.clear();
                  }
              } else {
                  failCount.incrementAndGet();
                  log.warn("數(shù)據(jù)驗(yàn)證失敗: {}", user);
              }
          }
      
          /**
           * 所有數(shù)據(jù)讀取完成后觸發(fā)
           */
          @Override
          public void doAfterAllAnalysed(AnalysisContext context) {
              // 處理剩余數(shù)據(jù)
              if (!batchList.isEmpty()) {
                  saveBatchData();
                  batchList.clear();
              }
              log.info("當(dāng)前Sheet導(dǎo)入結(jié)束,成功: {}, 失敗: {}", successCount.get(), failCount.get());
          }
      
          /**
           * 批量保存數(shù)據(jù)
           */
          private void saveBatchData() {
              try {
                  // 調(diào)用業(yè)務(wù)層批量保存(帶事務(wù))
                  userService.batchSaveUsers(batchList);
                  log.debug("批量保存成功,數(shù)量: {}", batchList.size());
              } catch (Exception e) {
                  log.error("批量保存失敗,數(shù)量: {}", batchList.size(), e);
                  // 失敗處理:可記錄失敗數(shù)據(jù)到文件或數(shù)據(jù)庫
                  handleSaveFailure(batchList);
              }
          }
      
          /**
           * 數(shù)據(jù)驗(yàn)證邏輯
           */
          private boolean validateUser(User user) {
              // 基礎(chǔ)字段驗(yàn)證(根據(jù)實(shí)際業(yè)務(wù)調(diào)整)
              if (user == null) return false;
              if (user.getId() == null) return false;
              if (user.getName() == null || user.getName().trim().isEmpty()) return false;
              return true;
          }
      
          /**
           * 處理保存失敗的數(shù)據(jù)
           */
          private void handleSaveFailure(List<User> failedData) {
              // 實(shí)現(xiàn)失敗數(shù)據(jù)的處理邏輯(例如寫入失敗日志表)
              // userService.saveFailedData(failedData);
          }
      
          // Getter方法用于統(tǒng)計(jì)結(jié)果
          public long getSuccessCount() {
              return successCount.get();
          }
      
          public long getFailCount() {
              return failCount.get();
          }
      }
      
      

      導(dǎo)入Service類

      package com.example.easyexcel.service;
      
      import com.alibaba.excel.EasyExcel;
      import com.alibaba.excel.support.ExcelTypeEnum;
      import com.example.easyexcel.listener.SheetCountListener;
      import com.example.easyexcel.listener.UserImportListener;
      import com.example.easyexcel.model.User;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
      import org.springframework.stereotype.Service;
      import org.springframework.web.multipart.MultipartFile;
      
      import java.io.IOException;
      import java.io.InputStream;
      import java.util.ArrayList;
      import java.util.List;
      import java.util.concurrent.CompletableFuture;
      import java.util.concurrent.atomic.AtomicLong;
      
      
      /**
       * 百萬級Excel數(shù)據(jù)導(dǎo)入服務(wù)
       */
      @Service
      @Slf4j
      public class ExcelImportService {
      
          private final ThreadPoolTaskExecutor excelExecutor;
          private final UserService userService;
      
          public ExcelImportService(ThreadPoolTaskExecutor excelExecutor, UserService userService) {
              this.excelExecutor = excelExecutor;
              this.userService = userService;
      
          }
      
          /**
           * 多線程導(dǎo)入百萬級用戶數(shù)據(jù)(每個(gè)Sheet一個(gè)線程)
           */
          public void importMillionUsers(MultipartFile file) throws IOException {
              // 1. 保存成臨時(shí)文件,避免多線程共用 InputStream
              java.io.File tmpFile = java.io.File.createTempFile("excel_", ".xlsx");
              file.transferTo(tmpFile);          // Spring 提供的零拷貝
              tmpFile.deleteOnExit();            // JVM 退出時(shí)自動清理
      
              ExcelTypeEnum excelType = getExcelType(file.getOriginalFilename());
      
              // 2. 拿 sheet 數(shù)量
              int sheetCount;
              try (InputStream in = new java.io.FileInputStream(tmpFile)) {
                  sheetCount = getSheetCount(in);
              }
              log.info("開始導(dǎo)入,總 Sheet 數(shù): {}", sheetCount);
      
              // 3. 并發(fā)讀,每個(gè) Sheet 獨(dú)立 FileInputStream
              AtomicLong totalSuccess = new AtomicLong(0);
              AtomicLong totalFail    = new AtomicLong(0);
      
              List<CompletableFuture<Void>> futures = new ArrayList<>(sheetCount);
              for (int sheetNo = 0; sheetNo < sheetCount; sheetNo++) {
                  final int idx = sheetNo;
                  futures.add(CompletableFuture.runAsync(() -> {
                      try (InputStream in = new java.io.FileInputStream(tmpFile)) {
                          UserImportListener listener = new UserImportListener(userService);
                          EasyExcel.read(in, User.class, listener)
                                  .excelType(excelType)
                                  .sheet(idx)
                                  .doRead();
      
                          totalSuccess.addAndGet(listener.getSuccessCount());
                          totalFail.addAndGet(listener.getFailCount());
                          log.info("Sheet {} 完成,成功: {}, 失敗: {}", idx, listener.getSuccessCount(), listener.getFailCount());
                      } catch (IOException e) {
                          throw new RuntimeException("Sheet " + idx + " 讀取失敗", e);
                      }
                  }, excelExecutor));
              }
      
              CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
              log.info("全部導(dǎo)入完成,總成功: {},總失敗: {}", totalSuccess.get(), totalFail.get());
          }
      
          /**
           * 獲取Excel中的Sheet數(shù)量
           */
          private int getSheetCount(InputStream inputStream) {
              SheetCountListener countListener = new SheetCountListener();
              EasyExcel.read(inputStream)
                      .registerReadListener(countListener)
                      .doReadAll();
              return countListener.getSheetCount();
          }
      
          /**
           * 獲取Excel文件類型
           *
           */
          public ExcelTypeEnum getExcelType(String fileName) {
              if (fileName == null) return null;
              if (fileName.toLowerCase().endsWith(".xlsx")) {
                  return ExcelTypeEnum.XLSX;
              } else if (fileName.toLowerCase().endsWith(".xls")) {
                  return ExcelTypeEnum.XLS;
              }
              return null;
          }
      
      
      }
      
      

      3?? Controller

       @PostMapping("/import")
      @ApiOperation("導(dǎo)入用戶數(shù)據(jù)")
      public ResponseEntity<String> importUsers(@RequestParam("file") MultipartFile file) {
          try {
              if (file.isEmpty()) {
                  return ResponseEntity.badRequest().body("請選擇要導(dǎo)入的文件");
              }
      
              String fileName = file.getOriginalFilename();
              ExcelTypeEnum excelType = importService.getExcelType(fileName);
              if (excelType == null) {
                  return ResponseEntity.badRequest().body("不支持的文件類型,文件名:" +  fileName);
              }
      
              importService.importMillionUsers(file);
              return ResponseEntity.ok("文件導(dǎo)入成功,正在后臺處理數(shù)據(jù)");
          } catch (Exception e) {
              log.error("導(dǎo)入用戶數(shù)據(jù)失敗", e);
              return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                      .body("導(dǎo)入失敗:" + e.getMessage());
          }
      }
      

      性能優(yōu)化技巧

      技巧 說明
      分批查詢 避免一次性加載全表
      分批寫入 每5k條批量插入
      臨時(shí)文件 并發(fā)讀時(shí)先 MultipartFile.transferTo(tmp)
      線程池 配置專用線程池,隔離業(yè)務(wù)線程
      # application.yml
      spring:
        task:
          execution:
            pool:
              core-size: 10
              max-size: 30
              queue-capacity: 1000
      

      常見問題 & 解決方案

      問題 解決方案
      Can not create temporary file! 并發(fā)讀時(shí)先保存臨時(shí)文件,再獨(dú)立流讀取
      Stream Closed 每個(gè)任務(wù)獨(dú)立 InputStream
      OutOfMemoryError 分批處理 + 及時(shí) clear()

      總結(jié)

      ? Spring Boot + EasyExcel零侵入 的情況下即可完成百萬級數(shù)據(jù)的導(dǎo)入導(dǎo)出。
      ? 通過 分批、并發(fā)、順序?qū)?/strong> 等技巧,內(nèi)存占用降低 90% 以上。
      ?? 完整代碼參考:springboot-easyexcel-batch


      如果本文對你有幫助,歡迎 Star & Fork 源碼!

      posted @ 2025-08-04 11:17  smileNicky  閱讀(830)  評論(0)    收藏  舉報(bào)
      主站蜘蛛池模板: 国产精品一区二区三区日韩| 日本a在线播放| 婷婷综合缴情亚洲| 狠狠色婷婷久久综合频道日韩| 精品中文字幕人妻一二| 亚洲精品国产成人| 国产探花在线精品一区二区| 成人无码精品1区2区3区免费看| 人妻激情文学| 蜜臀91精品国产高清在线| 国产精品一区在线蜜臀| 中文字幕日韩有码一区| 国产高清国产精品国产专区| 国产乱码精品一区二三区| 日韩精品人妻黄色一级片| 豆国产97在线 | 亚洲| 成人aⅴ综合视频国产| 亚洲av成人无码精品电影在线| 高安市| 激情五月日韩中文字幕| 久久香蕉国产线看观看怡红院妓院| av午夜福利一片免费看久久| 又湿又紧又大又爽A视频男| 亚洲AV蜜桃永久无码精品| 国产人妻人伦精品1国产丝袜| 国产精品无码久久久久| 国产最大的福利精品自拍| 亚洲精品成人片在线观看精品字幕| 77777亚洲午夜久久多人| 熟妇的奶头又大又长奶水视频| 精品国产一区二区三区麻豆| 国产资源精品中文字幕| 国产精品一区二区三区黄色| 极品少妇xxxx| 久久精品女人天堂av免费观看| 又湿又紧又大又爽A视频男| 国产精品久久人妻无码网站一区 | 国产精品亚洲二区在线看| 国产成人啪精品视频免费APP| 色噜噜亚洲男人的天堂| 老熟女重囗味hdxx69|