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条:

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

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

相关推荐
LuckyLay2 分钟前
Spring学习笔记_27——@EnableLoadTimeWeaving
java·spring boot·spring
向阳121815 分钟前
Dubbo负载均衡
java·运维·负载均衡·dubbo
Gu Gu Study24 分钟前
【用Java学习数据结构系列】泛型上界与通配符上界
java·开发语言
WaaTong1 小时前
《重学Java设计模式》之 原型模式
java·设计模式·原型模式
m0_743048441 小时前
初识Java EE和Spring Boot
java·java-ee
AskHarries1 小时前
Java字节码增强库ByteBuddy
java·后端
佳佳_1 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
小灰灰__1 小时前
IDEA加载通义灵码插件及使用指南
java·ide·intellij-idea
夜雨翦春韭1 小时前
Java中的动态代理
java·开发语言·aop·动态代理
程序媛小果2 小时前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot