厉害了!12秒将百万数据通过EasyExcel导入MySQL数据库中

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介: 【5月更文挑战第12天】厉害了!12秒将百万数据通过EasyExcel导入MySQL数据库中

一、写在开头

我们在上一篇文章中提到了通过EasyExcel处理Mysql百万数据的导入功能,当时我们经过测试数据的反复测验,100万条放在excel中的数据,4个字段的情况下,导入数据库,平均耗时500秒,这对于我们来说肯定难以接受,今天我们就来做一次性能优化。

image.png


二、性能瓶颈分析

一般的大数据量excel入库的场景中,耗时大概在如下几点里:

  • 耗时1: 百万数据读取,字段数量,sheet页个数,文件体积;针对这种情况,我们要选择分片读取,选择合适的集合存储。
  • 耗时2: 百万数据的校验,逐行分字段校验;这种情况的耗时会随着字段个数逐渐增加,目前我们的案例中不设计,暂不展开。
  • 耗时3: 百万数据的写入;选择合适的写入方式,如Mybatis-plus的分批插入,采用多线程处理等。

三、针对耗时1进行优化

耗时2的场景我们在案例中并未用到,耗时1中针对百万级数据的读取,我们必然要选择分片读取,分片处理,这在我们上一篇文章中就已经采用了该方案,这里通过实现EasyExcel的ReadListener页面读取监听器,实现其invoke方法,在方法中我们增加BATCH_COUNT(单次读取条数)配置,来进行分片读取。读取完后,我们一定要选择合适的集合容器存放临时数据,不同集合之间的增加数据性能存在差异这里我们选择ArrayList。

【优化前代码片段】
@Slf4j
@Service
public class EasyExcelImportHandler implements ReadListener<User> {
   
    /*成功数据*/
    private final CopyOnWriteArrayList<User> successList = new CopyOnWriteArrayList<>();
    /*单次处理条数*/
    private final static int BATCH_COUNT = 20000;
    @Resource
    private ThreadPoolExecutor threadPoolExecutor;
    @Resource
    private UserMapper userMapper;



    @Override
    public void invoke(User user, AnalysisContext analysisContext) {
   
        if(StringUtils.isNotBlank(user.getName())){
   
            successList.add(user);
            return;
        }
        if(successList.size() >= BATCH_COUNT){
   
            log.info("读取数据:{}", successList.size());
            saveData();
        }
    }
    ///
    ///
}
AI 代码解读
【优化后代码片段】
@Slf4j
@Service
public class EasyExcelImportHandler implements ReadListener<User> {
   
    /*成功数据*/
   // private final CopyOnWriteArrayList<User> successList = new CopyOnWriteArrayList<>();
    private final List<User> successList =  new ArrayList<>();
    /*单次处理条数,有原来2万变为10万*/
    private final static int BATCH_COUNT = 100000;
    @Resource
    private ThreadPoolExecutor threadPoolExecutor;
    @Resource
    private UserMapper userMapper;


    @Override
    public void invoke(User user, AnalysisContext analysisContext) {
   
        if (StringUtils.isNotBlank(user.getName())) {
   
            successList.add(user);
            return;
        }
        //size是否为100000条:这里其实就是分批.当数据等于10w的时候执行一次插入
        if (successList.size() >= BATCH_COUNT) {
   
            log.info("读取数据:{}", successList.size());
            saveData();
            //清理集合便于GC回收
            successList.clear();
        }
    }
    ///
    ///
 }
AI 代码解读

这里面我们主要做了2点优化,1)将原来的线程安全的CopyOnWriteArrayList换为ArrayList,前者虽然可保线程安全,但存储数据性能很差;2)将原来单批次2000调整为100000,这个参数是因电脑而异的,并没有最佳数值。

【注】本文中的代码仅针对优化点贴出,完整代码参考文首中的上一篇文章连接哈!


四、针对耗时3进行优化

