导入
引入easyexcel
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
总结有三种导出方式:
-
单行读取,单行导入。这种方案最简单,最慢。
-
分页读取,分页导入。这种方案利用MySql批量插入可优化到二三十秒。
-
多线程分页读取,并导入。这种方案利用MySql innerDB引擎支持并发插入的特点可以进一步优化到10几秒。
数据准备
表格:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gwRABG0u-1693143285363)
表:
CREATE TABLE `test_import_export` (
`seckill_id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_id` varchar(51) NOT NULL COMMENT '订单id',
`user_id` int(20) NOT NULL,
`goods_id` int(11) NOT NULL,
PRIMARY KEY (`seckill_id`),
UNIQUE KEY `uidx_so_oid` (`order_id`) USING BTREE,
UNIQUE KEY `uidx_so_id` (`seckill_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
单行读取,单行导入
这种方式不做过多讲解,太慢了
//单行读取
@Test
public void importFile() throws InterruptedException, ExecutionException, FileNotFoundException {
String fileName = "D:\\repeatedWrite1693111137007.xlsx";
long start = System.currentTimeMillis();
//单行解析
// 这里默认每次会读取1条数据 然后返回过来 直接调用使用数据就行
// 具体需要返回多少行可以在`PageReadListener`的构造函数设置
List<ReadSheet> sheets = EasyExcelUtils.listSheet(new FileInputStream(fileName));
for (int i = 0; i < sheets.size(); i++) {
int finalI = i;
EasyExcel.read(fileName, TestImportExport.class, new PageReadListener<TestImportExport>(dataList -> {
log.info("第{}页,读取到{}条数据", dataList.size(), finalI);
testImportExportMapper.insert(dataList.get(0));
}, 1)).excelType(ExcelTypeEnum.XLSX).sheet(finalI).doRead();
}
log.info("消耗:{}", (System.currentTimeMillis() - start) / 1000);
}
分页读取,分页导入
//分页读取 --22s
@Test
public void importFile1() throws InterruptedException, ExecutionException, FileNotFoundException {
String fileName = "D:\\repeatedWrite1693111137007.xlsx";
long start = System.currentTimeMillis();
ExecutorService executorService = Executors.newFixedThreadPool(17);
List<Callable<String>> callables = new ArrayList<>();
//单行解析
// 这里默认每次会读取10000条数据 然后返回过来 直接调用使用数据就行
// 具体需要返回多少行可以在`PageReadListener`的构造函数设置
List<ReadSheet> sheets = EasyExcelUtils.listSheet(new FileInputStream(fileName));
for (int i = 0; i < sheets.size(); i++) {
int finalI = i;
EasyExcel.read(fileName, TestImportExport.class, new PageReadListener<TestImportExport>(dataList -> {
log.info("第{}页,读取到{}条数据", dataList.size(), finalI);
testImportExportMapper.insertBatch(dataList);
}, 10000)).excelType(ExcelTypeEnum.XLSX).sheet(finalI).doRead();
}
log.info("消耗:{}", (System.currentTimeMillis() - start) / 1000);
}
这种方式利用easyexcel提供的PageReadListener来监听读取到的excel数据,每次读取10000条,并插入数据库。我这里的excel每个sheet有50000条数据,所以每个sheet会分5页。通过EasyExcelUtils.listSheet
获取到sheets的数量,接下来就循环去读取每个sheet的数据。
这里测试100万数据花了20秒。excel的格式如下
多线程分页读取,并导入
//多线程分页读取 --10s
@Test
public void importFile2() throws InterruptedException, ExecutionException, FileNotFoundException {
String fileName = "D:\\repeatedWrite1693111137007.xlsx";
long start = System.currentTimeMillis();
ExecutorService executorService = Executors.newFixedThreadPool(17);
List<Callable<String>> callables = new ArrayList<>();
//单行解析
// 这里默认每次会读取10000条数据 然后返回过来 直接调用使用数据就行
// 具体需要返回多少行可以在`PageReadListener`的构造函数设置
List<ReadSheet> sheets = EasyExcelUtils.listSheet(new FileInputStream(fileName));
for (int i = 0; i < sheets.size(); i++) {
int finalI = i;
//代码1
Callable callable = (Callable<String>) () -> {
EasyExcel.read(fileName, TestImportExport.class, new PageReadListener<TestImportExport>(dataList -> {
log.info("读取到{}条数据, 第{}页", dataList.size(), finalI);
testImportExportMapper.insertBatch(dataList);
}, 10000)).excelType(ExcelTypeEnum.XLSX).sheet(finalI).doRead();
return "OK";
};
callables.add(callable);
}
//代码2
List<Future<String>> futures = executorService.invokeAll(callables);
for (Future<String> future : futures) {
String s = future.get();
System.out.println(s);
}
log.info("消耗:{}", (System.currentTimeMillis() - start) / 1000);
}
代码1处在上一个方法的基础上将读取和插入数据库的操作放在Callable中,在代码2处统一用线程处理,这里定义的线程数是cpu+1个线程数(CPU密集型)
测试结果为10几秒时间
导出
导出主要在于优化查询语句,一次性查询全部肯定行不通,需要分页查询。
给出两种优化语句的方式:
- 第一种:利用上次查询到的最大主键id,先过滤掉小于最大主键id的数据,然后取最前面的pageSize条。这种适合连续的分页查询,不适合跳页。速度测试为ms级别
SELECT seckill_id, order_id, user_id, goods_id FROM test_import_export
WHERE
seckill_id > #{idMax}
limit #{pageSize}
-
第二种:先查询出需要查询的最开始那条的id,然后过滤掉小于该id的数据,取最前面的pageSize条。这种适合分页查以及加条件都可以,但是查询较慢。测试速度为s级别。
select seckill_id, order_id, user_id, goods_id from test_import_export where seckill_id >= ( select seckill_id from test_import_export limit #{start},1) limit #{pageSize};
分页查询,单线程读,单线程导出
@Test
public void export1() throws FileNotFoundException {
long start = System.currentTimeMillis();
long max = 0;
int pageSize = 50000;
Long count = testImportExportMapper.selectCount(null);
long totalPageNum = count % pageSize == 0 ? count/pageSize : count/pageSize + 1;
String fileName = "D:\\repeatedWrite" + System.currentTimeMillis() + ".xlsx";
try (ExcelWriter excelWriter = EasyExcel.write(fileName, TestImportExport.class).build()) {
for (int i = 1; i <= totalPageNum; i++) {
log.info("正在读第{}页", i);
List<TestImportExport> data = testImportExportMapper.findPage(max, pageSize);
Optional<TestImportExport> max1 = data.stream().max(TestImportExportServiceImplTest2::compare);
max = Math.max(max1.get().getSeckillId(), max);
//导出
WriteSheet writeSheet = EasyExcel.writerSheet(i, "页" + i).build();
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
excelWriter.write(data, writeSheet);
}
}
System.out.println("消耗:" + (System.currentTimeMillis() - start) / 1000);
}
这里分页查,每页都导出到一个sheet中,有多少页导出到多少个sheet。使用第一种优化分页sql语句方案。
分页查询,多线程读,单线程导出
@Test
public void export2() throws FileNotFoundException, InterruptedException, ExecutionException {
long start = System.currentTimeMillis();
ExecutorService executorService = Executors.newFixedThreadPool(16 + 1);
int pageSize = 50000;
List<Callable<String>> callableList = new ArrayList<>();
long count = testImportExportMapper.selectCount(null);
//代码1
long totalPageNum = count % pageSize == 0 ? count/pageSize : count/pageSize + 1;
Map<Integer, List<TestImportExport>> dataMap = new ConcurrentHashMap<>();
for (int i = 1; i <= totalPageNum; i++) {
int finalI = i;
Callable<String> callable = () -> {
log.info("正在读第{}页", finalI);
long start1 = (finalI - 1) * pageSize;
List<TestImportExport> data = testImportExportMapper.findPage2(start1, pageSize);
//代码2
dataMap.put(finalI, data);
return "OK";
};
callableList.add(callable);
}
List<Future<String>> futures = executorService.invokeAll(callableList);
for (Future<String> future : futures) {
log.info(future.get());
}
System.out.println("准备数据消耗:" + (System.currentTimeMillis() - start) / 1000);
String fileName = "D:\\repeatedWriteMiltyThread" + System.currentTimeMillis() + ".xlsx";
//代码3
try (ExcelWriter excelWriter = EasyExcel.write(fileName, TestImportExport.class).build()) {
for (Integer pageNum : dataMap.keySet()) {
//导出
WriteSheet writeSheet = EasyExcel.writerSheet(pageNum, "页" + pageNum).build();
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
excelWriter.write(dataMap.get(pageNum), writeSheet);
}
}
System.out.println("消耗:" + (System.currentTimeMillis() - start) / 1000);
}
代码1处先求出最大页数,然后一页定义一个线程去查询数据,最终汇总在dataMap中(如代码2处,key为页数,val为当页数据)。最终代码3处 将dataMap的key作为sheet的no,val为该sheet填充的数据,将dataMap写入到excel。