扫描下方海报二维码,试听课程:
mysql
(课程详细大纲,请参见文末)面试
=========================================sql
来源:https://www.cnblogs.com/flashsun数据库
===================================================编程
一直不知道性能优化都要作些什么,从哪方面思考,直到最近接手了一个公司的小项目,可谓麻雀虽小五脏俱全。让我这个编程小白学到了不少性能优化的知识,或者说一些思考方式。真的感觉到任何一点效率的损失放大必定倍数时,将会是天文数字。安全
最初个人程序计算下来须要跑2个月才能跑完,通过2周不断地调整架构和细节,将性能提高到了4小时完成。总体性能提高了360倍性能优化
不少心得体会,但愿和你们分享,也但愿多多批评指正,共同进步。多线程
我将公司的项目内容抽象,大概是要作这样一件事情:架构
一、数据库A中有2000万条用户数据;异步
二、将数据库A中的用户读出,为每条用户生成guid,并保存到数据库B中;
三、同时在数据库A中生成关联表;
项目要求为:
一、将用户存入数据库B的过程须要调用sdk的注册接口,不容许直接操做jdbc进行插入;
二、数据要求可恢复:再次运行要跳过已成功的数据;出错的数据要进行持久化以便下次能够选择恢复该部分数据;
三、数据要保证一致性:在不出错的状况下,数据库B的用户必然一一对应数据库A的关联表。若是出错,那么正确的数据加上记录下来的出错数据后要保证一致性;
四、速度要尽量块:共2000万条数据,在保证正确性的前提下,至多一天内完成;
特征:面向过程、单一线程、不可拓展、极度耦合、逐条插入、数据不可恢复
最初的一版简直是汇聚了一个项目的全部缺点。整个流程就是从A库读出一条数据,马上作处理,而后调用接口插入B库
而后在拼一个关联表的sql语句,插入A库。没有计数器,没有错误信息处理。
这样下来的代码最终预测2000万条数据要处理2个月。若是中间哪怕一条数据出错,又要从新再来2个月。简直可怕。
这个流程图就等同于废话,是彻底基于面向过程的思想,整个代码就是在一个大main方法里写的,实际业务流程彻底等同于代码的流程。
思考起来简单,但实现和维护起来极为困难,代码结构冗长混乱。并且几乎是不可扩展的。暂且不谈代码的设计美观,它的效率如此低下主要有一下几点:
一、每一条数据的速度受制于整个链条中最慢的一环。
试想假若有一条A库插入关联表的数据卡住了,等待将近1分钟(夸张了点),那这一分钟jvm彻底就在傻等,它彻底能够继续进行以前的两步。
正如你等待鸡蛋煮熟的过程当中能够同时去作其余的事同样。
二、向B库插入用户须要调用sdk(HTTP请求)接口
那每一次调用都须要创建链接,等待响应,再释放连接。正如你要给朋友送一箱苹果,你分红100次每次只送一个,时间全搭载路上了。
特征:面向对象、单一线程、可拓展、略微耦合、批量插入、数据可恢复
3.一、架构设计
根据初版设计的问题,第二版有了一些改进。固然最明显的就是从面向过程的思想转变为面向对象。
我将整个过程抽离出来,分配给不一样的对象去处理。这样,我所分配的对象时这样的:
一、一个配置对象:BatchStrategy。
负责从配置文件中读取本次任务的策略并传递给执行者,配置包括基础配置如总条数,每次批量查询的数量,每次批量插入的数量。
还有一些数据源方面的,如来源表的表名、列名、等,这样若是换成其余数据库的相似导入,就能供经过配置进行拓展了。
二、三个执行者:
整个执行过程能够分红三个部分:读数据--处理数据--写数据,能够分别交给三个对象Reader,Processor,Writer进行。
这样若是某一处逻辑变了,能够单独进行改变而不影响其余环节。
三、一个失败数据处理类:ErrorHandler。
这样每当有数据出现异常时,便把改数据扔给这个类,在这给类中进行写入日志,或者其余的处理办法。在必定程度上将失败数据的处理解耦。
这种设计很大程度上解除了耦合,尤为是失败数据的处理基本上彻底解耦。
但因为整个执行过程仍然是须要有一个main来分别调用三个对象处理任务,所以三者之间仍是没有彻底解耦
main部分的逻辑依然是面向过程的思想,比较复杂。即便把main中执行的逻辑抽出一个service,这个问题依然没有解决。
3.二、效率问题
因为将初版的逐条插入改成批量插入。其中sdk接口部分是批量传入一组数据,减小了http请求的次数。生成关联表的部分是用了jdbc batch操做,将以前逐条插入的excute改成excuteBatch,效率提高很明显。
这两部分批量带来的效率提高,将本来须要两个月时间的代码,提高到了21天,但依然是天文数字。
能够看出,本次效率提高仅仅是在减小http请求次数,优化sql的插入逻辑方面作出来努力,但依然没有解决初版的一个致命问题
即一次循环的速度依然受制于整个链条中最慢的一环,三者没有解耦也能够从这一点看出,在其余二者没有将工做作完时,就只能傻等,这是效率损失最严重的地方了。
特征:面向对象、多线程、可拓展、彻底解耦、批量插入、数据可恢复。
4.一、架构设计
该版并无代码实现,但确是过分到下一版的重要思考过程,故记录在次。这一版本较上一版的重大改进之处有两点:队列和多线程。
队列:其中队列的使用使上一版未彻底解耦的执行类之间,实现了彻底解耦,将同步过程变为异步,同时也是多线程可以使用的前提。
Reader作的事就是读取数据,并放入队列,至于它的下一个环节Processor如何处理队列的数据,它彻底不用理会,
这时即可以继续读取数据。这便作到了彻底解耦,处理队列的数据也可以使用多线程了。
多线程:Processor和Writer所作的事情,就是读取自身队列中的数据,而后处理。只不过Processor比Writer还承担了一个往下一环队列里放数据的过程。
此处的队列用的是多线程安全队列ConcurrentLinkedQueue。所以能够肆无忌惮地使用多线程来执行这二者的任务。
因为各个环节之间的彻底解耦,某一环上的偶尔卡主并再也不影响整个过程的进度,因此效率提高不知一两点。
还有一点就是数据的可恢复性在这个设计中有了保障,成功过的用户被保存起来以便再次运行不会冲突,失败的关联表数据也被记录下来
在下次运行时Writer会先将这一部分加入到本身的队列里,整个数据的正确性就有了一个不是特别完善的方案,效率也有了可观的提高。
4.二、效率问题
虽然效率从21天提高到了3天,但咱们还要思考一些问题。实际在执行的过程当中发现,Writer所完成的数据老是紧跟在Processor以后。
这就说明Processor的处理速度要慢于Writer,由于Processor插入数据库以前还要走一段注册用户的业务逻辑。
这就有个问题,当上一环的速度慢过下一环时,还有必要进行批量的操做么?
答案是不须要的。
试想一下,若是你在生产线上,你的上一环2秒钟处理一个零件,而你的速度是1秒钟一个。这时即便你的批量处理速度更快,从系统最优的角度考虑,你也应该来一个零件就立刻处理,而不是等积攒到100个再批量处理。
还有一个问题是,咱们从未考虑过Reader的性能。实际上我用的是limit操做来批量读取数据库
而mysql的limit是先全表查再截取,当起始位置很大时,就会愈来愈慢。0-1000万还算轻松,但1000万到2000万简直是“步履维艰”。因此最终效率的瓶颈反而落到了读库操做上。
特征:面向接口、多线程、可拓展、彻底解耦、批量或逐条插入、数据可恢复、优化查询的limit操做
5.一、架构的思考
优雅的代码应该是整洁而美妙,不该是冗长而复杂的。这一版将会设计出简洁度如初版,而性能和拓展性超越全部版本的架构。
经过总结前三版特征,我发现不管是Reader,Processor,Writer,都有共同的特征:启动任务、处理任务、结束任务。
而Reader和Processor又有一个共同的能够向下一道工序传递数据,通知下一道工序数据传递结束的功能。
他们就像生产线上的一个个工序,相互关联而又各自独立地运行着。每一道工序均可以启动,疯狂地处理任务,直到上一道工序通知结束为止。
而第一个发起通知结束的即是Reader,以后便一个通知下一个,直到整个工序中止,这个过程就是美妙的。
所以咱们能够将这三者都看作是Job,除了Reader外又都有与上一道工序交互的能力(其实Reader的上一道工序就是数据库),所以便有了以下的接口设计。
有了这样的接口设计,不论实现类具体怎么写,主方法已经能够写出了,变得异常整洁有序。
只提炼主干部分,去掉了一些细枝末节,如日志输出、时间记录等。
接下来就是具体实现类的问题了,这里实现类主要实现的是三个功能:
一、接收上一环的数据:
属于Interactive接口的receive方法的实现,基于以前的设计,便是对象中有一个ConcurrentLinkedQueue类型的属性,用来接收上一环传来的数据。
二、处理数据并传递给下一环:
在每个(有下一环的)对象属性中,放入下一环的对象。如Reader中要有Processor对象,Processor要有Writer,一旦有数据须要加入下一环的队列,调用其receiive方法便可。
三、告诉下一环我结束了:
本任务结束时,调用下一环对象的closeInteractive方法。而每一个对象判断自身结束的方法视状况而定
好比Reader结束的条件是批量读取的数据超过了一开始设置的total,说明数据读取完毕,能够结束。
而Processor结束的条件是,它被上一环通知告终束,而且从本身的队列中poll不出东西了,证实应该结束,结束后再通知下一环节。
这样整个工序就安全有序地退出了。不过因为是多线程,因此Processor不能贸然通知Writer结束信号,须要在Processor内部弄一个计数器,只有计数器达到预期的数量的那个线程的Processor,才能发起结束通知。
5.二、效率问题:
正如上一版提出的,Processor的处理速度要慢于Writer,因此Writer并不须要用batch去处理数据的插入,该成逐条插入反而是提升性能的一种方式。
大数据量limit操做十分耗时,因为测试部分只是在前几百万条测试,因此仍是大大低估了效率的损失。在后几百万条能够说每一次limit的读取都步履维艰。
考虑到这个问题,我选去了惟一一个有索引而且稍稍易于排序的字段“用户的手机号”,(不想吐槽它们设计表的时候竟然没有自增id。。。)
每次全表将手机号排序,再limit查询。查询以后将最后一条的手机号保存起来,成为当前读取的最后一条数据的一个标识。下次再limit操做就能够从这个手机号以后开始查询了。
这样每次查询不论从哪里开始,速度都是同样的。虽然前面部分的数据速度与以前的方案相比慢了很多,但却完美解决了大数据量limit操做的超长等待时间,预防了危险的发生。
至此,项目架构再次简洁起来,但同初版相比,已经不是同一级别的简洁了。
一、Reader部分是单线程在处理,因为读取是从数据库中,并非队列中,所以设计成多线程有些麻烦,但并非不可,这里是优化点
二、日志部分占有很大一部分比例,2000万条读、处理、写就要有至少6000万第二天志输出。若是设计成异步处理,效率会提高很多。
END
《21天互联网Java进阶面试训练营(分布式篇)》详细目录,扫描图片末尾的二维码,试听课程