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

打印 上一主题 下一主题

主题 881|帖子 881|积分 2643

本文原文链接

尼恩说在前面

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

所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技能肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”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 工作簿 写入的案例。
  1. import org.apache.poi.ss.usermodel.Cell;
  2. import org.apache.poi.ss.usermodel.Row;
  3. import org.apache.poi.xssf.usermodel.XSSFSheet;
  4. import org.apache.poi.xssf.usermodel.XSSFWorkbook;
  5. import java.io.FileOutputStream;
  6. import java.io.IOException;
  7. public class CreateExcel {
  8.     public static void main(String[] args) {
  9.         XSSFWorkbook workbook = new XSSFWorkbook();
  10.         XSSFSheet sheet = workbook.createSheet("Sheet1");
  11.         // 创建行
  12.         Row row = sheet.createRow(0);
  13.         // 创建单元格并设置值
  14.         Cell cell = row.createCell(0);
  15.         cell.setCellValue("Hello, POI!");
  16.         try {
  17.             FileOutputStream outputStream = new FileOutputStream("example.xlsx");
  18.             workbook.write(outputStream);
  19.             workbook.close();
  20.             outputStream.close();
  21.         } catch (IOException e) {
  22.             e.printStackTrace();
  23.         }
  24.     }
  25. }
复制代码
首先创建一个XSSFWorkbook对象,它代表一个 Excel 工作簿。
然后通过workbook.createSheet方法创建一个工作表。接着在工作表中创建行和单位格,并使用cell.setCellValue方法设置单位格的值。
最后将工作簿写入文件流,天生 Excel 文件。
POI 读取 Excel 文件:

下面是一个经典的 Excel 工作簿 写入的案例。
  1. import org.apache.poi.ss.usermodel.Cell;
  2. import org.apache.poi.ss.usermodel.Row;
  3. import org.apache.poi.xssf.usermodel.XSSFSheet;
  4. import org.apache.poi.xssf.usermodel.XSSFWorkbook;
  5. import java.io.File;
  6. import java.io.FileInputStream;
  7. import java.io.IOException;
  8. public class ReadExcel {
  9.     public static void main(String[] args) {
  10.         try {
  11.             FileInputStream file = new FileInputStream(new File("example.xlsx"));
  12.             XSSFWorkbook workbook = new XSSFWorkbook(file);
  13.             XSSFSheet sheet = workbook.getSheetAt(0);
  14.             for (Row row : sheet) {
  15.                 for (Cell cell : row) {
  16.                     switch (cell.getCellType()) {
  17.                         case STRING:
  18.                             System.out.print(cell.getStringCellValue() + " ");
  19.                             break;
  20.                         case NUMERIC:
  21.                             System.out.print(cell.getNumericCellValue() + " ");
  22.                             break;
  23.                         // 可以处理其他类型的单元格数据
  24.                     }
  25.                 }
  26.                 System.out.println();
  27.             }
  28.             workbook.close();
  29.             file.close();
  30.         } catch (IOException e) {
  31.             e.printStackTrace();
  32.         }
  33.     }
  34. }
复制代码
上面的经典代码, 首先通过FileInputStream读取 Excel 文件,然后创建XSSFWorkbook对象。
通过workbook.getSheetAt方法获取工作表,再使用嵌套的循环遍历行和单位格。
根据单位格的数据类型(如字符串、数字等),使用不同的方法获取单位格的值并打印出来。
尼恩给大家画了一下,这个程序的流程图:

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 中添加以下依赖:
  1. <dependency>
  2.     <groupId>com.alibaba</groupId>
  3.     <artifactId>easyexcel</artifactId>
  4.     <version>3.1.1</version>
  5. </dependency>
复制代码
使用EasyExcel读取 Excel 文件

以下是一个简单的读取 Excel 文件的示例:
  1. import com.alibaba.excel.EasyExcel;
  2. import com.alibaba.excel.context.AnalysisContext;
  3. import com.alibaba.excel.event.AnalysisEventListener;
  4. import java.util.ArrayList;
  5. import java.util.List;
  6. public class ReadExcel {
  7.     public static void main(String[] args) {
  8.         String fileName = "path/to/your/excel/file.xlsx";
  9.         List<DemoData> demoDataList = new ArrayList<>();
  10.         // 匿名内部类实现监听器
  11.         EasyExcel.read(fileName, DemoData.class, new AnalysisEventListener<DemoData>() {
  12.             @Override
  13.             public void invoke(DemoData data, AnalysisContext context) {
  14.                 demoDataList.add(data);
  15.             }
  16.             @Override
  17.             public void doAfterAllAnalysed(AnalysisContext context) {
  18.                 // 读取完所有数据后的操作
  19.                 System.out.println("Read " + demoDataList.size() + " rows of data.");
  20.             }
  21.         }).sheet().doRead();
  22.         // 打印读取的数据
  23.         for (DemoData data : demoDataList) {
  24.             System.out.println(data);
  25.         }
  26.     }
  27. }
  28. @data  // getter 和 setter 方法
  29. class DemoData {
  30.     private String name;
  31.     private Integer age;
  32. }
