项目场景:
新项目需要对接老系统的数据,而客户给我们的是几份Excel文件,每份文件中大概有3000多条数据,而在我们最新设计的系统当中,是需要分开去存储这些东西的,这就导致了在三千条数据中可能要涉及到四到五张表的保存以及校验
准备过程
一开始是没有考虑太多就直接去写的,首先引入easypoi
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
其次定义我们要读取Excel的实体类,这里我去了解了一下easypoi的相关文档,代码直接贴在下面了,@Excel注解
@Data
public class DeviceImportDto {
@Excel(name = "设备编号",replace = "null_''")
private String deviceNo;
@Excel(name = "设备类型",replace = "null_''")
private String productType;
@Excel(name = "设备型号",replace = "null_''")
private String productModel;
@Excel(name = "设备厂家",replace = "null_''")
private String productManufacturer;
@Excel(name = "检测类型",replace = "null_''")
private String propertyName;
@Excel(name = "传感器品牌",replace = "null_''")
private String productName;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DeviceImportDto that = (DeviceImportDto) o;
return Objects.equals(deviceNo, that.deviceNo) && Objects.equals(productType, that.productType) && Objects.equals(productModel, that.productModel) && Objects.equals(productManufacturer, that.productManufacturer) && Objects.equals(propertyName, that.propertyName) && Objects.equals(productName, that.productName);
}
@Override
public int hashCode() {
return Objects.hash(deviceNo, productType, productModel, productManufacturer, propertyName, productName);
}
}
准备结束之后直接就写业务代码了呗,easypoi会直接帮我们读取Excel内的数据,我们只需要定义接口以及传参就可以了,具体可以去easypoi官网了解一下。我们在controller直接定义接收参数为@RequestPart("file") MultipartFile file
就可以了。
遇到的问题
因为一开始是直接写的,没有分析性能啊、数据库啊等会不会出问题。等到代码写完了一运行,就开始出现了一堆问题:
- 接口响应时间过长,导致超时
- 使用easypoi一次性读取了3000条数据,占用了大概1秒时间,直接遍历读出来的数组,并分别存入不同的实体类集合中最后进行批量保存,速度过慢,影响主线程,也有数据库崩掉的危险,在遍历数组中我需要进行数据校验,符合的存入集合中不符合的踢掉,而校验我当时写的是在遍历中去查数据库是否存在,这就导致如果3000条数据我可能要读12000次数据库,因为数据中涉及到了不同的分类,我每一种分类都需要去校验。这就导致数据库直接挂掉
解决方案:
针对上面的问题一个一个的解决
1. 针对接口响应时间过长的问题进行解决.
涉及到大文件的上传处理、导入导出等一般都会使用异步,所以我们这里也是直接使用异步,只需要在业务代码中打印相应的结果就行了,不需要考虑给用户返回,所以也没有设计相关的表。首先配置线程池,不选择用Spring默认的配置
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "test1")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(4);
// 最大线程数
executor.setMaxPoolSize(8);
// 队列大小
executor.setQueueCapacity(Integer.MAX_VALUE);
// 线程活跃时间
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("testname1-");
executor.setThreadGroupName("testgroup1");
// 所有任务结束后关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.initialize();
return executor;
}
}
配置好线程池之后我们在service层上面直接加@Async("test1")
指定我们要使用的线程池
之后重新启动项目,调用接口,很快的返回了200,在我预料之中应该马上就会启动线程,执行我的导入操作,但是失败了,抛出的异常我也不记得了,但是原因是因为在springboot接收了MultipartFile
之后会生成一个临时文件,用来对他进行操作,操作完之后会进行清理。然而因为我们使用的异步,导致springboot认为我们已经处理完了,这时候已经把临时文件删除了,然后我们的线程自然是找不到上传的Excel文件了,对此我的解决方法是在service层接收inputStream即可,因为我的异步注解是在service层加的,controller里面多加了一层。
public GenericResponse importDevice(@RequestPart("file") MultipartFile file) {
try {
if (file != null && !file.isEmpty()) {
uploadService.importExcel(file.getInputStream());
}
}catch (Exception e) {
throw new GenericException(e.getMessage());
}
return GenericResponse.success();
}
这样在主线程给用户返回结果之前我就已经把数据流传递到service层了,后续异步再进行操作也不需要去找临时文件了,这样异步的问题就解决啦
2. 处理数据过多,业务处理复杂导致速度过慢
其实这个问题很好解决,也是我自己本身的项目经验不足,我们只需要对读取的集合进行分组处理即可,因为前面已经用异步处理了接口,这也说明我们已经不需要担心速度的问题了,因为用户那边已经有了反馈,所以我就对文件的读取没有分批处理,而是在文件读取之后进行分批处理。我以100个为一组,来计算需要切分的次数,并使用Stream把分批之后的集合重新保存到一个集合中
private static final Integer MAX_NUMBER = 100;//按每一组一百个进行分割
// 计算切分的次数
private static Integer countStep(Integer size) {
return (size + MAX_NUMBER - 1) / MAX_NUMBER;
}
上面是计算切分次数以及定义每组数量的方法,下面是我实际使用中,将读取到的Excel集合分批次存入新的数组中,每一组100个,相当于我新的数组中有30组数据,这样100个100个的处理,数据库的压力也会小很多,速度也会快很多,不是特别占用资源
int limit = countStep(list.size());// 这里的list是Excel读取到的数组集合
List<List<DeviceImportDto>> splitList = new ArrayList<>();
Stream.iterate(0, n -> n + 1).limit(limit).forEach(i -> {
splitList.add(list.stream().skip((long) i * MAX_NUMBER).limit(MAX_NUMBER).collect(Collectors.toList()));
});
目前出现的问题都解决完了,主要的业务代码我就不放了,只是理一个思路。