100000行级别数据的 Excel 导入优化之路

news2024/12/26 22:11:53

项目中有一个 Excel 导入的需求:缴费记录导入

由实施 / 用户 将别的系统的数据填入我们系统中的 Excel 模板,应用将文件内容读取、校对、转换之后产生欠费数据、票据、票据详情并存储到数据库中。

在接手之前可能由于之前导入的数据量并不多没有对效率有过高的追求。但是到了 4.0 版本,预估导入时Excel 行数会是 10w+ 级别,而往数据库插入的数据量是大于 3n 的,也就是说 10w 行的 Excel,则至少向数据库插入 30w 行数据。因此优化原来的导入代码是势在必行的。逐步分析和优化了导入的代码,使之在百秒内完成(最终性能瓶颈在数据库的处理速度上,测试服务器 4g 内存不仅放了数据库,还放了很多微服务应用。处理能力不太行)。具体的过程如下,每一步都有列出影响性能的问题和解决的办法。

导入 Excel 的需求在系统中还是很常见的,优化办法可能不是最优的,欢迎读者在评论区留言交流提供更优的思路

一些细节

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

  • 数据校验:数据校验有两种:

    • 字段长度、字段正则表达式校验等,内存内校验不存在外部数据交互。对性能影响较小

    • 数据重复性校验,如票据号是否和系统已存在的票据号重复(需要查询数据库,十分影响性能)

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

迭代记录

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

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

  1. 手动读取 Excel 成 List

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

    1. 检验字段长度

    2. 一些查询数据库的校验,比如校验当前行欠费对应的房屋是否在系统中存在,需要查询房屋表

    3. 写入当前行数据

  3. 返回执行结果,如果出错 / 校验不合格。则返回提示信息并回滚数据

显而易见的,这样实现一定是赶工赶出来的,后续可能用的少也没有察觉到性能问题,但是它最多适用于个位数/十位数级别的数据。存在以下明显的问题:

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

  • 写入数据也是逐行写入的,问题和上面的一样

  • 数据读取使用原生 POI,代码十分冗余,可维护性差。

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

针对第一版分析的三个问题,分别采用以下三个方法优化

缓存数据,以空间换时间

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

例如:校验行中的房屋是否存在,原本是要用 区域 + 楼宇 + 单元 + 房号 去查询房屋表匹配房屋ID,查到则校验通过,生成的欠单中存储房屋ID,校验不通过则返回错误信息给用户。而房屋信息在导入欠费的时候是不会更新的。并且一个小区的房屋信息也不会很多(5000以内)因此我采用一条SQL,将该小区下所有的房屋以 区域/楼宇/单元/房号 作为 key,以 房屋ID 作为 value,存储到 HashMap 中,后续校验只需要在 HashMap 中命中

自定义 SessionMapper

Mybatis 原生是不支持将查询到的结果直接写人一个 HashMap 中的,需要自定义 SessionMapper

SessionMapper 中指定使用 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;
    }
}

示例 Mapper

@Mapper
@Repository 
public interface BaseUnitMapper {
    // 收费标准绑定 区域楼宇单元房号 - 房屋ID
    Map<String, Long> getHouseMapByAreaId(@Param("areaId") Long areaId);
}
示例 Mapper.xml
<select id="getHouseMapByAreaId" resultMap="mapResultLong">
    SELECT
        CONCAT( h.bulid_area_name, h.build_name, h.unit_name, h.house_num ) k,
        h.house_id v
    FROM
        base_house h
    WHERE
        h.area_id = ##{areaId}
    GROUP BY
        h.house_id
</select>
            
<resultMap id="mapResultLong" type="java.util.HashMap">
    <result property="key" column="k" javaType="string" jdbcType="VARCHAR"/>
    <result property="value" column="v" javaType="long" jdbcType="INTEGER"/>
</resultMap>

之后在代码中调用 SessionMapper 类对应的方法即可。

使用 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

http://doc.wupaas.com/docs/easypoi/easypoi-1c0u4mo8p4ro8 采用基于注解的导入导出,修改注解就可以修改Excel,非常方便,代码维护起来也容易。

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

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

但是好景不长,有新小区需要迁入,票据 Excel 有 41w 行,这个时候使用 EasyPOI 在开发环境跑直接就 OOM 了,增大 JVM 内存参数之后,虽然不 OOM 了,但是 CPU 占用 100% 20 分钟仍然未能成功读取全部数据。故在读取大 Excel 时需要再优化速度。莫非要我这个渣渣去深入 POI 优化了吗?别慌,先上 GITHUB 找找别的开源项目。这时阿里 EasyExcel 映入眼帘。

EasyExcel 采用和 EasyPOI 类似的注解方式读写 Excel,因此从 EasyPOI 切换过来很方便,分分钟就搞定了。也确实如阿里大神描述的:41w行、25列、45.5m 数据读取平均耗时 50s,因此对于大 Excel 建议使用 EasyExcel 读取。

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

在第二版插入的时候,我使用了 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。

图片

并行插入工具类

并行插入的代码我封装了一个函数式编程的工具类,也提供给大家

/**
 * 功能:利用并行流快速插入数据
 *
 * @author Keats
 * @date 2020/7/1 9:25
 */
public class InsertConsumer {
    /**
     * 每个长 SQL 插入的行数,可以根据数据库性能调整
     */
    private final static int SIZE = 1000;

    /**
     * 如果需要调整并发数目,修改下面方法的第二个参数即可
     */
    static {
        System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "4");
    }

    /**
     * 插入方法
     *
     * @param list     插入数据集合
     * @param consumer 消费型方法,直接使用 mapper::method 方法引用的方式
     * @param <T>      插入的数据类型
     */
    public static <T> void insertData(List<T> list, Consumer<List<T>> consumer) {
        if (list == null || list.size() < 1) {
            return;
        }

        List<List<T>> streamList = new ArrayList<>();

        for (int i = 0; i < list.size(); i += SIZE) {
            int j = Math.min((i + SIZE), list.size());
            List<T> subList = list.subList(i, j);
            streamList.add(subList);
        }
        // 并行流使用的并发数是 CPU 核心数,不能局部更改。全局更改影响较大,斟酌
        streamList.parallelStream().forEach(consumer);
    }
}

这里多数使用到很多 Java8 的API,不了解的朋友可以翻看我之前关于 Java 的博客。方法使用起来很简单

InsertConsumer.insertData(feeList, arrearageMapper::insertList);

其他影响性能的内容

日志

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

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

打印日志

图片

不打印日志

图片

我以为是我选错 Excel 文件了,又重新选了一次,结果依旧

图片

缓存校验数据-校验完毕 不打印日志耗时仅仅是打印日志耗时的 1/10 !

总结

提升Excel导入速度的方法:

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

  • 对于需要与数据库交互的校验、按照业务逻辑适当的使用缓存。用空间换时间

  • 使用 values(),(),() 拼接长 SQL 一次插入多行数据

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

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

最后说一句(求关注!别白嫖!)

如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。

关注公众号:woniuxgg,在公众号中回复:笔记  就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1434574.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

冀蒙辽三地共同推进北斗卫星导航定位基准站资源共享

冀蒙辽三地共同推进北斗卫星导航定位基准站资源共享 近期&#xff0c;冀蒙辽三地共同举办了“北斗卫星导航定位基准站资源共享推进会”&#xff0c;旨在推动北斗卫星导航定位系统的规模化应用&#xff0c;加强区域北斗卫星导航定位基准站网络的协同服务能力&#xff0c;为经济…

QT 槽函数的五种写法

前三种写法&#xff1a; 方法五&#xff1a;

clr的执行模型-笔记

学习来源&#xff1a;《CLR via C by Jeffrey Richter 》第四版&#xff0c;第1章 clr的执行模型 1.C#编译生成执行程序集文件 编译文件的组成&#xff1a;pe32/pe32头&#xff0c;clr头&#xff0c;元数据&#xff0c;IL pe32/pe32头&#xff1a;windows标准执行文件头 cl…

FPGA平台以太网学习:涉及1G/2.5G Ethernet 和Tri Mode Ethernet MAC两个IP核的学习记录(二)——IP学习使用

文章目录 一、传输速率二、网口标准选择三、核功能选择四、共享逻辑五、总结&#xff08;重点&#xff09; 学习不能稀里糊涂&#xff0c;要学会多思考&#xff0c;发散式学习以及总结&#xff1a; FPGA作为一种器件&#xff0c;只是实现目的的一种方法&#xff0c;过度追求实现…

第二十四回 王婆计啜西门庆 淫妇药鸩武大郎-Numpy索引和切片操作示例

郓哥被王婆打了&#xff0c;就去找武大郎。将情况一说&#xff0c;两人商定去抓奸。一天武大郎只做了两三扇炊饼&#xff0c;约好了时间&#xff0c;郓哥进去顶住大门不让王婆关&#xff0c;武大郎直接跑进去&#xff0c;西门庆刚开始躲到床底下&#xff0c;后被潘金莲提醒&…

Uibot (RPA设计软件)智能识别信息+微信群发助手(升级版)———课后练习1

微信群发助手机器人的小项目友友们可以参考小北的课前材料二博客~ (本博客中会有部分课程ppt截屏,如有侵权请及请及时与小北我取得联系~&#xff09; 紧接着小北的前两篇博客&#xff0c;友友们我们即将开展新课的学习~RPA 培训前期准备指南——安装Uibot(RPA设计软件&#x…

微信小程序(三十五)双向绑定警告去除方法

该警告的出现原因是开发者工具自身的不足&#xff0c;但在调试过程中容易刷屏&#xff0c;这里讲一下解决方法 1. 在双向绑定后面绑定一个空函数&#xff08;bind:input"emptyfn"&#xff09; <input type"text" model:value"{{keyword}}" b…

物业公司数字档案室建设要求

物业公司数字档案室建设的要求可以包括以下几个方面&#xff1a; 1. 硬件设备&#xff1a;需要配置足够的计算机、服务器、网络设备等硬件设备&#xff0c;以支持档案的数字化存储和管理。 2. 软件系统&#xff1a;需要选择专久智能档案管理软件系统&#xff0c;确保可以方便地…

基于微信小程序的校园水电费管理小程序的研究与实现

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

不关电脑因为懒?你们不懂程序员!

笔者作为一只老程序猿&#xff0c;确实没有关电脑的习惯。有人说不关电脑因为懒&#xff0c;那只能说这个想法too young~ 这个问题的答案可能因人而异&#xff0c;但一般来说&#xff0c;程序员不喜欢关电脑的原因可能包括以下几个方面&#xff1a; 工作需要&#xff1a;对于…

用握力器玩谷歌小恐龙游戏(三)

往期回顾 用握力器玩谷歌小恐龙游戏&#xff08;一&#xff09; 用握力器玩谷歌小恐龙游戏&#xff08;二&#xff09; GS-GAME-PC 前言 这次更新主要是&#xff0c;将原来的使用Wifi Mesh串口接收上位机的方法&#xff0c;改成了蓝牙直连电脑的方式&#xff0c;这种方式的…

Google Chrome Close AutoUpdate

DOMException: play() failed because the user didn‘t interact with the document first.-CSDN博客 html5 audio video-CSDN博客 Google Chrome Close AutoUpdate 关闭google浏览器自动更新 1&#xff1a;检查是否已安装google浏览器&#xff0c;并卸载&#xff1a; 2&…

【JS】基于node-media-server搭建流媒体服务器示例

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍基于node-media-server搭建流媒体服务器示例。 学其所用&#xff0c;用其所学。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&…

【Shell的运行原理以及Linux当中的权限问题】

Shell的运行原理以及Linux当中的权限问题 Shell的运行原理Linux当中的权限问题Linux权限的概念如何实现用户账号之间的切换如何仅提升当前指令的权限如何将普通用户添加到信任列表 Linux权限管理文件访问者的分类 (人)文件类型和访问权限 (事物属性)文件权限值的表示方法文件访…

少儿编程考级:智慧启迪还是智商税?

在当前科技日新月异的时代背景下&#xff0c;少儿编程教育日益受到家长和社会的广泛关注。与此同时&#xff0c;各类少儿编程考级应运而生&#xff0c;引发了公众对于其价值和意义的深度探讨。一部分人认为这是对孩子逻辑思维与创新能力的有效锻炼&#xff0c;是智慧启迪的重要…

业务拓展利器!跨境电商如何选对代理IP?IPIDEA 一键连接全球商机!

文章目录 一、跨境电商发展与海外代理IP的重要性1.1 跨境电商的发展现状1.2 海外代理IP在跨境电商中的重要性 二、选对代理IP品牌的关键因素三、IPIDEA海外IP代理的优势3.1 IPIDEA的优势3.2 IPIDEA提供的代理类型 四、使用IPIDEA爬虫实战五、总结 一、跨境电商发展与海外代理IP…

算法——二分查找算法

1. 二分算法是什么&#xff1f; 简单来说&#xff0c;"二分"指的是将查找的区间一分为二&#xff0c;通过比较目标值与中间元素的大小关系&#xff0c;确定目标值可能在哪一半区间内&#xff0c;从而缩小查找范围。这个过程不断重复&#xff0c;每次都将当前区间二分…

五、Redis之发布订阅及事务管理

5.1 发布订阅 5.1.1 Redis 发布订阅 (pub/sub) 是一种消息通信模式&#xff1a;发送者 (pub) 发送消息&#xff0c;订阅者 (sub) 接收消息。Redis 客户端可以订阅任意数量的频道。下图展示了频道 channel1 &#xff0c;以及订阅这个频道的三个客户端 —— client1 、client2 …

2 月 5 日算法练习- 动态规划

DP&#xff08;动态规划&#xff09;全称Dynamic Programming&#xff0c;是运筹学的一个分支&#xff0c;是一种将复杂问题分解成很多重叠的子问题、并通过子问题的解得到整个问题的解的算法。 在动态规划中有一些概念&#xff1a; n<1e3 [][] &#xff0c;n<100 [][][…

Jenkins配置http请求github,发布release

学无止境&#xff0c;气有浩然&#xff01; Jenkins配置http请求github&#xff0c;发布release 前言Jenkins配置github配置在这里插入图片描述 打完收工! 前言 工作中进行了github迁移&#xff0c;原先的gitlab中配置的Jenkins的CI/CD步骤需要发布到Github发布release版本&am…