复制代码
上面使用EasyExcel.read 方法, 读取文件,核心的参数如下:


  • fileName:要读取的 Excel 文件的路径。
  • DemoData.class:将 Excel 中的数据映射到 DemoData 类的对象。
  • AnalysisEventListener<DemoData>:监听器,用于处理读取到的数据。
  • invoke(DemoData data, AnalysisContext context):每读取一行数据,就会调用该方法,将数据添加到 demoDataList 中。
  • doAfterAllAnalysed(AnalysisContext context):读取完所有数据后调用该方法。
AnalysisEventListener 接口分析

上面的代码中,非常告急的是 AnalysisEventListener

AnalysisEventListener 是 EasyExcel 中的一个核心接口,用于监听 Excel 文件读取过程中的事件。
通过实现这个接口,可以对读取到的数据进行处理,好比数据转换、数据校验、异常处理等。
AnalysisEventListener 的一些重要功能和用法如下:

  • 数据转换与处理方法 invoke(T data, AnalysisContext context)
    这是 AnalysisEventListener 中最告急的方法之一,EasyExcel 在剖析每一行数据后会调用此方法。
    在这里,你可以对数据进行处理,好比数据转换、数据校验等。
    每读取一行数据,invoke 方法就会被调用一次,参数 data 是转换后的 Java 对象,context 提供了分析的上下文信息。
  • 使用invoke 实现 批量处理
    在 invoke 方法中, 可以将数据临时存储到一个列表中,当列表达到肯定数量后,可以进行批量处理,好比批量存储到数据库中。如许可以进步数据导入的效率。
  • 异常处理
    onException(Exception exception, AnalysisContext context):当读取过程中出现异常时,会调用此方法。在这里可以进行异常处理,好比记录日志、抛出自界说异常等。
  • 所有数据读取完毕后的处理 doAfterAllAnalysed
    doAfterAllAnalysed(AnalysisContext context):在所有数据都被分析后,会调用此方法。
    可以用于执行一些清理工作,或者处理那些必要在所有数据读取完毕后才能进行的操作,好比批量存储剩余的数据。
  • 其他方法
    大家本身去读源码吧。
AnalysisEventListener 监听器是 EasyExcel 处理大数据量 Excel 文件时不可或缺的一部门,它提供了一种流程化的方式来处理数据,使得代码更加简洁和易于维护。
通过实现 AnalysisEventListener,可以机动地处理 Excel 文件中的数据,使得数据导入变得更加可控和高效。
EasyExcel写入 Excel 文件

以下是一个简单的写入 Excel 文件的示例
  1. import com.alibaba.excel.EasyExcel;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. public class WriteExcel {
  5.     public static void main(String[] args) {
  6.         String fileName = "path/to/your/output/file.xlsx";
  7.         List<DemoData> demoDataList = new ArrayList<>();
  8.         demoDataList.add(new DemoData("Alice", 25));
  9.         demoDataList.add(new DemoData("Bob", 30));
  10.         EasyExcel.write(fileName, DemoData.class).sheet("Sheet1").doWrite(demoDataList);
  11.     }
  12. }
复制代码
使用EasyExcel.write(fileName, DemoData.class) 进行写入, 参数介绍如下:


  • fileName:要写入的 Excel 文件的路径。
  • DemoData.class:要写入的数据对应的类。
  • sheet("Sheet1"):指定写入的工作表名称。
  • doWrite(demoDataList):将 demoDataList 中的数据写入 Excel 文件。
百万级数据量的高速导入的架构设计

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


  • 高性能分片读取:
    针对百万数据读取,选择分片读取,防止出现 OOM 。
    这里使用EasyExcel 高性能组件进行分片读取。
  • 高性能 队列缓冲:
    百万数据的数据,必要用一个队列聚集缓存起来,以方便做一些必要的业务处理如校验,也方便很后面的的批量写入。
  • 高并发批量写入:
    选择batch批写的方式 , 实现百万数据的写入,这里使用Mybatis-plus的分批插入,并且结合接纳多线程处理。

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

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

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

以下是一个完备的实现上述架构方案的示例代码,包含数据导入模块、高并发队列缓冲和数据写入模块。
使用 Spring Boot 和 MyBatis-Plus 框架,并结合 EasyExcel 进行数据读取和 MyBatis-Plus 进行数据写入,同时使用 Disruptor 作为高并发队列缓冲:
1. 引入依赖

首先,在 pom.xml 文件中添加所需的依赖:
  1. <dependencies>
  2.     <!-- Spring Boot Starter Web -->
  3.     <dependency>
  4.         <groupId>org.springframework.boot</groupId>
  5.         <artifactId>spring-boot-starter-web</artifactId>
  6.         <version>2.5.6</version>
  7.     </dependency>
  8.     <!-- EasyExcel 依赖 -->
  9.     <dependency>
  10.         <groupId>com.alibaba</groupId>
  11.         <artifactId>easyexcel</artifactId>
  12.         <version>3.1.1</version>
  13.     </dependency>
  14.     <!-- MyBatis-Plus 依赖 -->
  15.     <dependency>
  16.         <groupId>com.baomidou</groupId>
  17.         <artifactId>mybatis-plus-boot-starter</artifactId>
  18.         <version>3.4.3.4</version>
  19.     </dependency>
  20.     <!-- Disruptor 依赖 -->
  21.     <dependency>
  22.         <groupId>com.lmax</groupId>
  23.         <artifactId>disruptor</artifactId>
  24.         <version>3.4.4</version>
  25.     </dependency>
  26.     <!-- MySQL 驱动 -->
  27.     <dependency>
  28.         <groupId>mysql</groupId>
  29.         <artifactId>mysql-connector-java</artifactId>
  30.         <version>8.0.26</version>
  31.     </dependency>
  32.     <!-- Lombok 依赖,用于简化实体类代码 -->
  33.     <dependency>
  34.         <groupId>org.projectlombok</groupId>
  35.         <artifactId>lombok</artifactId>
  36.         <version>1.18.22</version>
  37.         <scope>provided</scope>
  38.     </dependency>
  39. </dependencies>
复制代码
2. 配置 Spring Boot 应用程序

创建一个 Spring Boot 主应用程序类:
  1. import org.springframework.boot.SpringApplication;
  2. import org.springframework.boot.autoconfigure.SpringBootApplication;
  3. import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
  4. import org.mybatis.spring.annotation.MapperScan;
  5. @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
  6. @MapperScan("com.example.demo.mapper")
  7. public class ExcelImportDemoApplication {
  8.     public static void main(String[] args) {
  9.         SpringApplication.run(ExcelImportDemoApplication.class, args);
  10.     }
  11. }
复制代码
3. 创建实体类

创建一个与数据库表对应的实体类 DataRecord:
  1. import com.baomidou.mybatisplus.annotation.TableId;
  2. import com.baomidou.mybatisplus.annotation.TableName;
  3. import lombok.Data;
  4. @Data
  5. @TableName("data_record")
  6. public class DataRecord {
  7.     @TableId
  8.     private Long id;
  9.     private String column1;
  10.     private String column2;
  11.     private String column3;
  12. }
复制代码
4. 创建控制器类

DataImportController 类, 这个非常简单,核心就是下面的方法:
importData(@RequestParam("file") MultipartFile file) 方法处理文件上传,调用 DataImportService 进行数据导入。
  1. import com.example.demo.service.DataImportService;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.web.bind.annotation.PostMapping;
  4. import org.springframework.web.bind.annotation.RequestParam;
  5. import org.springframework.web.bind.annotation.RestController;
  6. import org.springframework.web.multipart.MultipartFile;
  7. import java.io.IOException;
  8. @RestController
  9. public class DataImportController {
  10.     // 注入数据导入服务
  11.     @Autowired
  12.     private DataImportService dataImportService;
  13.     // 处理文件上传和数据导入的接口
  14.     @PostMapping("/import")
  15.     public String importData(@RequestParam("file") MultipartFile file) throws IOException {
  16.         // 调用数据导入服务进行数据导入
  17.         dataImportService.importData(file.getInputStream());
  18.         return "数据导入成功";
  19.     }
  20. }
复制代码
5. 高性能分片读取 数据 服务类 DataImportService

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

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

  • 创建 EasyExcel 监听器
