性能优化:Excel导入10w数据

需求说明

excel报表导出10w+数据
在我们的进出口贸易系统可能由于之前导入的数据量并不多没有对效率有过高的追求。但是到了二次开发版本,我预估导入时Excel 行数会是 10w+ 级别,而往数据库插入的数据量是大于 3n 的,也就是说 10w 行的 Excel,则至少向数据库插入 30w 行数据。

一些细节

数据导入:导入使用的模板由系统提供,格式是 xlsx (支持 65535+行数据) ,用户按照表头在对应列写入相应的数据。

数据校验:数据校验有两种:
2.字段长度、字段正则表达式校验等,内存内校验不存在外部数据交互。对性能影响较小,数据重复性校验,如票据号是否和系统已存在的票据号重复(需要查询数据库,十分影响性能)

1.数据插入:测试环境数据库使用 MySQL 5.5,未分库分表,连接池使用 Druid

迭代记录

第一版:POI + 逐行查询校对 + 逐行插入

这个版本是最古老的版本,采用原生 POI,手动将 Excel 中的行映射成 ArrayList 对象,然后存储到 List ,代码执行的步骤如下:
1.手动读取 Excel 成 List

2.循环遍历,在循环中进行以下步骤

.检验字段长度

一些查询数据库的校验,比如校验当前一些对于购销合同的发票是否在系统中存在,需要查询发票表

写入当前行数据

3.返回执行结果,如果出错 / 校验不合格。则返回提示信息并回滚数据显而易见的,这样实现一定是赶工赶出来的,后续可能用的少也没有察觉到性能问题,但是它最多适用于个位数/十位数级别的数据。存在以下明显的问题:

查询数据库的校验对每一行数据都要查询一次数据库,应用访问数据库来回的网络IO次数被放大了 n 倍,时间也就放大了 n 倍

写入数据也是逐行写入的,问题和上面的一样
数据读取使用原生 POI,代码十分冗余,可维护性差。

第二版:EasyPOI + 缓存数据库查询操作 + 批量插入

针对第一版分析的三个问题,分别采用以下三个方法优化。
缓存数据,以空间换时间。

逐行查询数据库校验的时间成本主要在来回的网络IO中,优化方法也很简单。将参加校验的数据全部缓存到 HashMap 中。直接到 HashMap 去命中。

例如:校验行中的报运单是否存在,原本是要用 合同号 去查询报运单表匹配合同ID,查到则校验通过,生成的装箱单ID,校验不通过则返回错误信息给用户。
而合同在到期的时候是不会更新的。因此我采用一条SQL,将该装箱单下所有的委托单 作为 key,以 购销合同ID 作为 value,存储到 HashMap 中,后续校验只需要在 HashMap 中命中自定义 SessionMapperMybatis 原生是不支持将查询到的结果直接写人一个 HashMap 中的,需要自定义SessionMapperSessionMapper 中指定使用 MapResultHandler 处理 SQL 查询的结果集。

@Repository
public class SessionMapper extends SqlSessionDaoSupport {
    
        @Resource      				 public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
    
            super.setSqlSessionFactory(sqlSessionFactory);    }
    // 区域楼宇单元房号 - 房屋ID    @SuppressWarnings("unchecked")    public Map<String, Long> getHouseMapByAreaId(Long areaId) {         MapResultHandler handler = new MapResultHandler();
 this.getSqlSession().select(BaseUnitMapper.class.getName()+".getHouseMapByAreaId", areaId, handler);        Map<String, Long> map = handler.getMappedResults();        return map;    }
MapResultHandler 处理程序,将结果集放入 HashMap
public class MapResultHandler implements ResultHandler {
    
        private final Map mappedResults = new HashMap();    @Override    public void handleResult(ResultContext context) {
    
            @SuppressWarnings("rawtypes")        Map map = (Map)context.getResultObject();        mappedResults.put(map.get("key"), map.get("value"));    }
    public Map getMappedResults() {
    
            return mappedResults;    }}

使用 values 批量插入
MySQL insert 语句支持使用 values (),(),() 的方式一次插入多行数据,通过 mybatis foreach 结合 java 集合可以实现批量插入,代码写法如下:

<insert id="insertList">    insert into table(colom1, colom2)    values    <foreach collection="list" item="item" index="index" separator=",">        ( #{item.colom1}, #{item.colom2})    </foreach></insert>

使用 EasyPOI 读写 Excel
EasyPOI 采用基于注解的导入导出,修改注解就可以修改Excel,非常方便,代码维护起来也容易

第三版:EasyExcel + 缓存数据库查询操作 + 批量插入

第二版采用 EasyPOI 之后,对于几千、几万的 Excel 数据已经可以轻松导入了,不过耗时有点久(5W 数据 10分钟左右写入到数据库)不过由于后来导入的操作基本都是开发在一边看日志一边导入,也就没有进一步优化

别慌,先上 GITHUB 找找别的开源项目。这时阿里 EasyExcel 映入眼帘:
EasyExcel 采用和 EasyPOI 类似的注解方式读写 Excel,因此从 EasyPOI 切换过来很方便,分分钟就搞定了。

也确实如阿里大神描述的:41w行、25列、45.5m 数据读取平均耗时 50s,因此对于大 Excel 建议使用 EasyExcel 读取。

第四版:优化数据插入速度

在第二版插入的时候,我使用了 values 批量插入代替逐行插入。每 30000 行拼接一个长 SQL、顺序插入。整个导入方法这块耗时最多,非常拉跨。后来我将每次拼接的行数减少到 10000、5000、3000、1000、500 发现执行最快的是 1000。

结合网上一些对 innodb_buffer_pool_size 描述我猜是因为过长的 SQL 在写操作的时候由于超过内存阈值,发生了磁盘交换。限制了速度,另外测试服务器的数据库性能也不怎么样,过多的插入他也处理不过来。所以最终采用每次 1000 条插入。

每次 1000 条插入后,为了榨干数据库的 CPU,那么网络IO的等待时间就需要利用起来,这个需要多线程来解决,而最简单的多线程可以使用 并行流 来实现,接着我将代码用并行流来测试了一下:10w行的 excel、42w 欠单、42w记录详情、2w记录、16 线程并行插入数据库、每次 1000 行。插入时间 72s,导入总时间 95 s。

其他影响性能的内容

日志,避免在 for 循环中打印过多的 info 日志。

在优化的过程中,我还发现了一个特别影响性能的东西:info 日志,还是使用 41w行、25列、45.5m 数据,在 开始-数据读取完毕 之间每 1000 行打印一条 info 日志,缓存校验数据-校验完毕 之间每行打印 3+ 条 info 日志,日志框架使用 Slf4j 。打印并持久化到磁盘。下面是打印日志和不打印日志效率的差别打印日志。

总结

提升Excel导入速度的方法:

使用更快的 Excel 读取框架(推荐使用阿里 EasyExcel)

对于需要与数据库交互的校验、按照业务逻辑适当的使用缓存。用空间换时间
使用 values(),(),() 拼接长 SQL 一次插入多行数据

使用多线程插入数据,利用掉网络IO等待时间(推荐使用并行流,简单易用)

避免在循环中打印无用的日志

猜你喜欢

转载自blog.csdn.net/weixin_46011971/article/details/108784325