记录一次因为OutOfMemoryError而发现的Excel文件导入慢的优化思路

起因:

公司新建了一个对账系统,流水的来源是在系统中埋点收集,账单部分,由业务员上传从业务方下载下来的Excel文件,初期业务量小,每个账单文件最多也就上千条数据。随着业务量慢慢增加,已经达到了上万数据,现在平均在每个账单文件为5W数据。

开始由业务员在群里反馈,文件上传了,但是文件内容并没有入库,也就是详情页查不到上传的数据。

---------------------

排查过程:

登录线上服务器看日志,上传账单后

1.将文件保存到服务器-OK

2.由POI解析成Bean-OK

3.校验数据-没有执行

4.保存数据库-没有执行

5.在日志中发现  OutOfMemoryError:GC overhead limit exceeded

2021-06-29 11:30:51.237[ERROR] com.alibaba.druid.pool.DruidDataSource$CreateConnectionThread.run(2779)[Druid-ConnectionPool-Create-1431142837]- create connection SQLException, url: xxxxx
java.sql.SQLException: java.lang.OutOfMemoryError: GC overhead limit exceeded
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:965) ~[mysql-connector-java-5.1.49.jar:5.1.49]
    ........................................
    at com.alibaba.druid.pool.DruidDataSource$CreateConnectionThread.run(DruidDataSource.java:2777) [druid-1.1.24.jar:1.1.24]
Caused by: java.lang.OutOfMemoryError: GC overhead limit exceeded

推测:

1.在创建连接时出现 OutOfMemoryError:GC overhead limit exceeded ,说明GC执行时间长,回收垃圾数量少。大量内存被占用,没有回收。

从日志中还能看到,程序卡在了‘校验数据’这一步,去看代码这一步到底做了什么?

因为其他公司的账单没有记录业务类型,所以‘校验数据’时,会根据流水号去数据库查对应的业务类型,这里导致了大量的查询。

看代码的查询条件,是根据“渠道”+“流水号”作为关联条件查询,查看流水表的索引情况,发现有联合索引 渠道 + 类型 + 流水号

根据MySql的最左匹配原则,这条查询只能命中“渠道”索引,而相同渠道的数据大概有几十万的数据,所以命中索引后还会扫描几十万的行才能拿到想要的数据。

为了验证猜测,将程序的查询条件写成SQL用explain执行,发现确实命中了联合索引,但是只命中了渠道字段,type=ref,rows= 383225。

从执行计划来看,确实比较浪费资源,增加一个渠道+流水号的联合索引,再次用explain检查SQL,type=ref,rows= 1,效率提升。

 

2.原来的‘校验数据‘这一步是单线程处理,如果一次Sql查询使用时间是50ms,乘以4W也是一个比较长的时间,大概26分钟。

如果把这4W数据校验完才能入库的话,那么就会有4W数据在内存中占用26分钟,如果一次上传一个文件,问题也不大,只是花费的时间比较久。

但是,如果同时上传十几个账单文件的话,就会导致上百万的数据在内存中占用,GC无法回收。

从这两条,我们会发现执行慢和GC异常的主要原因,就是内存中的Class得不到回收,而单线程查库又加长了占用内存的时间。

 

所以,优化的核心

 

1.减少数据库查询压力

给查询条件增加索引,尽量让一次查询的扫描范围为1,rows= 1

 

2.增加数据的处理速度

使用线程池 Executors.newFixedThreadPool(10) 每次处理10条数据。

 

3.减少数据在内存中的引用持续时间

将数据分批创建 Callable 对象,每100放入线程池,用invokeAll执行,等待本批次的数据都处理完成再放入下一批。

将处理完成的数据,及时入库,快速解放内存的占用。

说明:如果启用线程池,那么放入执行队列的时候,需要分批创建任务, 不然可能又会制造4W个Callable对象,导致内存占用增加。

 

最后:

最终优化结果,4W账单从上传到入库的处理时间,由之前的30分钟优化到现在的2分钟,每秒大约能处理450条账单数据。

因为当前的数据量不算大,每个文件才到万级别,所以没有过度设计,只是优化了数据库的查询和用多线程增加了并发处理的能力。

经过此次优化,得到的好处大概有一下几点:

1.优化后,对数据库CPU的压力减小
2.内存能及时回收,防止内存溢出,也减少了频繁GC带来的CPU压力
3.业务人员可以接受这个处理速度
4.在原有的架构上和逻辑上,改动比较少,不需要大范围的测试。

 

有没有更好的方案,肯定是有的。


但是最优的方案不一定是最好的方案,最好的方案就是用当前的资源和时间能解决问题的方案。

posted @ 2021-07-01 13:58  InkYi  阅读(550)  评论(0编辑  收藏  举报