Skip to content

Excel 大批量导入导出操作优化

1、Java 中的流行库

Java 对 Excel 的常用操作通常涉及读取、创建、编辑和保存 Excel 文件。

这些操作主要通过 Apache POI 和 jExcelAPI 这两个流行的库来实现。Apache POI 支持老式的 Excel XLS 格式和较新的 XLSX 格式,而 jExcelAPI 主要用于处理 XLS 文件。

以下是一些常见的操作示例:

Apache POI

Apache POI 是处理 Microsoft Office 文档的流行 Java 库,特别是 Excel 文档(包括 XLS 和 XLSX 格式)。

POI 提供了不同的模型来处理 Excel 文档,主要包括 HSSF(用于 .xls 格式)、XSSF(用于 .xlsx 格式)以及 SXSSF(用于处理大型 .xlsx 文件的扩展版本)。在处理 .xlsx 格式文件时,主要使用的是 XSSF 和 SXSSF。

使用示例:

<!-- 基本依赖,仅操作 xls 格式只需引入此依赖 -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>3.14</version>
</dependency>
<!-- 使用 xlsx 格式需要额外引入此依赖 -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>3.14</version>
</dependency>

演示代码

package com.ruoyi.luoqi.excel;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.*;

/**
 * @author luoqi
 * @File ApachePOITestOperation.java
 * @Desc
 * @Create 2024/4/3 15:18
 * @ChangeList --------------------------------------------------------------------
 * Date                          Editor                     ChangeReason
 */
public class ApachePOITestOperation {
    public static void main(String[] args){
        try {
            writeExcel();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void readExcel()  throws IOException {
        FileInputStream file = new FileInputStream(new File("path/to/excel.xlsx"));
        XSSFWorkbook workbook = new XSSFWorkbook(file);
        //读取工作表
        XSSFSheet sheet = workbook.getSheetAt(0);
        //遍历行与单元格
        for (Row row : sheet) {
            for (Cell cell : row) {
                switch (cell.getCellType()) {
                    case STRING: System.out.print(cell.getStringCellValue() + " ");
                        break;
                    case NUMERIC: System.out.print(cell.getNumericCellValue() + " ");
                        break;
                    case BOOLEAN: System.out.print(cell.getBooleanCellValue() + " ");
                        break;
                    default:
                }
            }
            System.out.println();
        }
    }


    public static void writeExcel() throws IOException {
        //创建 Excel 文件
        XSSFWorkbook workbook = new XSSFWorkbook();
        XSSFSheet sheet = workbook.createSheet("Name of the Sheet");
        //创建行和单元格
        Row row = sheet.createRow(0);
        Cell cell = row.createCell(0);
        cell.setCellValue("Cell Value");
        //保存 Excel 操作
        FileOutputStream out = new FileOutputStream(new File("path/to/excel2.xlsx"));
        workbook.write(out);
        out.close();
        workbook.close();
    }
}

jExcelAPI

        <dependency>
            <groupId>net.sourceforge.jexcelapi</groupId>
            <artifactId>jxl</artifactId>
            <version>2.6.12</version>
        </dependency>

演示代码

package com.ruoyi.luoqi.excel;
import jxl.Cell;
import jxl.Sheet;
import jxl.Workbook;
import jxl.read.biff.BiffException;
import jxl.write.Label;
import jxl.write.WritableSheet;
import jxl.write.WritableWorkbook;
import jxl.write.WriteException;

import java.io.File;
import java.io.IOException;

/**
 * @author luoqi
 * @File jExcelAPITestOpeartion.java
 * @Desc
 * @Create 2024/4/3 15:56
 * @ChangeList --------------------------------------------------------------------
 * Date                          Editor                     ChangeReason
 */
public class JExcelAPITestOpeartion {

    public static void main(String[] args) {
        try {
            JExcelAPITestOpeartion jExcelAPITestOpeartion = new JExcelAPITestOpeartion();
            jExcelAPITestOpeartion.readExcel();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (BiffException e) {
            e.printStackTrace();
        }
    }

    public  void readExcel() throws IOException, BiffException {
        //读取Excel 操作
        Workbook workbook = Workbook.getWorkbook(new File("path/to/excel.xls"));
        //读取工作表
        Sheet sheet = workbook.getSheet(0);
        //读取内容
        Cell cell = sheet.getCell(0, 0);
        String content = cell.getContents();
        System.out.println(content);
    }

    public static void writeExcel() throws IOException, WriteException {
        WritableWorkbook workbook = Workbook.createWorkbook(new File("path/to/excel.xls"));
        WritableSheet sheet = workbook.createSheet("Sheet Name", 0);
        Label label = new Label(0, 0, "Content");
        sheet.addCell(label);
        workbook.write();
        workbook.close();

    }
}

2、Excel的存储格式

参考: https://albenw.github.io/posts/d093ca4e/

image.png

Excel的存储格式

  • XLS
    • XLS一个Sheet最多也只能有 65535 行数据
    • 每个Sheet最多只能有 256 列(从A到IV)
  • XLSX(底层使用xml存储)
    • 一个Sheet最多可以有 1048576 行数据
    • 每个Sheet最多可以有 16384 列(从A到XFD)
    • 文件大小更小

导出优化

建议可以采用: SXSSFWorkbook 方式。

导入优化

官网示例: https://poi.apache.org/components/spreadsheet/how-to.html

POI对导入分为3种模式,用户模式 User Model,事件模式 Event Model,还有 Event User Model。

  • POI对XLS支持 Event Model
  • POI对 XLSX 支持 Event Model 和 Event User Model

默认采用:

Apache POI 在处理 .xlsx 文件时默认采用的是 User Model,它提供了更直观、更灵活的方式来处理 Excel 文件,但代价是较高的内存消耗。当处理大型文件或需要优化内存使用时,可以考虑使用 Event Model 或 Event User Model,尽管这需要更复杂的实现逻辑。

具体实现的话,可能会稍微复杂一点,需要自定义一些相关的配置和处理。


3、EasyExcel

对于导入的一些优化操作:

准备环境:

  • 使用 .xlsx 文件
  • 导入的时候需要进行数据校验
    • 数据校验有两种:
      • 字段长度、字段正则表达式校验等,内存内校验不存在外部数据交互。对性能影响较小。
      • 数据重复性校验(需要查询数据库,十分影响性能)。
  • 校验完成后,进行数据导入操作。
    • 这里的环境是:数据库使用 MySQL 5.7,未分库分表,连接池使用 Druid

迭代记录:

    1. POI + 逐行查询校对 + 逐行插入
    1. EasyPOI + 缓存数据库查询操作 + 批量插入
    1. EasyExcel + 缓存数据库查询操作 + 批量插入

优化建议

提升Excel导入速度的方法:

  • 使用更快的 Excel 读取框架(推荐使用阿里 EasyExcel)。
  • 对于需要与数据库交互的校验、按照业务逻辑适当的使用缓存。用空间换时间。
  • 使用 values( ),( ),( ) 拼接长 SQL 一次插入多行数据。
  • 使用多线程插入数据,利用掉网络IO等待时间(推荐使用并行流,简单易用)。
  • 避免在循环中打印无用的日志。

这里是使用的 若依 分离版进行二次改造

单独新增了一个页面,用于导入操作

引入依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.1.3</version>
</dependency>

具体操作

  • 1、定义实体类
  • 2、定义字典转换(如果需要,导出或者导入操作)
  • 3、定义导入和导出接口(最简单)
  • 4、考虑如何进行优化,并通过不同示例去掌握具体使用。

参考: https://zhuanlan.zhihu.com/p/641135306

实体类:

这里定义一个车辆订单的信息

先进行数据库表设计,再做实体类的映射关系,这里不使用前端,就使用后端接口进行测试操作。

订单号
订单状态
下单时间
排序号
VIN号

创建表

CREATE TABLE IF NOT EXISTS orders (
    order_id VARCHAR(32) NOT NULL,
    order_status VARCHAR(8) NOT NULL,
    order_time DATETIME NOT NULL,
    sort_number INT NOT NULL,
    vin_number VARCHAR(32) NOT NULL,
    PRIMARY KEY (order_id)
);

通过若以的代码生成,生成一下后端的代码

细节文件这里跳过

  • Orders
  • OrdersController
  • IOrdersService
  • OrdersServiceImpl
  • OrdersMapper
  • OrdersMapper.xml

测试批量插入操作

OrdersExcelOpertaion

package com.ruoyi.luoqi.excel;
import com.ruoyi.system.domain.Orders;
import com.ruoyi.system.service.IOrdersService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;

/**
 * @author luoqi
 * @File OrdersDataInsertOpertaion.java
 * @Desc
 * @Create 2024/4/5 10:08
 * @ChangeList --------------------------------------------------------------------
 * Date                          Editor                     ChangeReason
 */
@SpringBootTest
public class OrdersDataInsertOpertaion {


    @Autowired
    private IOrdersService ordersService;

    @Test
    public void testSomeServiceMethod() {
        int startNum = 1;
        int endNum = 1000;
        //计算耗时
        long startTime = System.currentTimeMillis();
        for(int i = startNum;i<= endNum ;i ++){
            Orders orders = new Orders();
            orders.setOrderId(String.valueOf(i));
            orders.setOrderStatus("20");
            orders.setSortNumber((long) i);
            orders.setVinNumber(getRandomSeventeenLength());
            orders.setOrderTime(new Date());
            ordersService.insertOrders(orders);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("方法执行耗时: " + (endTime - startTime) + " 毫秒");
    }

    @Test
    public void testSomeServiceMethodBatch() {
        // 创建一个订单列表用于批量插入
        List<Orders> ordersList = new ArrayList<>();
        int startNum = 2001;
        int endNum = 10000;
        //计算耗时
        long startTime = System.currentTimeMillis();
        for(int i = startNum;i<= endNum ;i ++){
            Orders orders = new Orders();
            orders.setOrderId(String.valueOf(i));
            orders.setOrderStatus("20");
            orders.setSortNumber((long) i);
            orders.setVinNumber(getRandomSeventeenLength());
            orders.setOrderTime(new Date());
            ordersList.add(orders);
        }
        ordersService.insertOrdersList(ordersList);
        long endTime = System.currentTimeMillis();
        System.out.println("方法执行耗时: " + (endTime - startTime) + " 毫秒");
    }

    @Test
    public void testSomeServiceMethodBatchUpdate() {
        //MySQL默认的最大包大小是4MB(可通过max_allowed_packet参数调整),这意味着一次批量插入操作的数据总量不能超过这个限制
        // 创建一个订单列表用于批量插入
        List<Orders> ordersList = new ArrayList<>();
        int startNum = 10001;
        int endNum = 20000;
        //计算耗时
        long startTime = System.currentTimeMillis();
        for(int i = startNum;i<= endNum ;i ++){
            Orders orders = new Orders();
            orders.setOrderId(String.valueOf(i));
            orders.setOrderStatus("20");
            orders.setSortNumber((long) i);
            orders.setVinNumber(getRandomSeventeenLength());
            orders.setOrderTime(new Date());
            ordersList.add(orders);
        }
        //批量大小的控制看实际情况,一方面要考虑实际插入大小(包括数据复杂的程度),另一方面还要考虑网络、数据库等的影响
        ordersService.batchInsert(ordersList,1000);
        long endTime = System.currentTimeMillis();
        System.out.println("方法执行耗时: " + (endTime - startTime) + " 毫秒");
    }

    public String getRandomSeventeenLength()  {
        Random random = new Random();
        StringBuilder sb = new StringBuilder(17);

        // 定义可能出现的字符集
        String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

        for (int i = 0; i < 17; i++) {
            // 随机选择一个字符
            int index = random.nextInt(characters.length());
            sb.append(characters.charAt(index));
        }

        String randomString = sb.toString();
        return randomString;
    }

}

特意说一下这个分配插入这里,这里是在 service 添加了一下事务控制

@Override  
@Transactional  
public void batchInsert(List<Orders> allOrders, int batchSize) {  
    // 分批处理数据  
    int totalSize = allOrders.size();  
    for (int i = 0; i < totalSize; i += batchSize) {  
        int end = Math.min(totalSize, i + batchSize);  
        List<Orders> batchList = allOrders.subList(i, end);  
        // 执行批量插入  
        ordersMapper.insertOrdersList(batchList);  
    }  
}

测试数据添加的情况

遍历插入1000条数据,方法执行耗时: 1465 毫秒

批量插入1000条数据,方法执行耗时: 205 毫秒

批量插入8000条数据,方法执行耗时: 699 毫秒

分批量插入 10000条数据,每次插入1000条,方法执行耗时: 650 毫秒

分批量插入 20000条数据,每次插入1000条,方法执行耗时: 875 毫秒

分批量的情况下的确会好很多

自定义导出 Excel 注解

在实际使用到 若依 的分离版项目的时候,他这里并没有使用 easyexcel ,仍然使用的是 poi ,然后使用了 自定义注解的方式。

SysLogininfor 待分析

to be contined....

EasyExcel 的一些基础操作

常用的一些注解属性,一般在 domain 类上使用

  • @ExcelProperty: 核心注解,value属性可用来设置表头名称,converter属性可以用来设置类型转换器;
  • @ColumnWidth: 用于设置表格列的宽度;
  • @DateTimeFormat: 用于设置日期转换格式;
  • @NumberFormat: 用于设置数字转换格式。

建议看一下官网的示例: https://easyexcel.opensource.alibaba.com

Order

package com.ruoyi.system.domain;

import java.util.Date;

import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.system.domain.converter.OrderStatusConverter;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;

/**
 * @author luoqi
 * @File Orders.java
 * @Desc
 * @Create 2024/4/5 9:47
 * @ChangeList --------------------------------------------------------------------
 * Date                          Editor                     ChangeReason
 */
public class Orders
{
    private static final long serialVersionUID = 1L;

    @ExcelProperty(value = "订单ID")
    private String orderId;

    //Excel的列描述, 字典转换:converter , converter = OrderStatusConverter.class
    @ExcelProperty(value = "订单状态", converter = OrderStatusConverter.class)
    //列宽
    @ColumnWidth(20)
    private String orderStatus;

    @ExcelProperty("订单时间")
    //日期转换格式
    @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
    private Date orderTime;

    @ExcelProperty("排序号")
    private Long sortNumber;

    @ExcelProperty("VIN号")
    private String vinNumber;

    public void setOrderId(String orderId)
    {
        this.orderId = orderId;
    }

    public String getOrderId()
    {
        return orderId;
    }
    public void setOrderStatus(String orderStatus)
    {
        this.orderStatus = orderStatus;
    }

    public String getOrderStatus()
    {
        return orderStatus;
    }
    public void setOrderTime(Date orderTime)
    {
        this.orderTime = orderTime;
    }

    public Date getOrderTime()
    {
        return orderTime;
    }
    public void setSortNumber(Long sortNumber)
    {
        this.sortNumber = sortNumber;
    }

    public Long getSortNumber()
    {
        return sortNumber;
    }
    public void setVinNumber(String vinNumber)
    {
        this.vinNumber = vinNumber;
    }

    public String getVinNumber()
    {
        return vinNumber;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
                .append("orderId", getOrderId())
                .append("orderStatus", getOrderStatus())
                .append("orderTime", getOrderTime())
                .append("sortNumber", getSortNumber())
                .append("vinNumber", getVinNumber())
                .toString();
    }
}

使用这个类导出的时候遇到一个坑,建议不要继承子类,导出是导出所有字段,除非你字段上显示忽略

OrderStatusConverter

package com.ruoyi.system.domain.converter;

import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.converters.ReadConverterContext;
import com.alibaba.excel.converters.WriteConverterContext;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.data.WriteCellData;

import java.util.HashMap;
import java.util.Map;

/**
 * @author luoqi
 * @File OrderStatusConverter.java
 * @Desc
 * @Create 2024/4/5 11:25
 * @ChangeList --------------------------------------------------------------------
 * Date                          Editor                     ChangeReason
 */
public class OrderStatusConverter implements Converter<String> {
    private static final Map<String, String> statusToDescriptionMap = new HashMap<>();
    private static final Map<String, String> descriptionToStatusMap = new HashMap<>();

    static {
        statusToDescriptionMap.put("00", "初始化");
        statusToDescriptionMap.put("05", "排序");
        statusToDescriptionMap.put("10", "VIN生成");
        statusToDescriptionMap.put("15", "展开");
        statusToDescriptionMap.put("20", "发布");
        statusToDescriptionMap.put("25", "在制");
        statusToDescriptionMap.put("30", "下线");

        descriptionToStatusMap.put("初始化", "00");
        descriptionToStatusMap.put("排序", "05");
        descriptionToStatusMap.put("VIN生成", "10");
        descriptionToStatusMap.put("展开", "15");
        descriptionToStatusMap.put("发布", "20");
        descriptionToStatusMap.put("在制", "25");
        descriptionToStatusMap.put("下线", "30");
    }

    @Override
    public Class<String> supportJavaTypeKey() {
        return String.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }

    //导入时进行类型转换操作
    @Override
    public String convertToJavaData(ReadConverterContext<?> context) {
        String description = context.getReadCellData().getStringValue();
        return descriptionToStatusMap.getOrDefault(description, "未知状态码");
    }

    //导出时进行类型转换操作
    @Override
    public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {
        String description = statusToDescriptionMap.getOrDefault(context.getValue(), "未知状态");
        return new WriteCellData<>(description);
    }
}

测试类

@SpringBootTest  
public class OrdersExcelOpertaion {  
  
    @Autowired  
    private IOrdersService ordersService;  
  
    @Test  
    public void exportOrderExcel2() {  
            Orders orders = new Orders();  
            orders.setSortNumber(789L);  
            List<Orders> list = ordersService.selectOrdersList(orders);  
            String fileName = "path/to/orderExcel2.xlsx";  
            EasyExcel.write(fileName, Orders.class)  
                    .head(Orders.class)  
                    .sheet("用户列表")  
                    .doWrite(list);  
    }  
  
  
    @Test  
    public void importOrderExcel2() {  
        try {  
            FileInputStream file = new FileInputStream(new File("path/to/orderExcel2.xlsx"));  
            List<Orders> ordersList = EasyExcel.read(file)  
                    .head(Orders.class)  
                    .sheet()  
                    .doReadSync();  
            for(Orders orders:ordersList)  
            {  
                System.out.println(orders);  
            }  
            //读取后进行存放  
            ordersService.batchInsert(ordersList,100);  
        } catch (IOException e) {  
            System.out.println("执行异常");  
        }  
    }  
}

如果是在 controller 类上

@GetMapping("/export2/order")  
public void exportOrderExcel(HttpServletResponse response) {  
    try {  
        Orders orders = new Orders();  
        orders.setSortNumber(789L);  
        //这个方法用于设置响应头,以便告知浏览器要下载一个文件,而不是直接在浏览器中打开这个响应。  
        this.setExcelResponseProp(response, "用户列表");  
        List<Orders> list = ordersService.selectOrdersList(orders);  
        EasyExcel.write(response.getOutputStream())  
                .head(Orders.class)  
                .excelType(ExcelTypeEnum.XLSX)  
                .sheet("用户列表")  
                .doWrite(list);  
    } catch (IOException e) {  
        throw new RuntimeException(e);  
    }  
}  
  
public void setExcelResponseProp(HttpServletResponse response, String fileName) {  
    try {  
        // 防止中文乱码  
        fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());  
    } catch (UnsupportedEncodingException e) {  
        // 日志记录错误或其他处理  
        throw new RuntimeException("文件名编码失败", e);  
    }  
  
    // 设置响应类型  
    response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");  
    // 设置文件名  
    response.setHeader("Content-disposition", "attachment;filename=" + fileName);  
    // 防止浏览器缓存  
    response.setHeader("Pragma", "no-cache");  
    response.setHeader("Cache-Control", "no-cache");  
    response.setDateHeader("Expires", 0);  
}  
  
@PostMapping("/import2/order")  
public AjaxResult importOrderExcel(@RequestPart(value = "file") MultipartFile file) {  
    try {  
        List<Orders> ordersList = EasyExcel.read(file.getInputStream())  
                .head(Orders.class)  
                .sheet()  
                .doReadSync();  
        for(Orders orders:ordersList)  
        {  
            System.out.println(orders);  
        }  
        //读取后进行存放  
        ordersService.batchInsert(ordersList,100);  
        return success(ordersList);  
    } catch (IOException e) {  
        return error();  
    }  
}

本人使用的是 apifox

导出操作(选择发送并下载)

image.png

导入操作(主要是 body 这里注意一下)

image.png

小结

  • 1、看官网,去看示例
  • 2、主要是掌握一些优化手段

包括:

  • 使用更快的 Excel 读取框架(推荐使用阿里 EasyExcel)。
  • 对于需要与数据库交互的校验、按照业务逻辑适当的使用缓存。用空间换时间。
  • 使用 values( ),( ),( ) 拼接长 SQL 一次插入多行数据。
  • 使用多线程插入数据,利用掉网络IO等待时间(推荐使用并行流,简单易用)。
  • 避免在循环中打印无用的日志。

讲一下第四点:

在处理大量数据导入数据库时,网络I/O等待时间(特别是在分布式数据库环境中)可能成为性能瓶颈。

为了减少这些等待时间并提高数据处理速度,可以采用多线程或并行处理技术来并发执行数据插入操作。Java 8引入的并行流(Parallel Streams)提供了一种简单而强大的方式来利用多核处理器的并行处理能力。

使用并行流进行数据插入

并行流利用了Java的ForkJoinPool,它将一个大任务拆分成多个小任务,这些小任务被分发到线程池中的不同线程上并行执行,最后将结果合并。这使得并行流特别适合进行批量数据处理和插入操作。

以下是一个使用并行流进行数据插入优化的示例:


    @Test
    public void testSomeServiceMethodBatchUpdateByForkJoin() {
        //MySQL默认的最大包大小是4MB(可通过max_allowed_packet参数调整),这意味着一次批量插入操作的数据总量不能超过这个限制
        // 创建一个订单列表用于批量插入
        List<Orders> ordersList = new ArrayList<>();
        int startNum = 40001;
        int endNum = 60000;
        //计算耗时
        long startTime = System.currentTimeMillis();
        for(int i = startNum;i<= endNum ;i ++){
            Orders orders = new Orders();
            orders.setOrderId(String.valueOf(i));
            orders.setOrderStatus("20");
            orders.setSortNumber((long) i);
            orders.setVinNumber(getRandomSeventeenLength());
            orders.setOrderTime(new Date());
            ordersList.add(orders);
        }
        //批量大小的控制看实际情况,一方面要考虑实际插入大小(包括数据复杂的程度),另一方面还要考虑网络、数据库等的影响
        importData(ordersList);
        long endTime = System.currentTimeMillis();
        System.out.println("方法执行耗时: " + (endTime - startTime) + " 毫秒");
    }

    // 自定义并行度
    private static final int parallelism = 4;

    public void importData1(List<Orders> dataList) {
        int batchSize = 1000;

        // 使用自定义的ForkJoinPool来控制并行流的并行度
        ForkJoinPool customThreadPool = new ForkJoinPool(parallelism);
        try {
            customThreadPool.submit(() -> {
                // 分批处理数据,使用并行流进行批量插入
                dataList.stream()
                        .collect(Collectors.groupingBy(data -> dataList.indexOf(data) / batchSize))
                        .values()
                        .parallelStream()
                        .forEach(batchList -> ordersService.batchInsert(batchList,batchSize));
            }).get(); // 等待所有任务完成
        } catch (InterruptedException | ExecutionException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Failed to import data in parallel", e);
        } finally {
            customThreadPool.shutdown();
        }
    }

    public void importData(List<Orders> dataList) {
        int batchSize = 1000;

        // 使用自定义的ForkJoinPool来控制并行流的并行度
        ForkJoinPool customThreadPool = new ForkJoinPool(parallelism);
        try {
            customThreadPool.submit(() ->
                    //改进点:1、直接使用批次索引;2、避免使用 indexOf;3、数据分割考虑使用其他方式替代 groupingBy
                    IntStream.range(0, (dataList.size() + batchSize - 1) / batchSize)
                            .parallel()
                            .mapToObj(batchNum -> dataList.subList(batchNum * batchSize,
                                    Math.min(dataList.size(), (batchNum + 1) * batchSize)))
                            .forEach(batchList -> ordersService.batchInsert(batchList, batchSize))
            ).get(); // 等待所有任务完成
        } catch (InterruptedException | ExecutionException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Failed to import data in parallel", e);
        } finally {
            customThreadPool.shutdown();
        }
    }

使用 importData 方法,可以在 分批量插入 20000条数据,每次插入1000条,方法执行耗时: 800 毫秒;的基础上提升到 500毫秒左右。

4、导出优化

上文虽然讲述了一下 EasyExcel 的基础使用,以及对于导入操作的一些常见的优化手段,但关于导入,其实还有一些内容需要去了解下,这里主要以 https://mp.weixin.qq.com/s/5Ytgq_B8nvgFpU3FNeiyOQ 这篇文章内容,然后实际去运行和跑,看一下实际的效果。

先过一下基本的一些思路,实现放一下后面

  • 1、依然是使用 esayexcel 的开源框架,内部进行了很大的一个优化,比起 Poi 的方式来说;
  • 2、基本处理方式
    • 一个SHEET一次查询导出
    • 数据量适中(100W以内):一个SHEET分批查询导出
    • 数据里很大(几百万都行):多个SHEET分批查询导出

参考: https://www.cxyxiaowu.com/20723.html


参考: