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

基于SpringBoot+FastExcel的百萬級數(shù)據(jù)導(dǎo)入導(dǎo)出完整方案

 更新時間:2026年03月10日 08:34:23   作者:身如柳絮隨風(fēng)揚  
本文詳細(xì)介紹了在SpringBoot3.5.11+JDK17+MySQL環(huán)境下,使用FastExcel實現(xiàn)百萬級數(shù)據(jù)的導(dǎo)入和導(dǎo)出,內(nèi)容包括數(shù)據(jù)庫表設(shè)計、百萬測試數(shù)據(jù)生成、實體類與Mapper、配置文件、百萬數(shù)據(jù)導(dǎo)入、百萬數(shù)據(jù)導(dǎo)出、線程池、阻塞隊列、CountDownLatch的設(shè)計思路與原理,以及接口測試方法

本文將詳細(xì)介紹在 Spring Boot 3.5.11 + JDK17 + MySQL 環(huán)境下,使用 FastExcel 實現(xiàn)百萬級數(shù)據(jù)的 導(dǎo)入導(dǎo)出。內(nèi)容包括:

  • 數(shù)據(jù)庫表設(shè)計及百萬測試數(shù)據(jù)生成
  • 環(huán)境配置與依賴
  • 百萬數(shù)據(jù)導(dǎo)入(多線程批量插入)
  • 百萬數(shù)據(jù)導(dǎo)出(多線程分頁查詢 + 單線程寫入)
  • 線程池、阻塞隊列、CountDownLatch 的設(shè)計思路與原理
  • 接口測試方法

一、項目初始化

1.1 創(chuàng)建 Spring Boot 項目

使用 Spring Initializr 創(chuàng)建項目,選擇:

  • 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 (簡化數(shù)據(jù)庫操作,非必需但推薦) -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.9</version>
        </dependency>

        <!-- MySQL 驅(qū)動 -->
        <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 默認(rèn)) -->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

二、數(shù)據(jù)庫準(zhǔn)備

2.1 創(chuàng)建圖書表

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 '創(chuàng)建時間'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='圖書表';

2.2 生成百萬測試數(shù)據(jù)(用于導(dǎo)出測試)

-- 存儲過程:插入指定行數(shù)的隨機數(shù)據(jù)
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 ;

-- 調(diào)用存儲過程插入 100 萬條數(shù)據(jù)
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列(標(biāo)題行后)
    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,并調(diào)整其他字段的 index(從 0 開始)。本例假設(shè) 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> {

    /**
     * 分頁查詢圖書(用于導(dǎo)出)
     */
    @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);

    /**
     * 查詢總記錄數(shù)(用于導(dǎo)出)
     */
    @Select("SELECT COUNT(*) FROM book")
    long countAll();

    /**
     * 批量插入(用于導(dǎo)入)
     * 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          # 根據(jù)線程數(shù)適當(dāng)調(diào)大,避免連接不夠

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

五、百萬數(shù)據(jù)導(dǎo)入功能

5.1 設(shè)計思路

  • 讀取:使用 FastExcel 的監(jiān)聽器模式(SAX 方式),逐行讀取 Excel,避免內(nèi)存溢出。
  • 批處理:每讀取一定數(shù)量(如 BATCH_SIZE = 2000)的記錄,封裝成一個插入任務(wù)提交給線程池。
  • 線程池:多個插入任務(wù)并行執(zhí)行,提高數(shù)據(jù)庫寫入速度。
  • 阻塞隊列:如果讀取速度遠(yuǎn)快于插入速度,可先將任務(wù)放入隊列,由線程池消費(此處我們直接提交任務(wù),利用線程池的任務(wù)隊列)。
  • CountDownLatch:主線程等待所有插入任務(wù)完成后再返回結(jié)果。
  • 事務(wù):每個批次使用獨立的事務(wù),避免長事務(wù)。

5.2 導(dǎo)入 Service 接口

package com.example.excel.service;

import org.springframework.web.multipart.MultipartFile;

public interface BookImportService extends IService<Book> {

    /**
     * 從 Excel 文件導(dǎo)入圖書數(shù)據(jù)
     * @param file 上傳的 Excel 文件
     * @return 導(dǎo)入結(jié)果統(tǒng)計
     */
    ImportResult importBooks(MultipartFile file);

    // 導(dǎo)入結(jié)果內(nèi)部類
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    class ImportResult {
        private long totalRows;      // 總處理行數(shù)
        private long successRows;    // 成功插入行數(shù)
        private long failedRows;     // 失敗行數(shù)
        private long costTimeMillis; // 耗時
        // 構(gòu)造方法、getter/setter 省略(使用 Lombok 或手動添加)
    }
}

