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

easyexcel,mysql · 浏览次数 : 0

小编点评

**Java EasyExcel导入优化** **步骤:** 1. **获取数据库连接对象:** 使用 `JdbcConnectUtil` 获取连接对象。 2. **开启事务:** 设置事务关闭默认值为 `true`。 3. **创建 PreparedStatement 对象:** 定义 SQL 语句并设置参数。 4. **批量插入数据:** 使用 `addBatch()` 方法将数据添加到 PreparedStatement 对象中。 5. **提交事务:** 执行 `executeBatch()` 方法执行批量插入。 6. **关闭资源:** 关闭连接对象、PreparedStatement 和 ResultSet。 7. **实现多线程:** 使用 `threadPoolExecutor` 创建并执行多个线程。 8. **读取数据:** 使用 `split()` 方法将数据分批处理。 9. **启动线程:** 为每个线程执行数据插入操作。 10. **等待线程完成:** 使用 `countDownLatch` 监控线程执行数量。 11. **清理资源:** 清理内存中缓存的集合。 **XML 批量插入:** ```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> ``` **性能提升:** * 使用多线程提高插入效率。 * 使用 `split()` 和 `addBatch()` 实现批量插入。 * 使用 `threadPoolExecutor` 管理线程执行数量。 * 优化数据库连接和资源清理。

正文

一、写在开头

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

image


二、性能瓶颈分析

一般的大数据量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();
        }
    }
    ///
    ///
}

【优化后代码片段】

@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();
        }
    }
    ///
    ///
 }

这里面我们主要做了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);
    }
}

有了工具类后,我们就可以在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);
        }
    }

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

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

image

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>

在在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();
    }

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


五、总结

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

六、结尾彩蛋

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

image

如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

image

与厉害了!12秒将百万数据通过EasyExcel导入MySQL数据库中相似的内容:

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

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

[转帖]太厉害了,终于有人能把Ansible讲的明明白白了,建议收藏

https://zhuanlan.zhihu.com/p/530678807 一: ansible 的概述 1. ansible简介 Ansible是一款为类Unix系统开发的自由开源的配置和自动化工具。 它用Python写成,类似于saltstack和Puppet,但是有一个不同和优点是我们不需要

美团VS饿了么,到底谁更胜一筹?

最近啊,收到一个粉丝的投稿,我发现他在美团和饿了么都去面试过。 这俩企业大家应该都经常用吧,咱点外卖的时候,我有时候就琢磨,到底他俩谁更厉害点。 今天咱们就瞅瞅,在面试这块儿谁更难一些。 (目前都只有一面的情况,要是想要后续的,私聊我发给你哈) 美团 一面 自我介绍 项目做完了吗?背景是什么?项目初

前端说你的API接口太慢了,怎么办?

当有千万条海量数据时,前端调取接口发现接口响应的太慢,前端这时让你优化一下接口,你说有几千万条数据,觉得自己尽力了,前端觉得你好菜,别急,读完这篇文章,让前端喊你一声:大佬,厉害!!! 常用的方法总结 通过合理的分页加载、索引优化、数据缓存、异步处理、压缩数据等手段,可以有效地优化接口性能,提升系统

[转帖]Boost UDP Transaction Performance

https://www.cnblogs.com/charlieroro/p/14096252.html 提高UDP交互性能 这是一篇个人认为非常非常厉害的文章,取自这里。讲述了如何提升UDP流的处理速率,但实际涉及的技术点不仅仅限于UDP。这篇文章中涉及的技术正好可以把前段时间了解的知识串联起来。作

[转帖]perf-tools

https://github.com/brendangregg/perf-tools 网络不好 原作者很厉害 转帖学习. A miscellaneous collection of in-development and unsupported performance analysis tools f

大龄程序员思考

视觉AI很难做 近来一直在从事AI 视觉检测方面的工作,外行人可能觉得挺厉害,实际上这钱挣得基本等于搬砖 近两年以来,行业不景气、制造业利润上不去,那么在这个产业链中,老板就没有余钱来进行升级改造。 老板们是否愿意花钱投入视觉检测,一是老板的认知、二是是否有足够的资金实力; 就算最后,老板有需求,也

Vue3学习(二十四)- 文档页面功能开发

写在前面 这部分真的感觉超级难,其实也不能说难,主要是真的想不到这个思路应该这么做,或者说他好厉害,他怎么知道该这么设计实现。 说下难点吧,我觉得后天逻辑还好,主要是前端部分真的需要点花点时间来思考,比如布局、交互设计的实现等等。 文档页面功能开发 1、任务拆解 增加文档页面,首页点击电子书时,跳转

[转帖]40年前电脑、芯片、光刻机,美国第一,中国第二

https://www.mzfxw.com/e/action/ShowInfo.php?classid=12&id=111592 如果讲毛主席时代中国在电脑、芯片等技术产品很厉害,年轻人绝不会相信,说这不可能。 可这是事实,老徐(微信公众号:手抄报)就找到这些资料,有图有真相。 两个年轻人不记得的人