在上篇文章中,针对项目中最基本的大数据量的导入和导出最基本的使用进行了讲解,但是实际项目中的导入,可能不止是单纯的读取excel文件数据,然后直接插入到数据库中。在实际项目中,更多的还是读取到数据之后,需要进行一系列的判断,如:数据是否重复,如果在导入的时候,每条数据都要去查询一下是否存在,想象一下,100w条数据,每次都调一次数据库,这个时间得要多长。那么怎么解决呢?以下提供解决方案。
一、项目使用框架
代码见:gitee.com/blog-materi... 的 easyExcel部分
easyexcel官网:easyexcel.opensource.alibaba.com
由于其他的项目依赖跟上篇文章的都一样,这里就不做赘述了,详见:大数据量导入导出解决方案-EasyExcel
这里就只对导入时判断数据重复的问题,做一下讲解,主要变动文件就是EasyExcelStudentImportHandler类。
其他代码和前端代码可以查看源代码
版本一:每条都查询数据库,判断是否重复
EasyExcelStudentImportHandler 类:
java
@Slf4j
@Service
@Scope("prototype")
public class EasyExcelStudentImportHandler implements ReadListener<EasyExcelImportExcel> {
/** 成功数据集合 */
private final CopyOnWriteArrayList<EasyExcelImportExcel> successList = new CopyOnWriteArrayList<>();
/** 失败数据集合 */
private final CopyOnWriteArrayList<EasyExcelImportExcel> failList = new CopyOnWriteArrayList<>();
/** 批处理条数 */
private final static int BATCH_COUNT = 20000;
private Set<String> studentNumSet = new HashSet<>();
@Resource
private ThreadPoolExecutor easyExcelStudentImportThreadPool;
@Resource
private StudentMapper studentMapper;
/**
* 读取表格内容,每一条数据解析都会来调用
* @author LP to 2024/4/7
*/
@Override
public void invoke(EasyExcelImportExcel importExcel, AnalysisContext analysisContext) {
// 参数校验
if (StringUtils.isBlank(importExcel.getName())) {
importExcel.setErrorMsg("学生名称不能为空");
failList.add(importExcel);
return;
}
if (StringUtils.isBlank(importExcel.getStudentNum())) {
importExcel.setErrorMsg("学号不能为空");
failList.add(importExcel);
return;
}
// 这里用学号作为唯一值,用于判断文件里的数据是否有重复
if (studentNumSet.contains(importExcel.getStudentNum())) {
importExcel.setErrorMsg("学号重复(文件)");
failList.add(importExcel);
return;
}
studentNumSet.add(importExcel.getStudentNum());
// 版本①:每条数据进行数据库查询
if (studentMapper.selectCount(Wrappers.<Student>lambdaQuery().eq(Student::getStudentNum, importExcel.getStudentNum())) > 0) {
importExcel.setErrorMsg("学号重复(数据库)");
failList.add(importExcel);
return;
}
successList.add(importExcel);
if (successList.size() >= BATCH_COUNT) {
saveDate();
}
}
/**
* 在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。
* @author LP to 2024/4/7
*/
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
// 导入异常,实际业务可以做一些异常处理。比如记录失败数据等
}
/**
* 所有数据解析完成了调用
* @author LP to 2024/4/7
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
saveDate();
}
/**
* 保存信息
* @author Liupeng to 2024/4/2
*/
private void saveDate() {
// 拆分list,每个list 2000 条数据
List<List<EasyExcelImportExcel>> lists = ListUtil.split(successList, 2000);
final CountDownLatch countDownLatch = new CountDownLatch(lists.size());
for (List<EasyExcelImportExcel> list : lists) {
easyExcelStudentImportThreadPool.execute(() -> {
try {
studentMapper.insertBatch(list.stream().map(o -> {
Student student = new Student();
student.setNo(IdUtil.getSnowflakeNextId());
student.setName(o.getName());
student.setStudentNum(o.getStudentNum());
student.setAge(o.getAge());
student.setSex(o.getSex());
student.setBirthday(o.getBirthday());
return student;
}).collect(Collectors.toList()));
} catch (Exception e) {
log.error("启动线程失败,e:{}", e.getMessage(), e);
} finally {
countDownLatch.countDown();
}
});
}
// 等待所有线程执行完
try {
countDownLatch.await();
} catch (Exception e) {
log.error("等待所有线程执行完异常,e:{}", e.getMessage(), e);
}
// 提前将不再使用的集合清空,释放资源
successList.clear();
failList.clear();
lists.clear();
}
}
由于这种方式过于慢,这里只用了10w条数据做测试,只解析文件,用时/ms:
解析文件 + 插入数据库,用时/ms
版本二:批量处理
EasyExcelStudentImportHandler 类:
java
@Slf4j
@Service
@Scope("prototype")
public class EasyExcelStudentImportHandler implements ReadListener<EasyExcelImportExcel> {
/** 代校验数据集合 */
private final CopyOnWriteArrayList<EasyExcelImportExcel> checkList = new CopyOnWriteArrayList<>();
/** 成功数据集合 */
private final CopyOnWriteArrayList<EasyExcelImportExcel> successList = new CopyOnWriteArrayList<>();
/** 失败数据集合 */
private final CopyOnWriteArrayList<EasyExcelImportExcel> failList = new CopyOnWriteArrayList<>();
/** 批处理条数 */
private final static int BATCH_COUNT = 20000;
private Set<String> studentNumSet = new HashSet<>();
@Resource
private ThreadPoolExecutor easyExcelStudentImportThreadPool;
@Resource
private StudentMapper studentMapper;
/**
* 读取表格内容,每一条数据解析都会来调用
* @author LP to 2024/4/7
*/
@Override
public void invoke(EasyExcelImportExcel importExcel, AnalysisContext analysisContext) {
// 参数校验
if (StringUtils.isBlank(importExcel.getName())) {
importExcel.setErrorMsg("学生名称不能为空");
failList.add(importExcel);
return;
}
if (StringUtils.isBlank(importExcel.getStudentNum())) {
importExcel.setErrorMsg("学号不能为空");
failList.add(importExcel);
return;
}
// 这里用学号作为唯一值,用于判断文件里的数据是否有重复
if (studentNumSet.contains(importExcel.getStudentNum())) {
importExcel.setErrorMsg("学号重复(文件)");
failList.add(importExcel);
return;
}
studentNumSet.add(importExcel.getStudentNum());
// 版本①:每条数据进行数据库查询
// if (studentMapper.selectCount(Wrappers.<Student>lambdaQuery().eq(Student::getStudentNum, importExcel.getStudentNum())) > 0) {
// importExcel.setErrorMsg("学号重复(数据库)");
// failList.add(importExcel);
// return;
// }
checkList.add(importExcel);
if (checkList.size() >= BATCH_COUNT) {
checkData();
saveDate();
}
}
/**
* 在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。
* @author LP to 2024/4/7
*/
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
// 导入异常,实际业务可以做一些异常处理。比如记录失败数据等
}
/**
* 所有数据解析完成了调用
* @author LP to 2024/4/7
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
saveDate();
}
/**
* 版本②:校验数据
* @author Liupeng to 2024/9/30
*/
private void checkData() {
// 拆分list,每个list 2000 条数据
List<List<EasyExcelImportExcel>> lists = ListUtil.split(checkList, 2000);
final CountDownLatch countDownLatch = new CountDownLatch(lists.size());
for (List<EasyExcelImportExcel> list : lists) {
easyExcelStudentImportThreadPool.execute(() -> {
try {
// 判断学号是否重复,批量查询学号
Map<String, EasyExcelImportExcel> studentNumMap = list.stream().collect(Collectors.toMap(EasyExcelImportExcel::getStudentNum, o -> o, (t1, t2) -> t2));
List<Student> studentList = studentMapper.selectList(Wrappers.<Student>lambdaQuery().in(Student::getStudentNum, studentNumMap.keySet()));
if (null == studentList) {
// 为空,表示没有重复
successList.addAll(list);
} else {
// 不为空,则需要遍历比较哪些值重复,将重复值从map删除,剩下map里的值就是不重复的
studentList.forEach(o -> {
EasyExcelImportExcel importExcel = studentNumMap.get(o.getStudentNum());
importExcel.setErrorMsg("学号重复(数据库)");
failList.add(importExcel);
studentNumMap.remove(o.getStudentNum());
});
successList.addAll(studentNumMap.values().stream().toList());
}
} catch (Exception e) {
log.error("数据校验失败,e:{}", e.getMessage(), e);
} finally {
countDownLatch.countDown();
}
});
}
// 等待所有线程执行完
try {
countDownLatch.await();
} catch (Exception e) {
log.error("等待所有线程执行完异常,e:{}", e.getMessage(), e);
}
// 提前将不再使用的集合清空,释放资源
checkList.clear();
lists.clear();
}
/**
* 保存信息
* @author Liupeng to 2024/4/2
*/
private void saveDate() {
// 拆分list,每个list 2000 条数据
List<List<EasyExcelImportExcel>> lists = ListUtil.split(successList, 2000);
final CountDownLatch countDownLatch = new CountDownLatch(lists.size());
for (List<EasyExcelImportExcel> list : lists) {
easyExcelStudentImportThreadPool.execute(() -> {
try {
studentMapper.insertBatch(list.stream().map(o -> {
Student student = new Student();
student.setNo(IdUtil.getSnowflakeNextId());
student.setName(o.getName());
student.setStudentNum(o.getStudentNum());
student.setAge(o.getAge());
student.setSex(o.getSex());
student.setBirthday(o.getBirthday());
return student;
}).collect(Collectors.toList()));
} catch (Exception e) {
log.error("启动线程失败,e:{}", e.getMessage(), e);
} finally {
countDownLatch.countDown();
}
});
}
// 等待所有线程执行完
try {
countDownLatch.await();
} catch (Exception e) {
log.error("等待所有线程执行完异常,e:{}", e.getMessage(), e);
}
// 提前将不再使用的集合清空,释放资源
successList.clear();
lists.clear();
}
}
导入10w条数据,只解析文件,用时/ms:
解析文件 + 插入数据库,用时/ms:
数据库中原本有100w条数据,插入成功10w条:
导入100w条数据,只解析文件,用时/ms:
解析文件 + 插入数据库,用时/ms:
数据库中原本有100w条数据,插入成功100w条:
可以看到,以批量的方式来处理这种问题,速度是有质的提升的。我这里举得例子,还可以适用于其他的逻辑判断,如:导入的时候有学生的老师归属,需要判断老师是否存在,也可以使用批量的方式来处理。
以上就是大数据量导入情况下,需要判断数据问题的解决方案。