本文原文链接
尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,最近有小伙伴面试招商银行,遇到下面的绝命 12题,狠狠被拷打了, 彻底懵了。 项目场景题太难了,不好好准备,真的答不出!
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书
招商银行的Java后端面试真题
被狠狠拷打了,问的人都懵了。 项目场景题太难了,不好好准备,真的答不出!
尼恩将给出上面 招商银行绝命12 题的 全部答案:
1.如何让系统抗住双十一的预约抢购活动?
2.如何从零搭建10万级QPS大流量、高并发优惠券系统?
3.百万级别数据的 Excel 如何快速导入到数据
就是本文。
4.如何设计一个支持万亿GB网盘实现秒传与限速的系统?
即将发布。
5.如何根据应用场景选择合适的消息中间件?
即将发布。
6.如何提升 RocketMQ 顺序消费性能?
即将发布。
7.使用分布式调度框架该考虑哪些问题?
即将发布。
9.如何让系统抗住双十一的预约抢购活动?
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
方法获取工作表,再使用嵌套的循环遍历行和单元格。
根据单元格的数据类型(如字符串、数字等),使用不同的方法获取单元格的值并打印出来。
尼恩给大家画了一下,这个程序的流程图:
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
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 文件的示例
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的分批插入,并且结合采用多线程处理。
交互图:数据导入、队列缓冲和 写入模块 三者之间的交互图
以下是一个完整的交互图,展示了上述架构方案的交互数据流,包含数据导入模块、高并发队列缓冲和数据写入模块。
百万级数据量的高速导入的代码实现
以下是一个完整的实现上述架构方案的示例代码,包含数据导入模块、高并发队列缓冲和数据写入模块。
使用 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
的处理流程的简单介绍:
- 接收文件输入流:
DataImportService
首先会接收一个文件的输入流,这个输入流是要导入的数据的来源,通常可以是用户上传的 Excel 文件或其他数据源。
- 创建 EasyExcel 监听器:
为 EasyExcel 创建一个名为 DataRecordExcelListener
的监听器。
这个监听器的作用是在 EasyExcel 读取数据的过程中处理数据的读取事件。
- 开始使用 EasyExcel 读取文件:
调用 EasyExcel 的 read
方法开始读取文件,使用创建的 DataRecordExcelListener
来监听数据读取的过程。
- 数据读取和批处理:
在读取过程中,会逐行读取文件中的数据。
每读取一行数据,将其添加到一个批处理列表中。
当批处理列表中的数据量达到 10000 条时:
- 将该批处理列表的数据发布到
DataRecordDisruptor
中,以便后续处理。 - 清空批处理列表,为存储下一批数据做好准备。
- 处理剩余数据:
- 当文件中没有更多的数据需要读取时,会检查批处理列表是否还有未处理的数据。
- 如果批处理列表不为空(即还有未达到 10000 条的数据),将其发布到 DataRecordDisruptor` 中。
总体而言,DataImportService
利用 EasyExcel 逐行读取文件数据,将数据按批处理列表存储,达到一定数量后将数据发送到 DataRecordDisruptor
进行后续的处理。
这个过程通过批处理和使用 DataRecordDisruptor
实现了高性能的分片读取和数据缓冲,避免了大量数据读取时可能出现的内存溢出问题,并提高了数据处理的性能和效率。
高性能分片读取 数据 服务类 DataImportService
流程图如下
高性能分片读取 数据 服务类 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 的批量插入功能,为处理大量数据提供了一种高效的机制, 核心的流程如下:
- 初始化和启动阶段:
首先,初始化 Disruptor:首先会设置 Disruptor 的环形缓冲区大小、事件工厂、线程工厂、生产者类型和等待策略。
环形缓冲区大小决定了可以存储多少数据事件,这里设置的大小可根据实际需求调整。
事件工厂用于创建 DataRecordEvent
对象,线程工厂负责创建处理数据的线程,生产者类型设置为多生产者模式,以支持多个来源的数据,等待策略则是 BlockingWaitStrategy
,它会在缓冲区满时阻塞生产者,防止数据丢失。
然后,启动 Disruptor:完成上述设置后,启动 Disruptor,使其处于可接收数据的状态。
- 数据接收和存储阶段:
等待接收数据:启动后,DataRecordDisruptor
处于等待接收数据的状态,它将接收来自 DataImportService
的数据。
存储数据到环形缓冲区:当接收到来自 DataImportService
的数据时,将这些数据存储在环形缓冲区中。环形缓冲区是 Disruptor 的核心组件,它提供了高效的数据存储和访问机制。
- 异步数据处理阶段:
- 数据处理事件触发:当数据存储到环形缓冲区时,会触发相应的数据处理事件。
- 添加数据到批处理列表:将触发的数据添加到一个批处理列表中。这个批处理列表用于临时存储数据,方便后续的批量操作。
- 判断是否达到批处理条件:检查批处理列表的大小是否达到或超过 1000 条数据,或者是否处理完一批数据。这个批处理大小是为了优化数据库操作,减少数据库交互次数。
- 批量插入操作:如果满足上述条件,使用 MyBatis-Plus 进行批量插入操作,将数据存储到数据库中。
- 清空批处理列表:完成批量插入后,清空批处理列表,为下一批数据的存储和处理做好准备。
- 循环处理:
- 只要还需要接收数据,整个过程会不断重复上述步骤,持续进行数据的接收、存储、处理和插入操作,直到没有更多的数据需要处理。
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后端面试真题
被狠狠拷打了,问的人都懵了。 项目场景题太难了,不好好准备,真的答不出!
按照此文的套路去回答,一定会 吊打面试官,让面试官爱到 “不能自已、口水直流”,然后实现”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,请到下面公号【技术自由圈】取↓↓↓