最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

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

 更新時間:2025年08月04日 15:40:39   作者:SmileNicky的博客  
本文將基于開源項目 springboot-easyexcel-batch 進行解析與擴展,手把手教大家如何在 Spring Boot 2.2.1 中集成 Alibaba EasyExcel,輕松實現(xiàn)百萬級數(shù)據(jù)的導(dǎo)入與導(dǎo)出

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

springboot-easyexcel-batch
├── src/main/java/com/example/easyexcel
│   ├── controller/      # 導(dǎo)入導(dǎo)出接口
│   ├── listener/        # 導(dǎo)入監(jiān)聽器
│   ├── model/           # 實體類
│   ├── 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)出實戰(zhàn)

場景

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

核心代碼

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;

    // 每個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);

        // 計算總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寫入順序的前一個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);

                // 每個Sheet的處理依賴于前一個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;
        }
    }

    /**
     * 寫入單個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ù),累計 {} 條",
                    sheetName, currentStart, currentEnd, totalWritten);

            // 清除引用,幫助GC
            batchData = new ArrayList<>();
        }

        log.info("Sheet {} 寫入完成,共 {} 條數(shù)據(jù)", sheetName, totalWritten);
    }
}


效果

指標(biāo)優(yōu)化前優(yōu)化后
內(nèi)存峰值1.2GB100MB
耗時45s18s

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

場景

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

監(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)聽器(獨立類實現(xiàn))
 */
@Slf4j
public class UserImportListener extends AnalysisEventListener<User> {

    // 批量保存閾值(可根據(jù)內(nèi)存調(diào)整)
    private static final int BATCH_SIZE = 5000;
    
    // 臨時存儲批次數(shù)據(jù)
    private final List<User> batchList = new ArrayList<>(BATCH_SIZE);
    
    // 導(dǎo)入結(jié)果統(tǒng)計
    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ù)驗證
        if (validateUser(user)) {
            batchList.add(user);
            successCount.incrementAndGet();
            
            // 達到批次大小則保存
            if (batchList.size() >= BATCH_SIZE) {
                saveBatchData();
                // 清空列表釋放內(nèi)存
                batchList.clear();
            }
        } else {
            failCount.incrementAndGet();
            log.warn("數(shù)據(jù)驗證失敗: {}", 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ù)驗證邏輯
     */
    private boolean validateUser(User user) {
        // 基礎(chǔ)字段驗證(根據(jù)實際業(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) {
        // 實現(xiàn)失敗數(shù)據(jù)的處理邏輯(例如寫入失敗日志表)
        // userService.saveFailedData(failedData);
    }

    // Getter方法用于統(tǒng)計結(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ù)(每個Sheet一個線程)
     */
    public void importMillionUsers(MultipartFile file) throws IOException {
        // 1. 保存成臨時文件,避免多線程共用 InputStream
        java.io.File tmpFile = java.io.File.createTempFile("excel_", ".xlsx");
        file.transferTo(tmpFile);          // Spring 提供的零拷貝
        tmpFile.deleteOnExit();            // JVM 退出時自動清理

        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ā)讀,每個 Sheet 獨立 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;
    }


}

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條批量插入
臨時文件并發(fā)讀時先 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ā)讀時先保存臨時文件,再獨立流讀取
Stream Closed每個任務(wù)獨立 InputStream
OutOfMemoryError分批處理 + 及時 clear()

總結(jié)

Spring Boot + EasyExcel零侵入 的情況下即可完成百萬級數(shù)據(jù)的導(dǎo)入導(dǎo)出。

通過 分批、并發(fā)、順序?qū)?/strong> 等技巧,內(nèi)存占用降低 90% 以上。

以上就是SpringBoot集成EasyExcel實現(xiàn)百萬級別的數(shù)據(jù)導(dǎo)入導(dǎo)出實踐指南的詳細內(nèi)容,更多關(guān)于SpringBoot EasyExcel數(shù)據(jù)導(dǎo)入導(dǎo)出的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • java實現(xiàn)memcache服務(wù)器的示例代碼

    java實現(xiàn)memcache服務(wù)器的示例代碼

    本篇文章主要介紹了java實現(xiàn)memcache服務(wù)器的示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-04-04
  • 深入了解Java中Volatile關(guān)鍵字

    深入了解Java中Volatile關(guān)鍵字

    這篇文章主要介紹了Java中Volatile關(guān)鍵字的相關(guān)知識,文章講解非常詳細,代碼幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下
    2020-06-06
  • java自定義日期轉(zhuǎn)化類示例

    java自定義日期轉(zhuǎn)化類示例

    這篇文章主要介紹了java自定義日期轉(zhuǎn)化類示例,需要的朋友可以參考下
    2014-05-05
  • Java項目時的命名規(guī)范使用及說明

    Java項目時的命名規(guī)范使用及說明

    文章總結(jié)了SpringBoot項目命名規(guī)范,包括項目、包、類、方法、變量、配置文件、Starter、API、數(shù)據(jù)庫和其它命名規(guī)則,以確保代碼的一致性和可讀性
    2026-01-01
  • Java實戰(zhàn)玩具商城的前臺與后臺實現(xiàn)流程

    Java實戰(zhàn)玩具商城的前臺與后臺實現(xiàn)流程

    讀萬卷書不如行萬里路,只學(xué)書上的理論是遠遠不夠的,只有在實戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+JSP+SSM+Springboot+Jsp+maven+Mysql實現(xiàn)一個玩具商城系統(tǒng),大家可以在過程中查缺補漏,提升水平
    2022-01-01
  • pom.xml中解決Provides?transitive?vulnerable?dependency?maven:org.yaml:snakeyaml:1.33警告問題

    pom.xml中解決Provides?transitive?vulnerable?dependency?mave

    這篇文章主要介紹了在pom.xml中如何解決Provides?transitive?vulnerable?dependency?maven:org.yaml:snakeyaml:1.33警告問題,需要的朋友可以參考下
    2023-06-06
  • Java如何實現(xiàn)判斷并輸出文件大小

    Java如何實現(xiàn)判斷并輸出文件大小

    這篇文章主要介紹了Java如何實現(xiàn)判斷并輸出文件大小問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-04-04
  • idea中不用git解除關(guān)聯(lián)的方式

    idea中不用git解除關(guān)聯(lián)的方式

    IntelliJ?IDEA是一款高效的Java編程環(huán)境,提供智能編碼輔助、J2EE、Ant等集成,支持本地和遠程調(diào)試,本文講述了如何在IDEA中解除版本控制和刪除.git文件夾,幫助開發(fā)者更好地管理項目設(shè)置和提升開發(fā)效率
    2024-10-10
  • java 終止線程的4種方式小結(jié)

    java 終止線程的4種方式小結(jié)

    本文主要介紹了java終止線程的4種方式小結(jié),包含布爾標(biāo)志位,interrupt()方法,stop()方法和Thread.interrupt()方法,具有一定的參考價值,感興趣的可以了解一下
    2024-05-05
  • java接口私有方法實現(xiàn)過程解析

    java接口私有方法實現(xiàn)過程解析

    這篇文章主要介紹了java接口私有方法實現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2019-11-11

最新評論

嘉黎县| 巧家县| 尉氏县| 米易县| 乌兰县| 宿迁市| 梅河口市| 金坛市| 皋兰县| 怀宁县| 保靖县| 武定县| 新龙县| 湘西| 新乐市| 涟水县| 丰县| 三台县| 法库县| 肃宁县| 疏勒县| 南澳县| 昆明市| 邵阳县| 翁牛特旗| 米林县| 宁陵县| 玛纳斯县| 平罗县| 灵寿县| 呼伦贝尔市| 宁武县| 翁源县| 兴海县| 五指山市| 静乐县| 清流县| 秦安县| 平远县| 盘锦市| 花莲市|