EasyExcel 大数据量导入导出解决方案进阶

在上篇文章中,针对项目中最基本的大数据量的导入和导出最基本的使用进行了讲解,但是实际项目中的导入,可能不止是单纯的读取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条:

可以看到,以批量的方式来处理这种问题,速度是有质的提升的。我这里举得例子,还可以适用于其他的逻辑判断,如:导入的时候有学生的老师归属,需要判断老师是否存在,也可以使用批量的方式来处理。

以上就是大数据量导入情况下,需要判断数据问题的解决方案。

相关推荐
北极无雪20 分钟前
Spring源码学习(拓展篇):SpringMVC中的异常处理
java·开发语言·数据库·学习·spring·servlet
VXbishe27 分钟前
(附源码)基于springboot的“我来找房”微信小程序的设计与实现-计算机毕设 23157
java·python·微信小程序·node.js·c#·php·课程设计
YONG823_API1 小时前
电商平台数据批量获取自动抓取的实现方法分享(API)
java·大数据·开发语言·数据库·爬虫·网络爬虫
扬子鳄0081 小时前
java注解的处理器
java
Amagi.1 小时前
Spring中Bean的作用域
java·后端·spring
2402_857589361 小时前
Spring Boot新闻推荐系统设计与实现
java·spring boot·后端
繁依Fanyi1 小时前
旅游心动盲盒:开启个性化旅行新体验
java·服务器·python·算法·eclipse·tomcat·旅游
J老熊2 小时前
Spring Cloud Netflix Eureka 注册中心讲解和案例示范
java·后端·spring·spring cloud·面试·eureka·系统架构
蜜桃小阿雯2 小时前
JAVA开源项目 旅游管理系统 计算机毕业设计
java·开发语言·jvm·spring cloud·开源·intellij-idea·旅游
CoderJia程序员甲2 小时前
重学SpringBoot3-集成Redis(四)之Redisson
java·spring boot·redis·缓存