招行面试:100万级别数据的Excel,如何秒级导入到数据库?

简介: 本文由40岁老架构师尼恩撰写,分享了应对招商银行Java后端面试绝命12题的经验。文章详细介绍了如何通过系统化准备,在面试中展示强大的技术实力。针对百万级数据的Excel导入难题,尼恩推荐使用阿里巴巴开源的EasyExcel框架,并结合高性能分片读取、Disruptor队列缓冲和高并发批量写入的架构方案,实现高效的数据处理。此外,文章还提供了完整的代码示例和配置说明,帮助读者快速掌握相关技能。建议读者参考《尼恩Java面试宝典PDF》进行系统化刷题,提升面试竞争力。关注公众号【技术自由圈】可获取更多技术资源和指导。

本文原文链接

尼恩说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,最近有小伙伴面试招商银行,遇到下面的绝命 12题,狠狠被拷打了, 彻底懵了。 项目场景题太难了,不好好准备,真的答不出!
image.png

所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。

当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书

招商银行的Java后端面试真题

被狠狠拷打了,问的人都懵了。 项目场景题太难了,不好好准备,真的答不出!

尼恩将给出上面 招商银行绝命12 题的 全部答案:

1.如何让系统抗住双十一的预约抢购活动?

16大绝招,完成10Wqps秒杀架构(3万字架构长文)

2.如何从零搭建10万级QPS大流量、高并发优惠券系统?

100万用户,抢10万优惠券,如何设计?

3.百万级别数据的 Excel 如何快速导入到数据

就是本文。

4.如何设计一个支持万亿GB网盘实现秒传与限速的系统?

即将发布。

5.如何根据应用场景选择合适的消息中间件?

即将发布。

6.如何提升 RocketMQ 顺序消费性能?

即将发布。

7.使用分布式调度框架该考虑哪些问题?

即将发布。

9.如何让系统抗住双十一的预约抢购活动?

16大绝招,完成10Wqps秒杀架构(3万字架构长文)

10.问 : 如何解决高并发下的库存抢购超卖少买?

即将发布。

11.为什么高并发下数据写入不推荐关系数据?

即将发布。

12.如果让你设计一个分布式链路跟踪系统?

即将发布。

前言

在日常的开发中,用的比较多的方式就是 Apache 下的 POI 框架了,但在目前数据量大的时代下,这种方式 已经不适合了, 当数据量过大时, POI 框架会出现 OOM 异常,

但是作为数据量小场景下的操作框架,还是OK的。百万级数据量的场景,这个就不行了。

这里,尼恩先是介绍原始 Apache POI ,然后介绍阿里巴巴开源框架,做对比介绍。

POI 框架特性对比

Apache POI 是 Apache 软件基金会的开放源码函式库,用于操作 Microsoft Office 格式文件,如 Excel、Word 和 PowerPoint 等。它提供了一组 Java API,让开发者能够在 Java 程序中创建、读取和修改这些文件格式,而无需依赖于 Microsoft Office 软件本身。

poi 依赖的基础接口: WorkBook ,有几种实现子类需要进行区分,如下:

HSSFWorkbook

HSSFWorkbook 主要处理 Excel 的.xls格式文件,Excel 2003(包含) 之前版本使用的子类对象,处理的文件格式都是 .xls 的,其是 poi 中最常用的方式,

HSSFWorkbook 提供了创建工作簿(HSSFWorkbook)、工作表(HSSFSheet)、行(HSSFRow)和单元格(HSSFCell)等对象的功能。

例如,可以使用这些对象来设置单元格的值、样式(如字体、颜色、对齐方式等)。

HSSFWorkbook 处理的行数在 6W+,一般处理的数据不超过这个大小就不会出现内存溢出的,这个量内存也是足够支撑的.

XSSFWorkbook:

Excel 2003-2007 使用的子类对象,目前还是有大量公司使用的这个,文件格式为 .xlsx,

XSSFWorkbook 用于处理 Excel 的.xlsx格式文件。

XSSFWorkbook 的功能与 HSSF 类似,但由于.xlsx格式是基于 XML 的,在处理大型文件时可能会有更好的性能和功能。例如,XSSF 支持更多的单元格样式和数据验证规则。

XSSFWorkbook 格式就是为了突破 HSSFWorkBook 6W 数据的局限,是为了针对Excel2007版本的 1048576行,16384 列,最多可以导出 104w 条数据,

虽然 XSSFWorkbook在数据上增加了,但是内存的瓶颈也就来了,OOM 离之不远了.

SXSSFWorkbook:

该实现类是 POI3.8 之后的版本才有的, 它可以操作 Excel2007 以后的所有版本 Excel,扩展名是 .xlsx

SXSSFWorkbook 是 XSSFWorkbook 的一个扩展,用于处理非常大的 Excel 文件。

SXSSFWorkbook 通过将数据缓存在内存和磁盘中,避免了一次性将大量数据加载到内存中导致内存溢出的问题,从而能够有效地处理大型 Excel 文件。

SXSSFWorkbook方式提供了一种低内存占用机制,存储百万数据丝毫不是问题,一般不会出现内存溢出(它使用硬盘来换内存,也就是说当内存数据到达一定时会采用硬盘来进行存储,内存里存储的只会是最新的数据),

缺点: SXSSFWorkbook使用到了硬盘,当数据到达硬盘以后,也就无法完成数据的克隆或者公式计算,sheet.clone() 已经无法被支持了

XSSFWorkbook VS SXSSFWorkbook 如何选择

在使用过程中,推荐使用 SXSSFWorkbook 或者 XSSFWorkbook

  • 数据量不超过 6W~7W 也涉及到了公式的计算,推荐使用 XSSFWorkbook

  • 如果不涉及到 Excel 公式和样式, 并且数据量较大的情况下,推荐使用 SXSSFWorkbook ;

POI 在 Excel 中的应用示例

POI 写入 Excel 文件:

下面是一个经典的 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.FileOutputStream;
import java.io.IOException;

public class CreateExcel {
   
    public static void main(String[] args) {
   
        XSSFWorkbook workbook = new XSSFWorkbook();
        XSSFSheet sheet = workbook.createSheet("Sheet1");
        // 创建行
        Row row = sheet.createRow(0);
        // 创建单元格并设置值
        Cell cell = row.createCell(0);
        cell.setCellValue("Hello, POI!");
        try {
   
            FileOutputStream outputStream = new FileOutputStream("example.xlsx");
            workbook.write(outputStream);
            workbook.close();
            outputStream.close();
        } catch (IOException e) {
   
            e.printStackTrace();
        }
    }
}

首先创建一个XSSFWorkbook对象,它代表一个 Excel 工作簿。

然后通过workbook.createSheet方法创建一个工作表。接着在工作表中创建行和单元格,并使用cell.setCellValue方法设置单元格的值。

最后将工作簿写入文件流,生成 Excel 文件。

POI 读取 Excel 文件:

下面是一个经典的 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.File;
import java.io.FileInputStream;
import java.io.IOException;

public class ReadExcel {
   
    public static void main(String[] args) {
   
        try {
   
            FileInputStream file = new FileInputStream(new File("example.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;
                        // 可以处理其他类型的单元格数据
                    }
                }
                System.out.println();
            }
            workbook.close();
            file.close();
        } catch (IOException e) {
   
            e.printStackTrace();
        }
    }
}

上面的经典代码, 首先通过FileInputStream读取 Excel 文件,然后创建XSSFWorkbook对象。

通过workbook.getSheetAt方法获取工作表,再使用嵌套的循环遍历行和单元格。

根据单元格的数据类型(如字符串、数字等),使用不同的方法获取单元格的值并打印出来。

尼恩给大家画了一下,这个程序的流程图:

image.png

POI应用场景和优势

应用场景1 : 数据导出和报表生成

在企业级应用中,经常需要将数据库中的数据导出为 Excel 或 Word 格式的报表。

POI 可以方便地将数据填充到表格中,设置表格样式和格式,生成专业的报表。

例如,财务系统可以使用 POI 将财务数据生成 Excel 报表,人力资源系统可以使用 POI 生成员工信息的 Word 文档。

应用场景2 : 文件格式转换

可以将一种 Office 格式转换为另一种格式。

例如,将.doc文件转换为.docx文件,或者将.xls文件转换为.xlsx文件,方便文件的统一管理和共享。

应用场景3 : 小批量 数据 处理

对于大量的 Office 文件,如需要批量修改文件中的数据、样式或者进行数据提取,POI 可以编写自动化脚本进行处理。

例如,在文档审核流程中,批量提取 Word 文档中的关键信息进行检查。