5.3 導(dǎo)入 Service 實現(xiàn)類

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密集型任務(wù),核心線程數(shù)適當(dāng)調(diào)大
    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;

    // 每批插入的數(shù)據(jù)量
    private static final int BATCH_SIZE = 2000;

    @Override
    public ImportResult importBooks(MultipartFile file) {
        long startTime = System.currentTimeMillis();
        ImportResult result = new ImportResult();

        // 創(chuàng)建線程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy() // 當(dāng)線程池滿時,由讀取線程執(zhí)行插入,起到限流作用
        );

        // 用于統(tǒng)計成功/失敗行數(shù)
        AtomicLong successCount = new AtomicLong(0);
        AtomicLong failedCount = new AtomicLong(0);

        // CountDownLatch 用于等待所有插入任務(wù)完成
        // 注意:任務(wù)數(shù)量未知,無法在讀取前確定。因此采用動態(tài)增加計數(shù)的方式。
        // 可以使用 AtomicInteger 記錄提交任務(wù)數(shù),再配合 CountDownLatch 的變種?簡單起見,我們使用一個計數(shù)器 + 等待所有任務(wù)完成的機制:
        // 方案1:使用一個 List<Future> 收集所有提交的任務(wù),最后遍歷 future.get() 等待完成。
        // 方案2:使用 CountDownLatch,但需要提前知道任務(wù)數(shù)??梢栽谧x取過程中先計數(shù)任務(wù)數(shù),但這樣需要兩次讀取?不可取。
        // 此處采用方案1:提交任務(wù)時獲得 Future,最后等待所有 Future 完成。
        List<Future<?>> futures = new CopyOnWriteArrayList<>();

        // 讀取監(jiān)聽器
        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) {
                    // 提交插入任務(wù)
                    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) {
                // 處理剩余不足一批的數(shù)據(jù)
                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();

            // 等待所有插入任務(wù)完成
            for (Future<?> future : futures) {
                try {
                    future.get(); // 阻塞直到任務(wù)完成,若任務(wù)拋出異常,此處會拋出 ExecutionException
                } catch (ExecutionException e) {
                    log.error("插入任務(wù)執(zhí)行異常", e.getCause());
                    // 異常已在線程內(nèi)記錄,此處可累加失敗計數(shù)(但已在線程內(nèi)記錄,可忽略)
                }
            }

        } catch (IOException e) {
            log.error("讀取 Excel 文件失敗", e);
            throw new RuntimeException("文件讀取失敗", e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("等待插入任務(wù)被中斷", e);
        } finally {
            // 關(guān)閉線程池
            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("導(dǎo)入完成,總行數(shù):{},成功:{},失?。簕},耗時:{}ms",
                result.getTotalRows(), result.getSuccessRows(), result.getFailedRows(), result.getCostTimeMillis());

        return result;
    }

    /**
     * 批量插入數(shù)據(jù),使用編程式事務(wù),每個批次獨立事務(wù)
     */
    private void insertBatch(List<Book> books, AtomicLong successCount, AtomicLong failedCount) {
        if (books.isEmpty()) {
            return;
        }

        // 定義事務(wù)
        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 默認(rèn)批量插入是循環(huán)單條,可開啟 rewriteBatchedStatements 優(yōu)化)
            // 或者自定義 XML 使用 foreach 批量插入。此處用 MP 的 saveBatch,需在配置中開啟批量提交。
            this.saveBatch(books); // 假設(shè)使用 MP 的 Service 或 Mapper,此處僅為示例,實際需注入 IService
            // 若使用純 MyBatis,可自定義 mapper 方法批量插入

            transactionManager.commit(status);
            success = true;
            successCount.addAndGet(books.size());
            log.debug("批量插入成功,數(shù)量:{}", books.size());
        } catch (Exception e) {
            transactionManager.rollback(status);
            failedCount.addAndGet(books.size());
            log.error("批量插入失敗,數(shù)量:{},錯誤:{}", books.size(), e.getMessage());
        }
    }
}

