> 文章列表 > 记一次优化过程(思维技巧和多线程)

记一次优化过程(思维技巧和多线程)

记一次优化过程(思维技巧和多线程)

业务情景:一个定时任务,每天一份3G的文件,从ftp服务器下载到本地服务器,然后从本地服务器一行行读取该文件。大概3000w 行的数据量,解析文件每一行,大概有1000w符合条件,然后插入数据库,并通过请求外部接口把数据发送到mq。
这里提供的文件有个问题,文件里提供的小区字段是错误的,所以还需要每次通过小区id去数据库查小区名称字段。

实践,并分析耗时的原因:
1、每个小区名称都需要通过小区id去数据库查询一次,每次大概2ms,1000w次大概要5.5小时
2、通过jpa的saveAll(list)方法批量入库,每2000条入库需要10s左右,大概需要13小时
3、每行数据都需要请求外部接口,做一些逻辑判断后发送到mq,需要请求1000w次,实际一条处理大概才15ms左右,而因为网络耗费时间太多,每次大概需要100ms,大概需要27小时

针对性解决耗时问题:
1、让需求人员去沟通能否提供正确的小区名称,结果不行。
看数据库,全省大概30w个小区,把小区id和小区名称一次性查询出来,放在一个map里,每次通过小区id去map中匹配小区名称即可。一次性查询出来大概40s左右,再转成map大概0.5s。
一共花费40s左右。

2、jpa的saveAll(list)原理是for循环里,每次都去insert,假如list有2000行数据,底层需要insert2000次。
需要优化成mybatis那种写法,insert () values(),(),()…,只insert一次,2000条大概600ms。
然后继续优化用线程池,每次攒够1w条就分给5个线程去执行,用countDownLatch等下5个线程执行完再继续下一次插入,不然数据库太多线程执行插入,可能兜不住。那大概1w条1s左右完成。
1000w条数据大概16分钟。

用jpa写类似mybatis的那种insert写法代码如下:

@Mapper
@Repository
public interface Itest{@Insert({"<script>" +"INSERT INTO table" +" (STATIS_DATE,AREA_NAME,CREATED_TIME,UPDATED_TIME)" +"select a.* from ( " +"<foreach collection=\\"list\\" item=\\"item1\\" index=\\"index\\"  separator=\\"union all\\"> " +" (select " +"#{item1.statisDate, jdbcType=BIGINT} as statisDate , "+"#{item1.areaName, jdbcType=VARCHAR} as areaName , "+"now() as createdTime , "+"now() as updatedTime "+"from dual) " +"</foreach> ) a " +"</script>"})int insertBatch(@Param("list") List<Entity> list);}

3、让提供接口的同事把接口参数做成list,这样我能一次请求一批,而不是一个个请求。
并让他那边做多线程处理,但是当数据发送到200w的时候,开始返回503的异常,他的服务崩了,分析是因为线程池堆积的任务太多,且该服务器本身分配的内存就少,让他那边取消用多线程处理。这里得承认我们经验不足,业内的经验基本都是调用方去写多线程。

所以由我这边调用方加线程池,开30个线程,每次list放200个过去,200*15ms,每个请求大概处理完需要3s, 6000条数据大概3s解决完,也就是1w条大概5s左右执行完。
1000w数据大概1.3小时。

因为入库和发送请求mq线程是异步执行的,所以执行时间取最长,大概1.3小时。愉快的解决了。

耗时的常见因素,主要是
IO
网络
服务器性能
资源的创建和释放:线程的创建和销毁、连接(数据库连接、网络连接)的创建和销毁
转换:字符到字节转换等
算法的时间复杂度高(如多层for循环,而且数据量很大)
数据库查询条件复杂没命中索引等

我们思考解决的角度是:
将串行变为并行或并发
同步操作变异步操作
多个请求合并成一个请求
用空间换时间
算法时间复杂度的优化
提高机器性能(CUP/内存/宽带/磁盘等)
利用各种池,如数据库连接池、缓存连接池等
数据库索引优化