通过EasyExcel+线程池实现百万级数据从Excel导入到数据库

EasyExcel的优缺点

优点:

  1. 高效性:EasyExcel采用零反射、零注解的方式读写Excel文件,这使得它在处理大型Excel文件时具有出色的性能。此外,它采用了高效的解析算法,能够快速读取和解析文件内容。
  2. 低内存占用:与传统的Excel文件读取方式相比,EasyExcel显著降低了内存占用。它采用基于事件驱动的模型,通过回调函数来处理每一行数据,而不是一次性将整个文件读入内存。这种流式的处理方式极大地节省了内存资源,使得处理大文件时更加稳定可靠。
  3. 支持多种文件格式:EasyExcel支持多种Excel文件格式,包括.xls、.xlsx、.xlsm等,这使得它在处理不同版本的Excel文件时具有更大的灵活性。
  4. 易用性:EasyExcel提供了简单易用的API,使得开发者能够轻松地实现Excel文件的读写操作。它支持读写多种数据类型,如基本类型、集合、自定义对象等,满足了开发者在处理复杂数据时的需求。
  5. 错误处理与稳定性:在处理大文件时,数据的一致性和完整性至关重要。EasyExcel提供了良好的错误处理机制,能够在读取过程中有效地识别和处理异常情况,确保数据的准确性和稳定性。

缺点:

  1. 特殊格式和功能的限制:EasyExcel可能无法完全支持Excel文件中的所有特殊格式和功能。例如,它可能无法正确解析某些复杂的公式、图表或特殊的数据验证规则。这可能导致在读取某些特定格式的Excel文件时出现问题。

  2. 对于大文件的写入性能:虽然EasyExcel在读取大文件时表现优秀,但在写入大文件时可能会遇到性能瓶颈。尤其是在处理大量数据并需要频繁写入到Excel文件时,可能会导致写入速度变慢,甚至可能出现超时或内存问题。

  3. 并发处理能力有限:EasyExcel在处理高并发请求时可能存在一定的问题。当多个线程或进程同时尝试读取或写入同一个Excel文件时,可能会出现数据冲突或不一致的情况。尽管可以通过一些同步机制来避免这些问题,但这可能会增加系统的复杂性和开销。

  4. 依赖环境和版本兼容性:EasyExcel的性能和稳定性可能受到Java环境、操作系统版本或其他依赖库的影响。如果环境配置不当或存在版本不兼容的问题,可能会导致读取大文件时出现异常或错误。

  5. 错误处理和日志记录不足:在某些情况下,EasyExcel可能无法提供足够的错误处理和日志记录功能。当遇到复杂的数据结构或格式问题时,可能难以快速定位和解决问题。这可能会增加开发和维护的难度。

具体实现

Maven依赖

        <!--EasyExcel-->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>easyexcel</artifactId>
			<version>3.0.5</version>
		</dependency>

		<!-- 数据库连接和线程池 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.24</version>
		</dependency>

import com.alibaba.excel.EasyExcel;
import com.demo.importExcel.entity.User;
import com.demo.importExcel.listener.DataReadListener;
import com.demo.importExcel.mapper.ImportExcelMapper;
import com.demo.importExcel.service.IDataBaseService;
import com.demo.importExcel.service.IImportExcelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Service
public class ImportExcelServiceImpl implements IImportExcelService {

    @Autowired
    private IDataBaseService dataBaseService;

    /**
     * 导入Excel数据实现
     */
    @Override
    public void importExcel() {
        long startTime = System.currentTimeMillis();
        //Excel路径
        String path = "D://exportExcel.xlsx";
        //读取sheet的数量
        int numberSheet = 20;

        //创建一个固定大小的线程池,大小和sheet数量一样
        ExecutorService executor = Executors.newFixedThreadPool(numberSheet);

        //遍历所有sheet
        for (int i = 0; i < numberSheet; i++) {
            //lambda表达式中的变量必须是final的
            int sheetNumber = i;
            //向线程池提交任务
            executor.submit(()->{
               //使用EasyExcel获取相对于sheet数据
                EasyExcel.read(path, User.class,new DataReadListener(dataBaseService))
                        .sheet(sheetNumber)//sheet数
                        .doRead();//开始读取数据
            });
        }
        //线程池关闭
        executor.shutdown();

        //等待所以任务完成读取操作
        try {
            executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("导入时长:" + String.valueOf(endTime-startTime));
    }
}

实现并发读取多个sheet代码:

import com.alibaba.excel.EasyExcel;
import com.demo.importExcel.entity.User;
import com.demo.importExcel.listener.DataReadListener;
import com.demo.importExcel.mapper.ImportExcelMapper;
import com.demo.importExcel.service.IDataBaseService;
import com.demo.importExcel.service.IImportExcelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Service
public class ImportExcelServiceImpl implements IImportExcelService {

