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

二、性能瓶颈分析
一般的大数据量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分多钟仍然感觉有点慢。

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文件,打开都需要七八秒呢!

五、总结
OK!以上就是SpringBoot项目下,通过阿里开源的EasyExcel技术进行百万级数据的导入功能的优化步骤啦,由原来的500秒优化到20秒!
六、结尾彩蛋
如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

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

厉害了!12秒将百万数据通过EasyExcel导入MySQL数据库中的更多相关文章
- node 爬虫 ---  将爬取到的数据,保存到 mysql 数据库中
		
步骤一:安装必要模块 (1)cheerio模块 ,一个类似jQuery的选择器模块,分析HTML利器. (2)request模块,让http请求变的更加简单 (3)mysql模块,node连接mysq ...
 - 将 excel文件数据导入MySQL数据库中
		
第一步:先将Excel文件另存为文本文件(制表符分割) 第二步:将生成的txt文件另存,并修改编码格式utf8; 第三步:将文件放到指定位置,或自己想要的位置: G:\city.txt 第四步:避免创 ...
 - 对大数据的批量导入MySQL数据库
		
自己的库里有索引在用insert导入数据时会变慢很多 使用事务+批量导入 可以配置使用spring+mybatis整合的方式关闭自动提交事务(地址),选择批量导入每一百条导入使用list存储值传入到m ...
 - 【MySQL笔记】Excel数据导入Mysql数据库的实现方法——Navicat
		
很多公司尤其有点年头的公司,财务业务部门的各种表单都是excel来做的表格,随着互联网的发展各种业务流程都电子化流程化了,再在茫茫多的文档中去查找某一个年份月份的报告是件相当枯燥的事,所以都在想办法将 ...
 - 从Excel中读取数据并批量写入MySQL数据库(基于MySQLdb)
		
一.Excel内容如下,现在需要将Excel中的数据全部写入的MySQL数据库中: 二.连接MySQL的第三方库使用的是“MySQLdb”,代码如下: # -*- coding:utf-8 -*-im ...
 - 在Linux环境下,将Solr部署到tomcat7中,导入Mysql数据库数据, 定时更新索引
		
什么是solr solr是基于Lucene的全文搜索服务器,对Lucene进行了扩展优化. 准备工作 首先,去下载以下软件包: JDK8:jdk-8u60-linux-x64.tar.gz TOMCA ...
 - Java版将EXCEL表数据导入到数据库中
		
1.采用第三方控件JXL实现 try { //实例化一个工作簿对象 Workbook workBook=Workbook.getWorkbook(new File("F://qzlx.xls ...
 - 使用sqoop将MySQL数据库中的数据导入Hbase
		
使用sqoop将MySQL数据库中的数据导入Hbase 前提:安装好 sqoop.hbase. 下载jbdc驱动:mysql-connector-java-5.1.10.jar 将 mysql-con ...
 - JDBC中 mysql数据库的连接工具类 Java登录 及增删改查 整理 附带:Navicat Premium 11.0.12中文破解版.zip(下载)mysql数据库工具
		
先写一个工具类,有实现MySQL数据库连接的方法,和关闭数据库连接.关闭ResultSet 结果集.关闭PreparedStatement 的方法.代码如下: package com.swift; ...
 - 解析excel文件并将数据导入到数据库中
		
今天领导给安排了一个临时工作,让我将一个excel里面的数据解析后放入数据库中,经过一个下午的努力成功完成,现在将代码献上,希望对大家有所帮助 一.需要导入的jar 1.commons-collect ...
 
随机推荐
- 记一次nginx服务异常-无法访问问题排查
			
上一秒还好好地,突然下一秒nginx服务器就访问不了啦. 这让人很是疑惑,到底是什么原因导致的呢?问题如下 开始一步一步地排查问题. 尝试一:在windows电脑上使用telnet命令查看端口是否正常 ...
 - #Splay#洛谷 1486 [NOI2004]郁闷的出纳员
			
题目 分析 考虑加减工资直接打标记,查询第\(k\)多可以用平衡树, 删除有点恶心,这里考虑Splay,将需要删除的部分的后继splay到根节点并将左子树断边 代码 #include <cstd ...
 - OpenHarmony 4.0 Beta2新版本发布,邀您体验
			
2023年8月3日,OpenAtom OpenHarmony(简称"OpenHarmony")发布了Beta2版本,相较于历史版本我们持续完善ArkUI.文件管理.媒体.窗口.安 ...
 - 【直播回顾】OpenHarmony知识赋能第八期:手把手教你实现涂鸦小游戏
			
OpenHarmony第八期知识赋能直播已经在9月29日圆满落幕!从9月15日起,资深OS框架开发工程师巴延兴老师于每周四进行分享,通过实现涂鸦小游戏来帮助大家全面了解ArkUI框架的应用,拓宽知识 ...
 - Matplotlib绘图设置---坐标轴上下限/标题设置
			
坐标轴上下限设置 plt.plot(x, np.sin(x)) #设置坐标轴上下限 plt.xlim(-1, 11) plt.ylim(-1.5, 1.5) plt.plot(x, np.sin(x) ...
 - 用户触达难?流失率高?HMS Core预测服务和智能运营,助你提前掌握营销时机,解决此难题。
			
用户流失了,触达难? 活动做了那么多,转化仍然很低? 运营也需要提前思考,预测用户动向,提前精准触达,才能事半功倍.结合HMS Core分析服务的预测服务和智能运营,洞察营销时机,实时落地营销策略,提 ...
 - MindSpore编译构建后Vmap模块的RuntimeError问题
			
技术背景 这篇文章来源于MindSpore仓库中的一个Issue,简单描述问题就是,如果你用MindSpore开发了一个python软件供别人使用,那么很有可能涉及到编译构建的问题.但是如果直接使用编 ...
 - easyexcel实现导出添加文字水印
			
引入jar包 由于easyexcel没有引入ooxml-schemas包,所以需要额外添加. <!-- easyexcel依赖 --> <dependency> <gro ...
 - ArkUI新能力,助力应用开发更便捷
			
原文链接:https://mp.weixin.qq.com/s/TAuq1WC6435ebn6L61rZAA,点击链接查看更多技术内容: ArkUI是一套构建分布式应用的声明式UI开发框架.它具 ...
 - 全面支持JS/eTS应用开发,DevEco Studio 3.0 Beta4新版本发布
			
原文:https://mp.weixin.qq.com/s/j5Cl48ZxzEmnnpfoM0pKJg ,点击链接查看更多技术内容. HUAWEI DevEco Studio(后文简称DevEco ...