记录一次因为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.在原有的架构上和逻辑上,改动比较少,不需要大范围的测试。
有没有更好的方案,肯定是有的。
但是最优的方案不一定是最好的方案,最好的方案就是用当前的资源和时间能解决问题的方案。