POI 优势

  • 跨平台:作为 Java 库,POI 可以在任何支持 Java 运行环境的平台上使用,这使得它在企业级的异构系统中非常有用。
  • 开源免费:POI 是开源软件,开发者可以免费使用和修改其代码,降低了开发成本。
  • 功能丰富:能够处理多种 Office 文件格式,并且提供了详细的 API 来操作文件的各个元素,如文档结构、内容、样式等。

POI 的不足:

大数据量 , POI 要么是 OOM,要么借助 磁盘,速度太慢。

百万级数据量解决思路

使用传统的 poi 导入导出方式,当数据量过大时,明显会出现 OOM 异常,

因此, 尼恩 推荐大家使用阿里巴巴开源的 easyExcel 框架作为导入导出的媒介

GitHub - alibaba/easyexcel: 快速、简单避免OOM的处理Excel工具

EasyExcel 是阿里巴巴开源的一款基于 Java 的简单、省内存的 Excel 处理工具。

EasyExcel 主要解决了 Apache POI 在处理大量数据时可能出现的内存溢出问题,提供了更加便捷、高效的 Excel 读写操作。

EasyExcel 主要优势有两点:

一:内存优化

EasyExcel 使用了 Sax 解析模式,在解析 Excel 文件时采用一行一行读取的方式,避免了将整个文件加载到内存中,大大减少了内存的使用,适用于处理大型 Excel 文件。

二:使用方便

EasyExcel 提供了简单的 API,使得读取和写入 Excel 数据变得更加容易,开发人员可以通过少量代码实现复杂的 Excel 操作。

EasyExcel的应用示例

导入EasyExcel依赖

pom.xml 中添加以下依赖:

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

使用EasyExcel读取 Excel 文件

以下是一个简单的读取 Excel 文件的示例:

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import java.util.ArrayList;
import java.util.List;

public class ReadExcel {
   
    public static void main(String[] args) {
   
        String fileName = "path/to/your/excel/file.xlsx";
        List<DemoData> demoDataList = new ArrayList<>();

        // 匿名内部类实现监听器
        EasyExcel.read(fileName, DemoData.class, new AnalysisEventListener<DemoData>() {
   
            @Override
            public void invoke(DemoData data, AnalysisContext context) {
   
                demoDataList.add(data);
            }

            @Override
            public void doAfterAllAnalysed(AnalysisContext context) {
   
                // 读取完所有数据后的操作
                System.out.println("Read " + demoDataList.size() + " rows of data.");
            }
        }).sheet().doRead();

        // 打印读取的数据
        for (DemoData data : demoDataList) {
   
            System.out.println(data);
        }
    }
}

@data  // getter 和 setter 方法
class DemoData {
   
    private String name;
    private Integer age;
}

上面使用EasyExcel.read 方法, 读取文件,核心的参数如下:

  • fileName:要读取的 Excel 文件的路径。

  • DemoData.class:将 Excel 中的数据映射到 DemoData 类的对象。

  • AnalysisEventListener<DemoData>:监听器,用于处理读取到的数据。

  • invoke(DemoData data, AnalysisContext context):每读取一行数据,就会调用该方法,将数据添加到 demoDataList 中。

  • doAfterAllAnalysed(AnalysisContext context):读取完所有数据后调用该方法。

AnalysisEventListener 接口分析

上面的代码中,非常重要的是 AnalysisEventListener

image.png

AnalysisEventListener 是 EasyExcel 中的一个核心接口,用于监听 Excel 文件读取过程中的事件。

通过实现这个接口,可以对读取到的数据进行处理,比如数据转换、数据校验、异常处理等。

AnalysisEventListener 的一些主要功能和用法如下:

  1. 数据转换与处理方法 invoke(T data, AnalysisContext context)

    这是 AnalysisEventListener 中最重要的方法之一,EasyExcel 在解析每一行数据后会调用此方法。

    在这里,你可以对数据进行处理,比如数据转换、数据校验等。

    每读取一行数据,invoke 方法就会被调用一次,参数 data 是转换后的 Java 对象,context 提供了分析的上下文信息。

  2. 使用invoke 实现 批量处理

    invoke 方法中, 可以将数据临时存储到一个列表中,当列表达到一定数量后,可以进行批量处理,比如批量存储到数据库中。这样可以提高数据导入的效率。

  3. 异常处理

    onException(Exception exception, AnalysisContext context):当读取过程中出现异常时,会调用此方法。在这里可以进行异常处理,比如记录日志、抛出自定义异常等。

  4. 所有数据读取完毕后的处理 doAfterAllAnalysed

    doAfterAllAnalysed(AnalysisContext context):在所有数据都被分析后,会调用此方法。

    可以用于执行一些清理工作,或者处理那些需要在所有数据读取完毕后才能进行的操作,比如批量存储剩余的数据。

  5. 其他方法

    大家自己去读源码吧。

AnalysisEventListener 监听器是 EasyExcel 处理大数据量 Excel 文件时不可或缺的一部分,它提供了一种流程化的方式来处理数据,使得代码更加简洁和易于维护。

通过实现 AnalysisEventListener,可以灵活地处理 Excel 文件中的数据,使得数据导入变得更加可控和高效。

EasyExcel写入 Excel 文件

以下是一个简单的写入 Excel 文件的示例

import com.alibaba.excel.EasyExcel;
import java.util.ArrayList;
import java.util.List;

public class WriteExcel {
   
    public static void main(String[] args) {
   
        String fileName = "path/to/your/output/file.xlsx";
        List<DemoData> demoDataList = new ArrayList<>();
        demoDataList.add(new DemoData("Alice", 25));
        demoDataList.add(new DemoData("Bob", 30));

        EasyExcel.write(fileName, DemoData.class).sheet("Sheet1").doWrite(demoDataList);
    }
}

使用EasyExcel.write(fileName, DemoData.class) 进行写入, 参数介绍如下:

  • fileName:要写入的 Excel 文件的路径。
  • DemoData.class:要写入的数据对应的类。

  • sheet("Sheet1"):指定写入的工作表名称。

  • doWrite(demoDataList):将 demoDataList 中的数据写入 Excel 文件。

百万级数据量的高速导入的架构设计

尼恩设计了 高性能 EasyExcel 分片读取 + 高性能Distruptor 队列缓冲 + 高并发 batch批量写入 结合的架构方案,具体如下:

  • 高性能分片读取:

    针对百万数据读取,选择分片读取,防止出现 OOM 。

    这里使用EasyExcel 高性能组件进行分片读取。

  • 高性能 队列缓冲:

    百万数据的数据,需要用一个队列集合缓存起来,以方便做一些必要的业务处理如校验,也方便很后面的的批量写入。

  • 高并发批量写入:

    选择batch批写的方式 , 实现百万数据的写入,这里使用Mybatis-plus的分批插入,并且结合采用多线程处理。

image.png

交互图:数据导入、队列缓冲和 写入模块 三者之间的交互图

以下是一个完整的交互图,展示了上述架构方案的交互数据流,包含数据导入模块、高并发队列缓冲和数据写入模块。

image.png

百万级数据量的高速导入的代码实现

以下是一个完整的实现上述架构方案的示例代码,包含数据导入模块、高并发队列缓冲和数据写入模块。

使用 Spring Boot 和 MyBatis-Plus 框架,并结合 EasyExcel 进行数据读取和 MyBatis-Plus 进行数据写入,同时使用 Disruptor 作为高并发队列缓冲:

1. 引入依赖

首先,在 pom.xml 文件中添加所需的依赖:

<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.5.6</version>
    </dependency>
    <!-- EasyExcel 依赖 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>easyexcel</artifactId>
        <version>3.1.1</version>
    </dependency>
    <!-- MyBatis-Plus 依赖 -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.3.4</version>
    </dependency>
    <!-- Disruptor 依赖 -->
    <dependency>
        <groupId>com.lmax</groupId>
        <artifactId>disruptor</artifactId>
        <version>3.4.4</version>
    </dependency>
    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.26</version>
    </dependency>
    <!-- Lombok 依赖,用于简化实体类代码 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.22</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

2. 配置 Spring Boot 应用程序

创建一个 Spring Boot 主应用程序类:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.mybatis.spring.annotation.MapperScan;


@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@MapperScan("com.example.demo.mapper")
public class ExcelImportDemoApplication {
   
    public static void main(String[] args) {
   
        SpringApplication.run(ExcelImportDemoApplication.class, args);
    }
}

3. 创建实体类

创建一个与数据库表对应的实体类 DataRecord

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;


@Data
@TableName("data_record")
public class DataRecord {
   
    @TableId
    private Long id;
    private String column1;
    private String column2;
    private String column3;
}

4. 创建控制器类

DataImportController 类, 这个非常简单,核心就是下面的方法:

importData(@RequestParam("file") MultipartFile file) 方法处理文件上传,调用 DataImportService 进行数据导入。

import com.example.demo.service.DataImportService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;


@RestController
public class DataImportController {
   
    // 注入数据导入服务
    @Autowired
    private DataImportService dataImportService;


    // 处理文件上传和数据导入的接口
    @PostMapping("/import")
    public String importData(@RequestParam("file") MultipartFile file) throws IOException {
   
        // 调用数据导入服务进行数据导入
        dataImportService.importData(file.getInputStream());
        return "数据导入成功";
    }
}

5. 高性能分片读取 数据 服务类 DataImportService

高性能分片读取 数据 服务类 DataImportService 的处理流程的简单介绍:

  1. 接收文件输入流

DataImportService 首先会接收一个文件的输入流,这个输入流是要导入的数据的来源,通常可以是用户上传的 Excel 文件或其他数据源。

  1. 创建 EasyExcel 监听器

为 EasyExcel 创建一个名为 DataRecordExcelListener 的监听器。

这个监听器的作用是在 EasyExcel 读取数据的过程中处理数据的读取事件。

  1. 开始使用 EasyExcel 读取文件

调用 EasyExcel 的 read 方法开始读取文件,使用创建的 DataRecordExcelListener 来监听数据读取的过程。

  1. 数据读取和批处理

在读取过程中,会逐行读取文件中的数据。

每读取一行数据,将其添加到一个批处理列表中。

当批处理列表中的数据量达到 10000 条时:

  • 将该批处理列表的数据发布到 DataRecordDisruptor 中,以便后续处理。
  • 清空批处理列表,为存储下一批数据做好准备。
  1. 处理剩余数据
  • 当文件中没有更多的数据需要读取时,会检查批处理列表是否还有未处理的数据。
  • 如果批处理列表不为空(即还有未达到 10000 条的数据),将其发布到 DataRecordDisruptor` 中。

总体而言,DataImportService 利用 EasyExcel 逐行读取文件数据,将数据按批处理列表存储,达到一定数量后将数据发送到 DataRecordDisruptor 进行后续的处理。

这个过程通过批处理和使用 DataRecordDisruptor 实现了高性能的分片读取和数据缓冲,避免了大量数据读取时可能出现的内存溢出问题,并提高了数据处理的性能和效率。

高性能分片读取 数据 服务类 DataImportService 流程图如下

image.png

高性能分片读取 数据 服务类 DataImportService 参考代码 如下

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.example.demo.disruptor.DataRecordDisruptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;


@Service
public class DataImportService {
   
    // 注入 Disruptor 组件
    @Autowired
    private DataRecordDisruptor dataRecordDisruptor;


    // 数据导入的主要方法,接收文件输入流
    public void importData(InputStream inputStream) {
   
        // 使用 EasyExcel 进行分片读取,添加自定义的监听器
        EasyExcel.read(inputStream, DataRecord.class, new DataRecordExcelListener(dataRecordDisruptor)).sheet().doRead();
    }


    // EasyExcel 的监听器,用于处理读取到的数据
    public static class DataRecordExcelListener extends AnalysisEventListener<DataRecord> {
   
        private final DataRecordDisruptor disruptor;
        // 存储数据的批处理列表
        private final List<DataRecord> batch = new ArrayList<>();


        public DataRecordExcelListener(DataRecordDisruptor disruptor) {
   
            this.disruptor = disruptor;
        }


        @Override
        public void invoke(DataRecord data, AnalysisContext context) {
   
            // 将读取到的数据添加到批处理列表中
            batch.add(data);
            // 当达到批处理大小,将数据发布到 Disruptor 进行处理
            if (batch.size() >= 10000) {
   
                disruptor.publish(batch);
                batch.clear();
            }
        }


        @Override
        public void doAfterAllAnalysed(AnalysisContext context) {
   
            // 处理最后一批数据,确保所有数据都被处理
            if (!batch.isEmpty()) {
   
                disruptor.publish(batch);
            }
        }
    }
}

DataImportService 类importData(InputStream inputStream) 方法:

使用 EasyExcel 进行分片读取,使用自定义的 DataRecordExcelListener 监听器处理读取到的数据。

DataRecordExcelListener是一个内部类,两个方法如下:

第一个方法 invoke(DataRecord data, AnalysisContext context)

这个方法将读取的数据添加到 batch 列表,当 batch 大小达到 10000 时,将数据发布到 DataRecordDisruptor

第二个方法doAfterAllAnalysed(AnalysisContext context) 方法

这个方法确保最后一批数据也能被处理。

5. 高性能Distruptor 队列缓冲 Disruptor 无锁队列

这里设计了一个DataRecordDisruptor 类,在整个数据导入架构中扮演着重要的角色,它作为高并发队列缓冲:

  • 一方面利用 Disruptor 的高性能特性缓存和缓冲数据,

  • 另一方面将数据以合适的批处理大小进行批量存储,减少了数据库的操作次数,提高了整体的数据处理效率。

DataRecordDisruptor结合了 Disruptor 的高性能和 MyBatis-Plus 的批量插入功能,为处理大量数据提供了一种高效的机制, 核心的流程如下:

  1. 初始化和启动阶段

首先,初始化 Disruptor:首先会设置 Disruptor 的环形缓冲区大小、事件工厂、线程工厂、生产者类型和等待策略。
环形缓冲区大小决定了可以存储多少数据事件,这里设置的大小可根据实际需求调整。

事件工厂用于创建 DataRecordEvent 对象,线程工厂负责创建处理数据的线程,生产者类型设置为多生产者模式,以支持多个来源的数据,等待策略则是 BlockingWaitStrategy,它会在缓冲区满时阻塞生产者,防止数据丢失。

然后,启动 Disruptor:完成上述设置后,启动 Disruptor,使其处于可接收数据的状态。

  1. 数据接收和存储阶段

等待接收数据:启动后,DataRecordDisruptor 处于等待接收数据的状态,它将接收来自 DataImportService 的数据。
存储数据到环形缓冲区:当接收到来自 DataImportService 的数据时,将这些数据存储在环形缓冲区中。环形缓冲区是 Disruptor 的核心组件,它提供了高效的数据存储和访问机制。

  1. 异步数据处理阶段
  • 数据处理事件触发:当数据存储到环形缓冲区时,会触发相应的数据处理事件。
  • 添加数据到批处理列表:将触发的数据添加到一个批处理列表中。这个批处理列表用于临时存储数据,方便后续的批量操作。
  • 判断是否达到批处理条件:检查批处理列表的大小是否达到或超过 1000 条数据,或者是否处理完一批数据。这个批处理大小是为了优化数据库操作,减少数据库交互次数。
  • 批量插入操作:如果满足上述条件,使用 MyBatis-Plus 进行批量插入操作,将数据存储到数据库中。
  • 清空批处理列表:完成批量插入后,清空批处理列表,为下一批数据的存储和处理做好准备。
  1. 循环处理
  • 只要还需要接收数据,整个过程会不断重复上述步骤,持续进行数据的接收、存储、处理和插入操作,直到没有更多的数据需要处理。

image.png

DataRecordDisruptor参考代码 如下

import com.lmax.disruptor.BlockingWaitStrategy;
import com.lmax.disruptor.EventFactory;
import com.lmax.disruptor.EventHandler;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.ProducerType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;


@Component
public class DataRecordDisruptor {
   
    // Disruptor 环形缓冲区的大小,可根据需要调整
    private static final int BUFFER_SIZE = 1024 * 1024;
    // 批量插入的大小,可根据性能测试调整
    private static final int BATCH_SIZE = 1000;
    private final Disruptor<DataRecordEvent> disruptor;
    private final RingBuffer<DataRecordEvent> ringBuffer;


    @Autowired
    private DataRecordMapper dataRecordMapper;


    public DataRecordDisruptor() {
   
        // 事件工厂,用于创建 DataRecordEvent 实例
        EventFactory<DataRecordEvent> factory = DataRecordEvent::new;
        // 创建线程工厂,使用默认的线程创建机制
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        // 初始化 Disruptor,使用多生产者模式和阻塞等待策略
        disruptor = new Disruptor<>(factory, BUFFER_SIZE, threadFactory, ProducerType.MULTI, new BlockingWaitStrategy());
        // 为 Disruptor 注册事件处理器
        disruptor.handleEventsWith(new DataRecordEventHandler());
        // 启动 Disruptor
        ringBuffer = disruptor.start();
    }


    // 发布数据到 Disruptor 的方法
    public void publish(List<DataRecord> dataRecords) {
   
        // 获取环形缓冲区的可用序列范围
        long sequence = ringBuffer.next(dataRecords.size());
        try {
   
            for (int i = 0; i < dataRecords.size(); i++) {
   
                // 将数据存储到环形缓冲区的事件中
                DataRecordEvent event = ringBuffer.get(sequence + i);
                event.setDataRecord(dataRecords.get(i));
            }
        } finally {
   
            // 发布事件
            ringBuffer.publish(sequence, sequence + dataRecords.size() - 1);
        }
    }


    // 内部类,作为 Disruptor 的事件对象
    private static class DataRecordEvent {
   
        private DataRecord dataRecord;


        public DataRecord getDataRecord() {
   
            return dataRecord;
        }


        public void setDataRecord(DataRecord dataRecord) {
   
            this.dataRecord = dataRecord;
        }
    }


    // 事件处理器,负责将数据批量插入数据库
    private class DataRecordEventHandler implements EventHandler<DataRecordEvent> {
   
        private final List<DataRecord> batch = new ArrayList<>();


        @Override
        public void onEvent(DataRecordEvent event, long sequence, boolean endOfBatch) {
   
            // 将事件中的数据添加到批处理列表中
            batch.add(event.getDataRecord());
            // 当达到批处理大小或处理完一批数据时进行插入操作
            if (batch.size() >= BATCH_SIZE || endOfBatch) {
   
                insertBatch(batch);
                batch.clear();
            }

            // 尼恩提示: 这里需要改造一下,加上一个结束的空事件
        }


        // 执行批量插入的方法
        private void insertBatch(List<DataRecord> dataRecords) {
   
            try {
   
                // 使用 MyBatis-Plus 的批量插入功能
                dataRecordMapper.insertBatch(dataRecords);
            } catch (Exception e) {
   
                // 异常处理,可添加日志记录等操作
                e.printStackTrace();
            }
        }
    }
}

7. 高并发 batch批量写入 的 Mapper 接口

数据写入模块包括:

  • DataRecordMapper 接口:

    自定义 insertBatch(List<DataRecord> dataRecords) 方法,在 XML 映射文件中实现批量插入逻辑。

  • DataRecordMapper.xml:

    使用 MyBatis-Plus 的 <foreach> 标签实现批量插入。

创建一个 MyBatis-Plus 的 Mapper 接口:

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.DataRecord;


@Mapper
public interface DataRecordMapper extends BaseMapper<DataRecord> {
   
    // 自定义的批量插入方法
    void insertBatch(List<DataRecord> dataRecords);
}

8. 实现 MyBatis-Plus 批量插入(XML)

resources/mapper/DataRecordMapper.xml 中添加以下代码:

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.DataRecordMapper">
    <insert id="insertBatch">
        insert into data_record (column1, column2, column3) values
        <foreach collection="list" item="item" separator=",">
            (#{item.column1}, #{item.column2}, #{item.column3})
        </foreach>
    </insert>
</mapper>

也可以使用手动提交事务 + preparestatement,进行批量插入。

具体的实现代码,这里忽略。

后面介绍尼恩Java面试宝典配套视频的时候,会配合视频进行介绍。

9. 配置文件

application.properties 中配置数据库连接:

spring.datasource.url=jdbc:mysql://localhost:3306/your_database_name?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=your_password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

百万级数据量的高速导入的总结

尼恩设计了 高性能 EasyExcel 分片读取 + 高性能Distruptor 队列缓冲 + 高并发 batch批量写入 结合的架构方案,具体如下:

  • 这个示例实现了一个完整的数据导入架构,使用 EasyExcel 进行高性能分片读取,避免了内存溢出问题。
  • 使用 Disruptor 作为高并发队列缓冲,将数据存储在环形缓冲区中,方便进行后续的业务处理。
  • 使用 MyBatis-Plus 的批量插入功能,并结合多线程处理,实现了高并发的批量数据写入。

通过以上架构和代码,你可以实现百万级数据量的快速导入,利用各组件的优势提高系统的性能和可扩展性。

性能可以由原来的500秒优化到20秒!

说在最后:有问题找老架构取经‍

回到开始的时候的面试题:招商银行的Java后端面试真题

被狠狠拷打了,问的人都懵了。 项目场景题太难了,不好好准备,真的答不出!

image.png

按照此文的套路去回答,一定会 吊打面试官,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。

在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。

很多小伙伴刷完后, 吊打面试官, 大厂横着走。

在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。

另外,如果没有面试机会,可以找尼恩来改简历、做帮扶。前段时间,刚指导一个小伙 暴涨200%(2倍),29岁/7年/双非一本 , 从13K 涨到 37K ,逆天改命

狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。

尼恩技术圣经系列PDF

……完整版尼恩技术圣经PDF集群,请找尼恩领取

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓

相关文章
|
9天前
|
调度 云计算 芯片
云超算技术跃进,阿里云牵头制定我国首个云超算国家标准
近日,由阿里云联合中国电子技术标准化研究院主导制定的首个云超算国家标准已完成报批,不久后将正式批准发布。标准规定了云超算服务涉及的云计算基础资源、资源管理、运行和调度等方面的技术要求,为云超算服务产品的设计、实现、应用和选型提供指导,为云超算在HPC应用和用户的大范围采用奠定了基础。
179604 21
|
1天前
|
弹性计算 人工智能 安全
对话 | ECS如何构筑企业上云的第一道安全防线
随着中小企业加速上云,数据泄露、网络攻击等安全威胁日益严重。阿里云推出深度访谈栏目,汇聚产品技术专家,探讨云上安全问题及应对策略。首期节目聚焦ECS安全性,提出三道防线:数据安全、网络安全和身份认证与权限管理,确保用户在云端的数据主权和业务稳定。此外,阿里云还推出了“ECS 99套餐”,以高性价比提供全面的安全保障,帮助中小企业安全上云。
对话 | ECS如何构筑企业上云的第一道安全防线
|
18天前
|
人工智能 自然语言处理 前端开发
从0开始打造一款APP:前端+搭建本机服务,定制暖冬卫衣先到先得
通义灵码携手科技博主@玺哥超carry 打造全网第一个完整的、面向普通人的自然语言编程教程。完全使用 AI,再配合简单易懂的方法,只要你会打字,就能真正做出一个完整的应用。
9518 25
|
4天前
|
机器学习/深度学习 分布式计算 供应链
阿里云先知安全沙龙(上海站) ——大模型基础设施安全攻防
大模型基础设施的安全攻防体系涵盖恶意输入防御和基础设施安全,包括框架、三方库、插件、平台、模型和系统安全。关键漏洞如CVE-2023-6019(Ray框架命令注入)、CVE-2024-5480(PyTorch分布式RPC)及llama.cpp中的多个漏洞,强调了代码安全性的重要性。模型文件安全方面,需防范pickle反序列化等风险,建议使用Safetensors格式。相关实践包括构建供应链漏洞库、智能化漏洞分析和深度检测,确保全方位防护。
|
6天前
|
JSON 分布式计算 数据处理
加速数据处理与AI开发的利器:阿里云MaxFrame实验评测
随着数据量的爆炸式增长,传统数据分析方法逐渐显现出局限性。Python作为数据科学领域的主流语言,因其简洁易用和丰富的库支持备受青睐。阿里云推出的MaxFrame是一个专为Python开发者设计的分布式计算框架,旨在充分利用MaxCompute的强大能力,提供高效、灵活且易于使用的工具,应对大规模数据处理需求。MaxFrame不仅继承了Pandas等流行数据处理库的友好接口,还通过集成先进的分布式计算技术,显著提升了数据处理的速度和效率。
|
22天前
|
Cloud Native Apache 流计算
资料合集|Flink Forward Asia 2024 上海站
Apache Flink 年度技术盛会聚焦“回顾过去,展望未来”,涵盖流式湖仓、流批一体、Data+AI 等八大核心议题,近百家厂商参与,深入探讨前沿技术发展。小松鼠为大家整理了 FFA 2024 演讲 PPT ,可在线阅读和下载。
5158 15
资料合集|Flink Forward Asia 2024 上海站
|
1月前
|
人工智能 自动驾驶 大数据
预告 | 阿里云邀您参加2024中国生成式AI大会上海站,马上报名
大会以“智能跃进 创造无限”为主题,设置主会场峰会、分会场研讨会及展览区,聚焦大模型、AI Infra等热点议题。阿里云智算集群产品解决方案负责人丛培岩将出席并发表《高性能智算集群设计思考与实践》主题演讲。观众报名现已开放。
|
14天前
|
Docker 容器
|
2天前
|
机器学习/深度学习 人工智能 安全
通义视觉推理大模型QVQ-72B-preview重磅上线
Qwen团队推出了新成员QVQ-72B-preview,这是一个专注于提升视觉推理能力的实验性研究模型。提升了视觉表示的效率和准确性。它在多模态评测集如MMMU、MathVista和MathVision上表现出色,尤其在数学推理任务中取得了显著进步。尽管如此,该模型仍存在一些局限性,仍在学习和完善中。
|
17天前
|
消息中间件 人工智能 运维
12月更文特别场——寻找用云高手,分享云&AI实践
我们寻找你,用云高手,欢迎分享你的真知灼见!
1323 76