注意:上述代碼中 this.saveBatch(books) 是 MyBatis Plus 的批量插入方法,需要注入 IService<Book> 或者使用 SqlSession 直接批量操作。為簡化,可以注入 com.baomidou.mybatisplus.extension.service.IService,或者自定義 Mapper 實現(xiàn)批量插入。這里為了代碼簡潔,假設(shè)已注入 BookMapper 并且 MP 的 saveBatch 可用(需要 BookMapper 繼承 BaseMapper,但 saveBatchIService 的方法,需要注入 ServiceImpl)。實際使用時可以注入 com.baomidou.mybatisplus.extension.service.IService<Book>。

5.4 導(dǎo)入 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);
    }
}

六、百萬數(shù)據(jù)導(dǎo)出功能

(保留之前導(dǎo)出設(shè)計,但稍作整理,確保與導(dǎo)入風(fēng)格一致)

6.1 導(dǎo)出 Service 接口

package com.example.excel.service;

import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.servlet.http.HttpServletResponse;

public interface BookExportService extends IService<Book> {

    /**
     * 導(dǎo)出百萬圖書數(shù)據(jù)到 Excel
     * @param response HTTP響應(yīng)
     */
    void exportMillionBooks(HttpServletResponse response);
}

6.2 導(dǎo)出 Service 實現(xiàn)類

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;

    // 線程池參數(shù)(根據(jù)服務(wù)器核心數(shù)和IO等待時間調(diào)整)
    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;

    // 每批查詢數(shù)量(根據(jù)數(shù)據(jù)庫性能和內(nèi)存調(diào)整)
    private static final int BATCH_SIZE = 5000;

    // 數(shù)據(jù)緩沖隊列(生產(chǎn)者-消費者模式)
    private final BlockingQueue<List<Book>> dataQueue = new LinkedBlockingQueue<>(QUEUE_CAPACITY);

    // 所有查詢?nèi)蝿?wù)完成標(biāo)志
    private volatile boolean queryFinished = false;

    // 用于捕獲寫線程異常
    private final AtomicReference<Exception> writerException = new AtomicReference<>();

    @Override
    public void exportMillionBooks(HttpServletResponse response) {
        // 設(shè)置響應(yīng)頭
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String fileName = URLEncoder.encode("百萬圖書導(dǎo)出", StandardCharsets.UTF_8)
                .replaceAll("\\+", "%20");
        response.setHeader("Content-disposition",
                "attachment;filename*=utf-8''" + fileName + ".xlsx");

        // 獲取總記錄數(shù)
        long totalCount = bookMapper.countAll();
        log.info("總記錄數(shù):{}", totalCount);
        if (totalCount == 0) {
            throw new RuntimeException("無數(shù)據(jù)可導(dǎo)出");
        }

        // 計算總頁數(shù)
        int totalPages = (int) Math.ceil((double) totalCount / BATCH_SIZE);
        CountDownLatch queryLatch = new CountDownLatch(totalPages);   // 等待所有查詢?nèi)蝿?wù)完成

        // 創(chuàng)建線程池(IO密集型,適當(dāng)調(diào)大線程數(shù))
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy()   // 當(dāng)線程池滿時由調(diào)用線程執(zhí)行(限流)
        );

        long startTime = System.currentTimeMillis();

        try {
            // 啟動消費者線程(單線程寫Excel),傳入 response
            Thread writerThread = new Thread(() -> writeDataToExcel(response), "Excel-Writer");
            writerThread.start();

            // 提交生產(chǎn)者任務(wù)(多線程分頁查詢)
            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("查詢?nèi)蝿?wù)被中斷", e);
                    } finally {
                        queryLatch.countDown();   // 無論成功失敗,都必須 countDown
                    }
                });
            }

            // 等待所有查詢?nèi)蝿?wù)完成
            queryLatch.await();
            // 所有查詢已完成,設(shè)置完成標(biāo)志
            queryFinished = true;

            // 等待寫線程結(jié)束
            writerThread.join();

            // 檢查寫線程是否有異常
            Exception ex = writerException.get();
            if (ex != null) {
                throw new RuntimeException("寫Excel過程中發(fā)生異常", ex);
            }

            long endTime = System.currentTimeMillis();
            log.info("導(dǎo)出成功,耗時:{} ms", endTime - startTime);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("導(dǎo)出被中斷", e);
            throw new RuntimeException("導(dǎo)出被中斷", e);
        } finally {
            executor.shutdown();
        }
    }

    /**
     * 消費者:從隊列獲取數(shù)據(jù)并寫入Excel(單線程)
     * @param response HTTP響應(yīng),用于獲取輸出流
     */
    private void writeDataToExcel(HttpServletResponse response) {
        OutputStream outputStream = null;
        cn.idev.excel.ExcelWriter writer = null;
        try {
            outputStream = response.getOutputStream();
            // 構(gòu)建ExcelWriter(注意:不要用 try-with-resources 自動關(guān)閉流,需手動控制順序)
            writer = EasyExcel.write(outputStream, Book.class).build();
            WriteSheet writeSheet = EasyExcel.writerSheet("圖書數(shù)據(jù)").build();

            while (true) {
                // 從隊列取數(shù)據(jù),超時1秒,避免一直阻塞導(dǎo)致無法檢測 finished
                List<Book> batch = dataQueue.poll(1, TimeUnit.SECONDS);
                if (batch != null) {
                    // 寫入一批數(shù)據(jù)
                    writer.write(batch, writeSheet);
                } else if (queryFinished && dataQueue.isEmpty()) {
                    // 所有查詢已完成且隊列為空,退出循環(huán)
                    break;
                }
                // 如果隊列為空但查詢尚未完成,繼續(xù)循環(huán)等待
            }

            // 所有數(shù)據(jù)寫入完成,必須調(diào)用 finish 來刷新并關(guān)閉內(nèi)部資源
            writer.finish();
            // 此時可以安全關(guān)閉輸出流(但 finish 內(nèi)部可能已關(guān)閉,此處可不顯式關(guān)閉)
            outputStream.flush();
        } catch (Exception e) {
            log.error("寫Excel過程中出錯", e);
            // 記錄異常,供主線程判斷
            writerException.set(e);
            // 如果writer已創(chuàng)建但未finish,嘗試finish以釋放資源(避免內(nèi)存泄漏)
            if (writer != null) {
                try {
                    writer.finish();
                } catch (Exception ex) {
                    log.error("關(guān)閉ExcelWriter時出錯", ex);
                }
            }
            // 不要拋出RuntimeException,因為是在子線程中,異常通過 writerException 傳遞
        } finally {
            // 確保流關(guān)閉(如果未關(guān)閉)
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    log.error("關(guān)閉輸出流時出錯", e);
                }
            }
        }
    }
}

6.3 導(dǎo)出 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);
    }
}

七、關(guān)鍵設(shè)計解析

7.1 為什么使用線程池?

  • 導(dǎo)入:數(shù)據(jù)庫插入是 IO 密集型操作,多線程并發(fā)插入可以充分利用數(shù)據(jù)庫連接和磁盤 IO,大幅提升吞吐量。
  • 導(dǎo)出:數(shù)據(jù)庫查詢同樣是 IO 密集型,多線程分頁查詢可以縮短數(shù)據(jù)獲取時間,配合單線程寫入,實現(xiàn)高效導(dǎo)出。

7.2 線程池參數(shù)選型

參數(shù)說明
corePoolSize10核心線程數(shù),根據(jù) CPU 核心數(shù)和 IO 等待時間估算,通常設(shè)為 CPU 核心數(shù) * (1 + 平均等待時間/計算時間),這里簡單設(shè)為 10
maximumPoolSize20最大線程數(shù),避免創(chuàng)建過多線程導(dǎo)致數(shù)據(jù)庫連接耗盡
keepAliveTime60s空閑線程存活時間
workQueueLinkedBlockingQueue(100)有界隊列,防止任務(wù)無限堆積導(dǎo)致內(nèi)存溢出
rejectedExecutionHandlerCallerRunsPolicy當(dāng)線程池和隊列都滿時,由提交任務(wù)的線程(如讀取 Excel 的線程)執(zhí)行插入,實現(xiàn)自然限流

7.3 為什么用 CountDownLatch?

  • 導(dǎo)入:需要等待所有插入任務(wù)完成后才能返回導(dǎo)入結(jié)果。由于任務(wù)數(shù)量在讀取過程中動態(tài)產(chǎn)生,我們使用 List<Future> 來等待每個任務(wù)完成,效果等同于 CountDownLatch。
  • 導(dǎo)出:需要等待所有分頁查詢?nèi)蝿?wù)結(jié)束,才能設(shè)置 finished = true,從而讓寫線程正確退出。CountDownLatch 初始化為總頁數(shù),每個查詢?nèi)蝿?wù)完成時調(diào)用 countDown(),主線程 await() 等待所有查詢完成。

7.4 為什么用阻塞隊列?

  • 導(dǎo)入:雖然我們直接提交插入任務(wù)給線程池,但線程池內(nèi)部有阻塞隊列,起到緩沖作用。讀取線程不會阻塞,除非線程池隊列滿(此時 CallerRunsPolicy 讓讀取線程自己插入,相當(dāng)于阻塞了讀?。?。
  • 導(dǎo)出:使用 BlockingQueue 作為數(shù)據(jù)通道,生產(chǎn)者(查詢線程)將數(shù)據(jù)放入隊列,消費者(寫線程)從隊列取出。當(dāng)寫速度慢于查詢速度時,隊列會填滿,生產(chǎn)者 put() 阻塞,自動調(diào)節(jié)生產(chǎn)速度,防止內(nèi)存溢出。

7.5 事務(wù)處理

  • 導(dǎo)入:每個批次使用獨立事務(wù),避免長事務(wù),且失敗批次不會影響其他批次。使用編程式事務(wù)精細(xì)控制。
  • 導(dǎo)出:只讀操作,無需事務(wù)。

7.6 數(shù)據(jù)庫連接池配置

  • 必須確保 maximum-pool-size 大于等于最大線程數(shù),否則線程會因獲取不到連接而阻塞或超時。本例中最大線程 20,連接池設(shè)為 30 比較穩(wěn)妥。

7.7 批量插入性能優(yōu)化

  • 使用 rewriteBatchedStatements=true 讓 MySQL 驅(qū)動將多條 insert 合并成一條,大幅提升批量插入性能。
  • MyBatis Plus 的 saveBatch 默認(rèn)也是循環(huán)單條,需要配置 rewriteBatchedStatements 才能發(fā)揮批量效果。也可以自定義 XML 使用 <foreach> 拼接成一條 insert 語句,但要注意 SQL 長度限制。

八、接口測試

8.1 導(dǎo)入測試

使用 Postman 或前端上傳文件:

  • URL: http://localhost:8080/import/books
  • Method: POST
  • Body: form-data,key 為 file,選擇包含百萬數(shù)據(jù)的 Excel 文件(可用導(dǎo)出功能先生成一個)

返回示例:

{
    "totalRows": 1000000,
    "successRows": 1000000,
    "failedRows": 0,
    "costTimeMillis": 189786
}
  • 189,786 毫秒(約 3.16 分鐘)的百萬數(shù)據(jù)導(dǎo)入速度在多數(shù)業(yè)務(wù)場景下屬于良好表現(xiàn),足以滿足日常需求。
  • 優(yōu)化建議(導(dǎo)入速度要看硬件或優(yōu)化)
    • 增大批量大小:嘗試將 BATCH_SIZE 提升至 5000~10000,觀察數(shù)據(jù)庫響應(yīng)。
    • 使用多線程并發(fā)插入:確保數(shù)據(jù)庫連接池支持并發(fā),并注意線程數(shù)不宜超過 CPU 核心數(shù)的 2~3 倍。
    • 使用原生批量插入語句:如 INSERT INTO table VALUES (...), (...), ...,代替框架的逐條轉(zhuǎn)換。

注意:本例子導(dǎo)入的Excel可以帶id列

8.2 導(dǎo)出測試

瀏覽器或 Postman 訪問:http://localhost:8080/export/books,將下載名為“百萬圖書導(dǎo)出.xlsx”的文件。

  • 導(dǎo)出用時40076 ms ≈ 40秒,處理 1,000,000 條數(shù)據(jù),平均每秒處理約 25,000 條。40秒處理百萬數(shù)據(jù),在大多數(shù)業(yè)務(wù)場景下屬于非???/strong>。

注意:導(dǎo)出Excel的ID不是按順序?qū)С龅?,可以使用對ID列使用排序來觀察是否連續(xù)。

8.3 監(jiān)控

  • 查看日志,觀察耗時和線程運行情況。
  • 使用 JConsole 或 VisualVM 觀察內(nèi)存、線程狀態(tài)。
  • 數(shù)據(jù)庫連接池監(jiān)控(如 Hikari 的 metrics)。

九、總結(jié)