为 EasyExcel 创建一个名为 DataRecordExcelListener 的监听器。
这个监听器的作用是在 EasyExcel 读取数据的过程中处理数据的读取事件。

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

  • 数据读取和批处理
在读取过程中,会逐行读取文件中的数据。
每读取一行数据,将其添加到一个批处理列表中。
当批处理列表中的数据量达到 10000 条时:


  • 将该批处理列表的数据发布到 DataRecordDisruptor 中,以便后续处理。
  • 清空批处理列表,为存储下一批数据做好准备。

  • 处理剩余数据


  • 当文件中没有更多的数据必要读取时,会检查批处理列表是否还有未处理的数据。
  • 假如批处理列表不为空(即还有未达到 10000 条的数据),将其发布到 DataRecordDisruptor` 中。
总体而言,DataImportService 利用 EasyExcel 逐行读取文件数据,将数据按批处理列表存储,达到肯定数量后将数据发送到 DataRecordDisruptor 进行后续的处理。
这个过程通过批处理和使用 DataRecordDisruptor 实现了高性能的分片读取和数据缓冲,避免了大量数据读取时大概出现的内存溢出题目,并进步了数据处理的性能和效率。
高性能分片读取 数据 服务类 DataImportService 流程图如下

高性能分片读取 数据 服务类 DataImportService 参考代码 如下
  1. import com.alibaba.excel.EasyExcel;
  2. import com.alibaba.excel.context.AnalysisContext;
  3. import com.alibaba.excel.event.AnalysisEventListener;
  4. import com.example.demo.disruptor.DataRecordDisruptor;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.stereotype.Service;
  7. import java.io.InputStream;
  8. import java.util.ArrayList;
  9. import java.util.List;
  10. @Service
  11. public class DataImportService {
  12.     // 注入 Disruptor 组件
  13.     @Autowired
  14.     private DataRecordDisruptor dataRecordDisruptor;
  15.     // 数据导入的主要方法,接收文件输入流
  16.     public void importData(InputStream inputStream) {
  17.         // 使用 EasyExcel 进行分片读取,添加自定义的监听器
  18.         EasyExcel.read(inputStream, DataRecord.class, new DataRecordExcelListener(dataRecordDisruptor)).sheet().doRead();
  19.     }
  20.     // EasyExcel 的监听器,用于处理读取到的数据
  21.     public static class DataRecordExcelListener extends AnalysisEventListener<DataRecord> {
  22.         private final DataRecordDisruptor disruptor;
  23.         // 存储数据的批处理列表
  24.         private final List<DataRecord> batch = new ArrayList<>();
  25.         public DataRecordExcelListener(DataRecordDisruptor disruptor) {
  26.             this.disruptor = disruptor;
  27.         }
  28.         @Override
  29.         public void invoke(DataRecord data, AnalysisContext context) {
  30.             // 将读取到的数据添加到批处理列表中
  31.             batch.add(data);
  32.             // 当达到批处理大小,将数据发布到 Disruptor 进行处理
  33.             if (batch.size() >= 10000) {
  34.                 disruptor.publish(batch);
  35.                 batch.clear();
  36.             }
  37.         }
  38.         @Override
  39.         public void doAfterAllAnalysed(AnalysisContext context) {
  40.             // 处理最后一批数据,确保所有数据都被处理
  41.             if (!batch.isEmpty()) {
  42.                 disruptor.publish(batch);
  43.             }
  44.         }
  45.     }
  46. }
复制代码
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 的批量插入功能,为处理大量数据提供了一种高效的机制, 核心的流程如下:

  • 初始化和启动阶段
首先,初始化 Disruptor:首先会设置 Disruptor 的环形缓冲区大小、事件工厂、线程工厂、生产者类型和等待策略。
环形缓冲区大小决定了可以存储多少数据事件,这里设置的大小可根据实际需求调整。
事件工厂用于创建 DataRecordEvent 对象,线程工厂负责创建处理数据的线程,生产者类型设置为多生产者模式,以支持多个来源的数据,等待策略则是 BlockingWaitStrategy,它会在缓冲区满时阻塞生产者,防止数据丢失。
然后,启动 Disruptor:完成上述设置后,启动 Disruptor,使其处于可吸取数据的状态。

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

  • 异步数据处理阶段


  • 数据处理事件触发:当数据存储到环形缓冲区时,会触发相应的数据处理事件。
  • 添加数据到批处理列表:将触发的数据添加到一个批处理列表中。这个批处理列表用于临时存储数据,方便后续的批量操作。
  • 判断是否达到批处理条件:检查批处理列表的大小是否达到或凌驾 1000 条数据,或者是否处理完一批数据。这个批处理大小是为了优化数据库操作,淘汰数据库交互次数。
  • 批量插入操作:假如满足上述条件,使用 MyBatis-Plus 进行批量插入操作,将数据存储到数据库中。
  • 清空批处理列表:完成批量插入后,清空批处理列表,为下一批数据的存储和处理做好准备。

  • 循环处理


  • 只要还必要吸取数据,整个过程会不绝重复上述步骤,持续进行数据的吸取、存储、处理和插入操作,直到没有更多的数据必要处理。

DataRecordDisruptor参考代码 如下
  1. import com.lmax.disruptor.BlockingWaitStrategy;
  2. import com.lmax.disruptor.EventFactory;
  3. import com.lmax.disruptor.EventHandler;
  4. import com.lmax.disruptor.RingBuffer;
  5. import com.lmax.disruptor.dsl.Disruptor;
  6. import com.lmax.disruptor.dsl.ProducerType;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.stereotype.Component;
  9. import java.util.ArrayList;
  10. import java.util.List;
  11. import java.util.concurrent.Executors;
  12. import java.util.concurrent.ThreadFactory;
  13. @Component
  14. public class DataRecordDisruptor {
  15.     // Disruptor 环形缓冲区的大小,可根据需要调整
  16.     private static final int BUFFER_SIZE = 1024 * 1024;
  17.     // 批量插入的大小,可根据性能测试调整
  18.     private static final int BATCH_SIZE = 1000;
  19.     private final Disruptor<DataRecordEvent> disruptor;
  20.     private final RingBuffer<DataRecordEvent> ringBuffer;
  21.     @Autowired
  22.     private DataRecordMapper dataRecordMapper;
  23.     public DataRecordDisruptor() {
  24.         // 事件工厂,用于创建 DataRecordEvent 实例
  25.         EventFactory<DataRecordEvent> factory = DataRecordEvent::new;
  26.         // 创建线程工厂,使用默认的线程创建机制
  27.         ThreadFactory threadFactory = Executors.defaultThreadFactory();
  28.         // 初始化 Disruptor,使用多生产者模式和阻塞等待策略
  29.         disruptor = new Disruptor<>(factory, BUFFER_SIZE, threadFactory, ProducerType.MULTI, new BlockingWaitStrategy());
  30.         // 为 Disruptor 注册事件处理器
  31.         disruptor.handleEventsWith(new DataRecordEventHandler());
  32.         // 启动 Disruptor
  33.         ringBuffer = disruptor.start();
  34.     }
  35.     // 发布数据到 Disruptor 的方法
  36.     public void publish(List<DataRecord> dataRecords) {
  37.         // 获取环形缓冲区的可用序列范围
  38.         long sequence = ringBuffer.next(dataRecords.size());
  39.         try {
  40.             for (int i = 0; i < dataRecords.size(); i++) {
  41.                 // 将数据存储到环形缓冲区的事件中
  42.                 DataRecordEvent event = ringBuffer.get(sequence + i);
  43.                 event.setDataRecord(dataRecords.get(i));
  44.             }
  45.         } finally {
  46.             // 发布事件
  47.             ringBuffer.publish(sequence, sequence + dataRecords.size() - 1);
  48.         }
  49.     }
  50.     // 内部类,作为 Disruptor 的事件对象
  51.     private static class DataRecordEvent {
  52.         private DataRecord dataRecord;
  53.         public DataRecord getDataRecord() {
  54.             return dataRecord;
  55.         }
  56.         public void setDataRecord(DataRecord dataRecord) {
  57.             this.dataRecord = dataRecord;
  58.         }
  59.     }
  60.     // 事件处理器,负责将数据批量插入数据库
  61.     private class DataRecordEventHandler implements EventHandler<DataRecordEvent> {
  62.         private final List<DataRecord> batch = new ArrayList<>();
  63.         @Override
  64.         public void onEvent(DataRecordEvent event, long sequence, boolean endOfBatch) {
  65.             // 将事件中的数据添加到批处理列表中
  66.             batch.add(event.getDataRecord());
  67.             // 当达到批处理大小或处理完一批数据时进行插入操作
  68.             if (batch.size() >= BATCH_SIZE || endOfBatch) {
  69.                 insertBatch(batch);
  70.                 batch.clear();
  71.             }
  72.             
  73.             // 尼恩提示: 这里需要改造一下,加上一个结束的空事件
  74.         }
  75.         // 执行批量插入的方法
  76.         private void insertBatch(List<DataRecord> dataRecords) {
  77.             try {
  78.                 // 使用 MyBatis-Plus 的批量插入功能
  79.                 dataRecordMapper.insertBatch(dataRecords);
  80.             } catch (Exception e) {
  81.                 // 异常处理,可添加日志记录等操作
  82.                 e.printStackTrace();
  83.             }
  84.         }
  85.     }
  86. }
复制代码
7. 高并发 batch批量写入 的 Mapper 接口

数据写入模块包括:


  • DataRecordMapper 接口:
    自界说 insertBatch(List<DataRecord> dataRecords) 方法,在 XML 映射文件中实现批量插入逻辑。
  • DataRecordMapper.xml:
    使用 MyBatis-Plus 的 <foreach> 标签实现批量插入。
创建一个 MyBatis-Plus 的 Mapper 接口:
  1. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  2. import com.example.demo.entity.DataRecord;
  3. @Mapper
  4. public interface DataRecordMapper extends BaseMapper<DataRecord> {
  5.     // 自定义的批量插入方法
  6.     void insertBatch(List<DataRecord> dataRecords);
  7. }
复制代码
8. 实现 MyBatis-Plus 批量插入(XML)

在 resources/mapper/DataRecordMapper.xml 中添加以下代码:
  1. <!DOCTYPE mapper
  2.         PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  3.         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  4. <mapper namespace="com.example.demo.mapper.DataRecordMapper">
  5.     <insert id="insertBatch">
  6.         insert into data_record (column1, column2, column3) values
  7.         <foreach collection="list" item="item" separator=",">
  8.             (#{item.column1}, #{item.column2}, #{item.column3})
  9.         </foreach>
  10.     </insert>
  11. </mapper>
复制代码
也可以使用手动提交事务 + preparestatement,进行批量插入。
具体的实现代码,这里忽略。
后面介绍尼恩Java面试宝典配套视频的时间,会共同视频进行介绍。
9. 配置文件

在 application.properties 中配置数据库毗连:
  1. spring.datasource.url=jdbc:mysql://localhost:3306/your_database_name?useSSL=false&serverTimezone=UTC
  2. spring.datasource.username=root
  3. spring.datasource.password=your_password
  4. spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
复制代码
百万级数据量的高速导入的总结

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


  • 这个示例实现了一个完备的数据导入架构,使用 EasyExcel 进行高性能分片读取,避免了内存溢出题目。
  • 使用 Disruptor 作为高并发队列缓冲,将数据存储在环形缓冲区中,方便进行后续的业务处理。
  • 使用 MyBatis-Plus 的批量插入功能,并结合多线程处理,实现了高并发的批量数据写入。
通过以上架构和代码,你可以实现百万级数据量的快速导入,利用各组件的优势进步系统的性能和可扩展性。
性能可以由原来的500秒优化到20秒!
说在最后:有题目找老架构取经‍

回到开始的时间的面试题:招商银行的Java后端面试真题
被狠狠拷打了,问的人都懵了。 项目场景题太难了,不好好准备,真的答不出!

按照此文的套路去回答,肯定会 吊打面试官,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。
很多小伙伴刷完后, 吊打面试官, 大厂横着走。
在刷题过程中,假如有啥题目,大家可以来 找 40岁老架构师尼恩交流。
另外,假如没有面试机会,可以找尼恩来改简历、做帮扶。前段时间,刚指导一个小伙 暴涨200%(2倍),29岁/7年/双非一本 , 从13K 涨到 37K ,逆天改命。
狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在非常严寒/痛楚被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。
尼恩技能圣经系列PDF



  • 《NIO圣经:一次穿透NIO、Selector、Epoll底层原理》
  • 《Docker圣经:大白话说Docker底层原理,6W字实现Docker自由》
  • 《K8S学习圣经:大白话说K8S底层原理,14W字实现K8S自由》
  • 《SpringCloud Alibaba 学习圣经,10万字实现SpringCloud 自由》
  • 《大数据HBase学习圣经:一本书实现HBase学习自由》
  • 《大数据Flink学习圣经:一本书实现大数据Flink自由》
  • 《响应式圣经:10W字,实现Spring响应式编程自由》
  • 《Go学习圣经:Go语言实现高并发CRUD业务开发》
……完备版尼恩技能圣经PDF集群,请找尼恩领取
   《尼恩 架构条记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技能自由圈】取↓↓↓

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

河曲智叟

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表