10w 行级别数据的 Excel 导入优化记录

  • 写入当前行数据

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

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

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

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

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

第二版: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

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

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

使用 values 批量插入

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

insert into table(colom1, colom2)

values

( #{item.colom1}, #{item.colom2})

使用 EasyPOI 读写 Excel

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

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

第二版采用 EasyPOI 之后,对于几千、几万的 Excel 数据已经可以轻松导入了,不过耗时有点久(5W 数据 10分钟左右写入到数据库)不过由于后来导入的操作基本都是开发在一边看日志一边导入,也就没有进一步优化。但是好景不长,有新小区需要迁入,票据 Excel 有 41w 行,这个时候使用 EasyPOI 在开发环境跑直接就 OOM 了,增大 JVM 内存参数之后,虽然不 OOM 了,但是 CPU 占用 100% 20 分钟仍然未能成功读取全部数据。故在读取大 Excel 时需要再优化速度。莫非要我这个渣渣去深入 POI 优化了吗?别慌,先上 GITHUB 找找别的开源项目。这时阿里 EasyExcel 映入眼帘:

在这里插入图片描述

emmm,这不是为我量身定制的吗!赶紧拿来试试。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。

在这里插入图片描述

并行插入工具类

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

/**

  • 功能:利用并行流快速插入数据

  • @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 插入的数据类型

*/

public static void insertData(List list, Consumer<List> consumer) {

if (list == null || list.size() < 1) {

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
图片转存中…(img-kb32Qtxu-1715586927416)]

[外链图片转存中…(img-3hy1i2QL-1715586927416)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值