针对耗时3的处理方案,我们这里准备了2个:JDBC分批插入+手动事务控制多线程+Mybatis-Plus批量插入

4.1 JDBC分批插入+手动事务控制

很多博文中都说mybatis批量插入性能低,有人建议使用原生的JDBC进行处理,那咱们就采用这种方案来测试一下。

首先我们既然要通过jdbc连接数据库进行操作,那就先准备一个连接工具类吧

public class JdbcConnectUtil {
   

    private static  String driver;
    private static  String url;
    private static  String name;
    private static  String password;

    /**
     * 创建数据Properties集合对象加载加载配置文件
     */
    static {
   
        Properties properties = new Properties();
        try {
   
            properties.load(JdbcConnectUtil.class.getClassLoader().getResourceAsStream("generator.properties"));
            driver = properties.getProperty("jdbc.driverClass");
            url = properties.getProperty("jdbc.connectionURL");
            name = properties.getProperty("jdbc.userId");
            password = properties.getProperty("jdbc.password");
            Class.forName(driver);
        } catch (IOException | ClassNotFoundException e) {
   
            e.printStackTrace();
        }
    }

    /**
     * 获取数据库连接对象
     * @return
     * @throws Exception
     */
    public static Connection getConnect() throws Exception {
   
        return DriverManager.getConnection(url, name, password);
    }

    /**
     * 关闭数据库相关资源
     * @param conn
     * @param ps
     * @param rs
     */
    public static void close(Connection conn, PreparedStatement ps, ResultSet rs) {
   
        try {
   
            if (conn != null) conn.close();
            if (ps != null) ps.close();
            if (rs != null) rs.close();
        } catch (SQLException e) {
   
            throw new RuntimeException(e);
        }
    }
    public static void close(Connection conn, PreparedStatement ps) {
   
        close(conn, ps, null);
    }
    public static void close(Connection conn, ResultSet rs) {
   
        close(conn, null, rs);
    }
}
AI 代码解读

有了工具类后,我们就可以在EasyExcelImportHandler类中进行JDBC导入逻辑的实现啦。

 /**
     * jdbc+事务处理
     */
    public void import4Jdbc(){
   

        //分批读取+JDBC分批插入+手动事务控制
        Connection conn = null;
        //JDBC存储过程
        PreparedStatement ps = null;
        try {
   
            //建立jdbc数据库连接
            conn = JdbcConnectUtil.getConnect();
            //关闭事务默认提交
            conn.setAutoCommit(false);
            String sql = "insert into user (id,name, phone_num, address) values";
            sql += "(?,?,?,?)";
            ps = conn.prepareStatement(sql);
            for (int i = 0; i < successList.size(); i++) {
   
                User user = new User();
                ps.setInt(1,successList.get(i).getId());
                ps.setString(2,successList.get(i).getName());
                ps.setString(3,successList.get(i).getPhoneNum());
                ps.setString(4,successList.get(i).getAddress());
                //将一组参数添加到此 PreparedStatement 对象的批处理命令中。
                ps.addBatch();
            }
            //执行批处理
            ps.executeBatch();
            //手动提交事务
            conn.commit();
        } catch (Exception e) {
   
            e.printStackTrace();
        } finally {
   
            //记得关闭连接
            JdbcConnectUtil.close(conn,ps);
        }
    }
AI 代码解读

这里我们通过PreparedStatement的addBatch()和executeBatch()实现JDBC的分批插入,然后用import4Jdbc()替换原来的savaData()即可。

经过多次导入测试,这种方案的平均耗时为140秒。相比之前的500秒确实有了大幅度提升,但是2分多钟仍然感觉有点慢。
image.png

4.2 多线程+Mybatis-Plus批量插入

我们知道Mybatis-Plus的IService中提供了saveBatch的批量插入方法,但经过查看日志发现Mybatis-Plus的saveBatch在最后还是循环调用的INSERT INTO语句!

这种情况下,测试多线程速度和单线程相差不大,所以需要实现真正的批量插入语句,两种方式,一种是通过给Mybatis-Plus注入器,增强批量插入,一种是在xml文件中自己拼接SQL语句,我们在这里选用后一种,因为我们只做一个表,直接手写xml很方便,如果是在企业开发时建议使用sql注入器实现(自定义SQL注入器实现DefaultSqlInjector,添加InsertBatchSomeColumn方法,通过使用InsertBatchSomeColumn方法批量插入。)。

【XML中手动批量插入】

 <insert id="insertSelective" parameterType="java.util.List">
    insert into user
    (id,name, phone_num, address
      )
    values
    <foreach collection="list" item="item" separator=",">
        (#{item.id},#{item.name},#{item.phoneNum},#{item.address})
    </foreach>
  </insert>
AI 代码解读

在在EasyExcelImportHandler类中的saveData()方法中实现多线程批量插入。

/**
     * 采用多线程读取数据
     */
    private void saveData() {
   
        List<List<User>> lists = ListUtil.split(successList, 1000);
        CountDownLatch countDownLatch = new CountDownLatch(lists.size());
        for (List<User> list : lists) {
   
            threadPoolExecutor.execute(() -> {
   
                try {
   
                    userMapper.insertSelective(list.stream().map(o -> {
   
                        User user = new User();
                        user.setName(o.getName());
                        user.setId(o.getId());
                        user.setPhoneNum(o.getPhoneNum());
                        user.setAddress(o.getAddress());
                        return user;
                    }).collect(Collectors.toList()));
                } catch (Exception e) {
   
                    log.error("启动线程失败,e:{}", e.getMessage(), e);
                } finally {
   
                    //执行完一个线程减1,直到执行完
                    countDownLatch.countDown();
                }
            });
        }
        // 等待所有线程执行完
        try {
   
            countDownLatch.await();
        } catch (Exception e) {
   
            log.error("等待所有线程执行完异常,e:{}", e.getMessage(), e);
        }
        // 提前将不再使用的集合清空,释放资源
        successList.clear();
        lists.clear();
    }
AI 代码解读

经过多次导入测试,100万数据量导入耗时平均在20秒,这就是一个很客观且友好用户的导入功能啦,毕竟100万的xlsx文件,打开都需要七八秒呢!
image.png


五、总结

OK!以上就是SpringBoot项目下,通过阿里开源的EasyExcel技术进行百万级数据的导入功能的优化步骤啦,由原来的500秒优化到20秒!

六、结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
【YashanDB知识库】MySQL迁移至崖山char类型数据自动补空格问题
**简介**:在MySQL迁移到崖山环境时,若字段类型为char(2),而应用存储的数据仅为&#39;0&#39;或&#39;1&#39;,查询时崖山会自动补空格。原因是mysql的sql_mode可能启用了PAD_CHAR_TO_FULL_LENGTH模式,导致保留CHAR类型尾随空格。解决方法是与应用确认数据需求,可将崖山环境中的char类型改为varchar类型以规避补空格问题,适用于所有版本。
Redis和Mysql如何保证数据⼀致?
1. 先更新Mysql,再更新Redis,如果更新Redis失败,可能仍然不⼀致 2. 先删除Redis缓存数据,再更新Mysql,再次查询的时候在将数据添加到缓存中 这种⽅案能解决1 ⽅案的问题,但是在⾼并发下性能较低,⽽且仍然会出现数据不⼀致的问题,⽐如线程1删除了 Redis缓存数据,正在更新Mysql,此时另外⼀个查询再查询,那么就会把Mysql中⽼数据⼜查到 Redis中 1. 使用MQ异步同步, 保证数据的最终一致性 我们项目中会根据业务情况 , 使用不同的方案来解决Redis和Mysql的一致性问题 : 1. 对于一些一致性要求不高的场景 , 不做处理例如 : 用户行为数据 ,
SpringBoot 通过集成 Flink CDC 来实时追踪 MySql 数据变动
通过详细的步骤和示例代码,您可以在 SpringBoot 项目中成功集成 Flink CDC,并实时追踪 MySQL 数据库的变动。
255 43
MySQL底层概述—4.InnoDB数据文件
本文介绍了InnoDB表空间文件结构及其组成部分,包括表空间、段、区、页和行。表空间是最高逻辑层,包含多个段;段由若干个区组成,每个区包含64个连续的页,页用于存储多条行记录。文章还详细解析了Page结构,分为通用部分(文件头与文件尾)、数据记录部分和页目录部分。此外,文中探讨了行记录格式,包括四种行格式(Redundant、Compact、Dynamic和Compressed),重点介绍了Compact行记录格式及其溢出机制。最后,文章解释了不同行格式的特点及应用场景,帮助理解InnoDB存储引擎的工作原理。
MySQL底层概述—4.InnoDB数据文件
基于SQL Server / MySQL进行百万条数据过滤优化方案
对百万级别数据进行高效过滤查询,需要综合使用索引、查询优化、表分区、统计信息和视图等技术手段。通过合理的数据库设计和查询优化,可以显著提升查询性能,确保系统的高效稳定运行。
48 9
MySQL和SQLSugar百万条数据查询分页优化
在面对百万条数据的查询时,优化MySQL和SQLSugar的分页性能是非常重要的。通过合理使用索引、调整查询语句、使用缓存以及采用高效的分页策略,可以显著提高查询效率。本文介绍的技巧和方法,可以为开发人员在数据处理和查询优化中提供有效的指导,提升系统的性能和用户体验。掌握这些技巧后,您可以在处理海量数据时更加游刃有余。
101 9
MySQL进阶突击系列(09)数据磁盘存储模型 | 一行数据怎么存?
文中详细介绍了MySQL数据库中一行数据在磁盘上的存储机制,包括表空间、段、区、页和行的具体结构,以及如何设计和优化行数据存储以提高性能。
PolarDB开源数据库进阶课18 通过pg_bulkload适配pfs实现批量导入提速
本文介绍了如何修改 `pg_bulkload` 工具以适配 PolarDB 的 PFS(Polar File System),从而加速批量导入数据。实验环境依赖于 Docker 容器中的 loop 设备模拟共享存储。通过对 `writer_direct.c` 文件的修改,替换了一些标准文件操作接口为 PFS 对应接口,实现了对 PolarDB 15 版本的支持。测试结果显示,使用 `pg_bulkload` 导入 1000 万条数据的速度是 COPY 命令的三倍多。此外,文章还提供了详细的步骤和代码示例,帮助读者理解和实践这一过程。
47 0
【YashanDB 知识库】MySQL 迁移至崖山 char 类型数据自动补空格问题
问题分类】功能使用 【关键字】char,char(1) 【问题描述】MySQL 迁移至崖山环境,字段类型源端和目标端都为 char(2),但应用存储的数据为'0'、'1',此时崖山查询该表字段时会自动补充空格 【问题原因分析】mysql 有 sql_mode 控制,检查是否启用了 PAD_CHAR_TO_FULL_LENGTH SQL 模式。如果启用了这个模式,MySQL 才会保留 CHAR 类型字段的尾随空格,默认没有启动。 #查看sql_mode mysql> SHOW VARIABLES LIKE 'sql_mode'; 【解决/规避方法】与应用确认存储的数据,正确定义数据
Linux下mysql数据库的导入与导出以及查看端口
本文详细介绍了在Linux下如何导入和导出MySQL数据库,以及查看MySQL运行端口的方法。通过这些操作,用户可以轻松进行数据库的备份与恢复,以及确认MySQL服务的运行状态和端口。掌握这些技能,对于日常数据库管理和维护非常重要。
175 8

热门文章

最新文章