    @Autowired
    private IDataBaseService dataBaseService;

    /**
     * 导入Excel数据实现
     */
    @Override
    public void importExcel() {
        long startTime = System.currentTimeMillis();
        //Excel路径
        String path = "D://exportExcel.xlsx";
        //读取sheet的数量
        int numberSheet = 20;

        //创建一个固定大小的线程池,大小和sheet数量一样
        ExecutorService executor = Executors.newFixedThreadPool(numberSheet);

        //遍历所有sheet
        for (int i = 0; i < numberSheet; i++) {
            //lambda表达式中的变量必须是final的
            int sheetNumber = i;
            //向线程池提交任务
            executor.submit(()->{
               //使用EasyExcel获取相对于sheet数据
                EasyExcel.read(path, User.class,new DataReadListener(dataBaseService))
                        .sheet(sheetNumber)//sheet数
                        .doRead();//开始读取数据
            });
        }
        //线程池关闭
        executor.shutdown();

        //等待所以任务完成读取操作
        try {
            executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("导入时长:" + String.valueOf(endTime-startTime));
    }
}

先定义sheet的数量和固定大小的线程池数量,每个sheet页做为单独的任务交给线程池处理。定义了DataReadListener,这个类是ReadListener的实现类。当EasyExcel每读取一行数据都会调用invoke方法,在invoke()中可以做我们自己的逻辑处理

以下是DataReadListener


import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.demo.importExcel.entity.User;
import com.demo.importExcel.mapper.ImportExcelMapper;
import com.demo.importExcel.service.IDataBaseService;
import com.demo.importExcel.service.IImportExcelService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Import;

import java.util.ArrayList;
import java.util.List;

//自定义监听器,处理读取到的Excel数据
@Slf4j
public class DataReadListener implements ReadListener<User> {

    private IDataBaseService dataBaseService;

    /**
     * 每次批量插入数据的数量
     */
    private static final int batchSize = 1000;

    /**
     * 用于暂存数据的集合,直到数量等于batchSize时就会进行插入操作并清空集合
     */
    private List<User> batchList = new ArrayList();

    /**
     * 注入mapper
     * @param mapper
     */
    public DataReadListener(IDataBaseService dataBaseService) {
        this.dataBaseService = dataBaseService;
    }

    //EasyExcel每读取一行数据就会执行一次
    @Override
    public void invoke(User user, AnalysisContext analysisContext) {
        log.info("读取到的行数据:{}",user);
        if(validateData(user)){
            batchList.add(user);
        }else {
            //没有通过校验的数据,打印日志
            log.error("id为[{}]的数据没有通过校验",user.getId());
        }

        //如果集合数量大于设置的批量数量,那么就插入数据并清空集合
        if(batchList.size() >= batchSize){
            dataBaseService.batchInsert(batchList);
            batchList.clear();
        }
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        log.info("Excel读取完成!");
        //如果还有数据就一起插入到数据库中
        if(!batchList.isEmpty()){
            dataBaseService.batchInsert(batchList);
        }
    }


    /**
     * 数据校验
     * @param user
     * @return
     */
    private boolean validateData(User user){
        int userCount = dataBaseService.findUserById(user.getId());
        //判断是否存在数据库中
        if(userCount == 0){
            return true;
        }
        //处理其他逻辑校验........
        return false;
    }

}

通过自定义的DataReadListener,我们就可以在读取Excel的时候做处理。

每读取到一行数据会先做数据校验,如果校验通过后就会放到缓存集合中,List数量积累到1000时就会通过Mybatis的批量操作进行数据插入。doAfterAllAnalysed方法会在读取Excel完成后进行调用

MyBatis批量操作:

@Mapper
public interface ImportExcelMapper {

    /**
     * 批量导入数据
     * @param dataList
     */
    void batchInsertData(List<User> dataList);

    /**
     * 查询用户
     * @return
     */
    List<User> findAllUser();

    /**
     * 根据id查询用户
     * @param id
     * @return
     */
    int findUserById(String id);
}

ImportExcel.xml文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.demo.importExcel.mapper.ImportExcelMapper">
    <insert id="batchInsertData" parameterType="list">
        INSERT INTO t_user (id, name,create_time)
        VALUES
        <foreach collection="list" item="user" separator=",">
            (#{user.id}, #{user.name},#{user.createTime})
        </foreach>
    </insert>

    <select id="findAllUser" resultType="com.demo.importExcel.entity.User">
        select * from t_user
    </select>

    <select id="findUserById" resultType="int">
        select count(id) from t_user where id=#{id}
    </select>
</mapper>

源码地址:https://github.com/Roaly/importExcel

在CSDN上,一键三连是对作者辛勤创作的最好鼓励!喜欢我的文章,就请点赞、收藏、转发吧!你们的支持是我持续分享知识的动力,感谢大家的陪伴与认可!💖🔝🔄

  • 69
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值