本文完整實現(xiàn)了基于 Spring Boot 3.5.11 + FastExcel 的百萬數(shù)據(jù)導(dǎo)入導(dǎo)出功能:

  • 導(dǎo)入:利用監(jiān)聽器逐行讀取,多線程批量插入,獨立事務(wù),確保性能和可靠性。
  • 導(dǎo)出:多線程分頁查詢 + 阻塞隊列 + 單線程寫入,內(nèi)存友好且高效。

以上就是基于SpringBoot+FastExcel的百萬級數(shù)據(jù)導(dǎo)入導(dǎo)出完整方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot FastExcel百萬級數(shù)據(jù)導(dǎo)入導(dǎo)出的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Mybatis -如何處理clob類型數(shù)據(jù)

    Mybatis -如何處理clob類型數(shù)據(jù)

    這篇文章主要介紹了Mybatis 如何處理clob類型數(shù)據(jù)的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-06-06
  • Swagger2配置Security授權(quán)認(rèn)證全過程

    Swagger2配置Security授權(quán)認(rèn)證全過程

    這篇文章主要介紹了Swagger2配置Security授權(quán)認(rèn)證全過程,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-03-03
  • JAVA基于SnakeYAML實現(xiàn)解析與序列化YAML

    JAVA基于SnakeYAML實現(xiàn)解析與序列化YAML

    這篇文章主要介紹了JAVA基于SnakeYAML實現(xiàn)解析與序列化YAML,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2019-12-12
  • IDEA打開項目所有東西都在報紅報錯的解決方案

    IDEA打開項目所有東西都在報紅報錯的解決方案

    這篇文章主要給大家介紹了關(guān)于IDEA打開項目所有東西都在報紅報錯的三個解決方案,文中通過圖文介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用idea具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2023-06-06
  • java?串口工具jSerialComm示例詳解

    java?串口工具jSerialComm示例詳解

    這篇文章主要介紹了java?串口工具jSerialComm,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-06-06
  • Java實現(xiàn)單位換算功能

    Java實現(xiàn)單位換算功能

    這篇文章主要為大家詳細(xì)介紹了Java實現(xiàn)單位換算功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-01-01
  • JAVA發(fā)送HTTP請求的多種方式詳細(xì)總結(jié)

    JAVA發(fā)送HTTP請求的多種方式詳細(xì)總結(jié)

    目前做項目中有一個需求是這樣的,需要通過Java發(fā)送url請求,查看該url是否有效,這時我們可以通過獲取狀態(tài)碼來判斷,下面這篇文章主要給大家介紹了關(guān)于JAVA發(fā)送HTTP請求的多種方式總結(jié)的相關(guān)資料,需要的朋友可以參考下
    2023-01-01
  • SpringBoot中間件ORM框架實現(xiàn)案例詳解(Mybatis)

    SpringBoot中間件ORM框架實現(xiàn)案例詳解(Mybatis)

    這篇文章主要介紹了SpringBoot中間件ORM框架實現(xiàn)案例詳解(Mybatis),本篇文章提煉出mybatis最經(jīng)典、最精簡、最核心的代碼設(shè)計,來實現(xiàn)一個mini-mybatis,從而熟悉并掌握ORM框架的涉及實現(xiàn),需要的朋友可以參考下
    2023-07-07
  • Java線程中start和run方法全面解析

    Java線程中start和run方法全面解析

    這篇文章主要介紹了Java線程中start和run方法的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下
    2016-08-08
  • 總結(jié)java多線程之互斥與同步解決方案

    總結(jié)java多線程之互斥與同步解決方案

    文中總結(jié)了線程互斥與同步,synchronized使用細(xì)節(jié)及原理,Reentrylock使用細(xì)節(jié)等知識,對解決Java多線程互斥與同步等問題很有效,,需要的朋友可以參考下
    2021-05-05

最新評論

白城市| 绥芬河市| 唐河县| 平原县| 达拉特旗| 乃东县| 永清县| 紫金县| 南乐县| 英德市| 宿州市| 巴彦淖尔市| 封开县| 尼勒克县| 安仁县| 黄陵县| 乌鲁木齐市| 常宁市| 长沙县| 枝江市| 慈利县| 莱州市| 合肥市| 徐汇区| 阿克陶县| 突泉县| 鄯善县| 三穗县| 章丘市| 图们市| 高陵县| 项城市| 大安市| 定安县| 龙泉市| 乌兰县| 安乡县| 同德县| 鱼台县| 梁河县| 青田县|