一直不知道性能优化都要做些什么,从哪方面思考,直到最近接手了一个公司的小项目,可谓麻雀虽小五脏俱全。让我这个编程小白学到了很多性能优化的知识,或者说一些思考方式。真的感受到任何一点效率的损失放大一定倍数时,将会是天文数字。最初我的程序计算下来需要跑2个月才能跑完,经过2周不断地调整架构和细节,将性能提升到了4小时完成。
很多心得体会,希望和大家分享,也希望多多批评指正,共同进步。
项目描述
我将公司的项目内容抽象,大概是要做这样一件事情。
1. 数据库A中有2000万条用户数据
2. 将数据库A中的用户读出,为每条用户生成guid,并保存到数据库B中
3. 同时在数据库A中生成关联表
项目要求为:
1. 将用户存入数据库B的过程需要调用sdk的注册接口,不允许直接操作jdbc进行插入
2. 数据要求可恢复:再次运行要跳过已成功的数据;出错的数据要进行持久化以便下次可以选择恢复该部分数据
3. 数据要保证一致性:在不出错的情况下,数据库B的用户必然一一对应数据库A的关联表。如果出错,那么正确的数据加上记录下来的出错数据后要保证一致性。
4. 速度要尽可能块:共2000万条数据,在保证正确性的前提下,至多一天内完成
第一版,面向过程——2个月
特征:面向过程、单一线程、不可拓展、极度耦合、逐条插入、数据不可恢复
最初的一版简直是汇聚了一个项目的所有缺点。整个流程就是从A库读出一条数据,立刻做处理,然后调用接口插入B库,然后在拼一个关联表的sql语句,插入A库。没有计数器,没有错误信息处理。这样下来的代码最终预测2000万条数据要处理2个月。如果中间哪怕一条数据出错,又要重新再来2个月。简直可怕。
这个流程图就等同于废话,是完全基于面向过程的思想,整个代码就是在一个大main方法里写的,实际业务流程完全等同于代码的流程。思考起来简单,但实现和维护起来极为困难,代码结构冗长混乱。而且几乎是不可扩展的。暂且不谈代码的设计美观,它的效率如此低下主要有一下几点:
1. 每一条数据的速度受制于整个链条中最慢的一环。试想假如有一条A库插入关联表的数据卡住了,等待将近1分钟(夸张了点),那这一分钟jvm完全就在傻等,它完全可以继续进行之前的两步。正如你等待鸡蛋煮熟的过程中可以同时去做其他的事一样。
2. 向B库插入用户需要调用sdk(HTTP请求)接口,那每一次调用都需要建立连接,等待响应,再释放链接。正如你要给朋友送一箱苹果,你分成100次每次只送一个,时间全搭载路上了。
第二版,面向对象——21天
特征:面向对象、单一线程、可拓展、略微耦合、批量插入、数据可恢复
架构设计
根据第一版设计的问题,第二版有了一些改进。当然最明显的就是从面向过程的思想转变为面向对象。
我将整个过程抽离出来,分配给不同的对象去处理。这样,我所分配的对象时这样的:
1. 一个配置对象:BatchStrategy。负责从配置文件中读取本次任务的策略并传递给执行者,配置包括基础配置如总条数,每次批量查询的数量,每次批量插入的数量。还有一些数据源方面的,如来源表的表名、列名、等,这样如果换成其他数据库的类似导入,就能供通过配置进行拓展了。
2. 三个执行者:整个执行过程可以分成三个部分:读数据--处理数据--写数据,可以分别交给三个对象Reader,Processor,Writer进行。这样如果某一处逻辑变了,可以单独进行改变而不影响其他环节。
3. 一个失败数据处理类:ErrorHandler。这样每当有数据出现异常时,便把改数据扔给这个类,在这给类中进行写入日志,或者其他的处理办法。在一定程度上将失败数据的处理解耦。
这种设计很大程度上解除了耦合,尤其是失败数据的处理基本上完全解耦。但由于整个执行过程仍然是需要有一个main来分别调用三个对象处理任务,因此三者之间还是没有完全解耦,main部分的逻辑依然是面向过程的思想,比较复杂。即使把main中执行的逻辑抽出一个service,这个问题依然没有解决。
效率问题
由于将第一版的逐条插入改为批量插入。其中sdk接口部分是批量传入一组数据,减少了http请求的次数。生成关联表的部分是用了jdbc batch操作,将之前逐条插入的excute改为excuteBatch,效率提升很明显。这两部分批量带来的效率提升,将原本需要两个月时间的代码,提升到了21天,但依然是天文数字。
可以看出,本次效率提升仅仅是在减少http请求次数,优化sql的插入逻辑方面做出来努力,但依然没有解决第一版的一个致命问题,就是一次循环的速度依然受制于整个链条中最慢的一环,三者没有解耦也可以从这一点看出,在其他两者没有将工作做完时,就只能傻等,这是效率损失最严重的地方了。
第三版,完全解耦(队列+多线程)——3天
特征:面向对象、多线程、可拓展、完全解耦、批量插入、数据可恢复
架构设计
该版并没有代码实现,但确是过度到下一版的重要思考过程,故记录在次。这一版本较上一版的重大改进之处有两点:队列和多线程。
队列:其中队列的使用使上一版未完全解耦的执行类之间,实现了完全解耦,将同步过程变为异步,同时也是多线程能够使用的前提。Reader做的事就是读取数据,并放入队列,至于它的下一个环节Processor如何处理队列的数据,它完全不用理会,这时便可以继续读取数据。这便做到了完全解耦,处理队列的数据也能够使用多线程了。
多线程:Processor和Writer所做的事情,就是读取自身队列中的数据,然后处理。只不过Processor比Writer还承担了一个往下一环队列里放数据的过程。此处的队列用的是多线程安全队列ConcurrentLinkedQueue。因此可以肆无忌惮地使用多线程来执行这两者的任务。由于各个环节之间的完全解耦,某一环上的偶尔卡主并不再影响整个过程的进度,所以效率提升不知一两点。
还有一点就是数据的可恢复性在这个设计中有了保障,成功过的用户被保存起来以便再次运行不会冲突,失败的关联表数据也被记录下来,在下次运行时Writer会先将这一部分加入到自己的队列里,整个数据的正确性就有了一个不是特别完善的方案,效率也有了可观的提升。
效率问题
虽然效率从21天提升到了3天,但我们还要思考一些问题。实际在执行的过程中发现,Writer所完成的数据总是紧跟在Processor之后。这就说明Processor的处理速度要慢于Writer,因为Processor插入数据库之前还要走一段注册用户的业务逻辑。这就有个问题,当上一环的速度慢过下一环时,还有必要进行批量的操作么?答案是不需要的。试想一下,如果你在生产线上,你的上一环2秒钟处理一个零件,而你的速度是1秒钟一个。这时即使你的批量处理速度更快,从系统最优的角度考虑,你也应该来一个零件就马上处理,而不是等积攒到100个再批量处理。
还有一个问题是,我们从未考虑过Reader的性能。实际上我用的是limit操作来批量读取数据库,而mysql的limit是先全表查再截取,当起始位置很大时,就会越来越慢。0-1000万还算轻松,但1000万到2000万简直是“寸步难行”。所以最终效率的瓶颈反而落到了读库操作上。
第四版,高度抽象(一键启动)——4小时
特征:面向接口、多线程、可拓展、完全解耦、批量或逐条插入、数据可恢复、优化查询的limit操作
架构的思考
优雅的代码应该是整洁而美妙,不应是冗长而复杂的。这一版将会设计出简洁度如第一版,而性能和拓展性超越所有版本的架构。
通过总结前三版特征,我发现不论是Reader,Processor,Writer,都有共同的特征:启动任务、处理任务、结束任务。而Reader和Processor又有一个共同的可以向下一道工序传递数据,通知下一道工序数据传递结束的功能。他们就像生产线上的一个个工序,相互关联而又各自独立地运行着。每一道工序都可以启动,疯狂地处理任务,直到上一道工序通知结束为止。而第一个发起通知结束的便是Reader,之后便一个通知下一个,直到整个工序停止,这个过程就是美妙的。
因此我们可以将这三者都看做是Job,除了Reader外又都有与上一道工序交互的能力(其实Reader的上一道工序就是数据库),因此便有了如下的接口设计。
1 /**
2 * 工作步骤接口.
3 */
4 public interface Job {
5 void init();
6 void start();
7 void stop();
8 void finish();
9 }
1 /**
2 * 可交互的(传入,通知结束).
3 */
4 public interface Interactive<T> {
5
6 /**
7 * 开放与外界交互的通道
8 */
9 void openInteract();
10
11 /**
12 * 接收外界传来的数据
13 * @param t
14 */
15 void receive(T t);
16
17 /**
18 * 关闭交互的通道
19 */
20 void closeInteract();
21
22 /**
23 * 是否处于可交互的状态
24 * @return true可交互的 false不可交互的活已关闭交互状态
25 */
26 boolean isInteractive();
27
28 }
有了这样的接口设计,不论实现类具体怎么写,主方法已经可以写出了,变得异常整洁有序。
只提炼主*分,去掉了一些细枝末节,如日志输出、时间记录等。
1 public static void main(String[] args) {
2
3 Job reader = Reader.getInstance();
4 Job processor = Processor.getInstance();
5 Job writer = Writer.getInstance();
6
7 reader.init();
8 processor.init();
9 writer.init();
10
11 start(reader, processor, processor, processor, writer, writer);
12
13 }
14
15 private static void start(Job... jobs){
16 for (Job job:jobs) {
17 Thread thread = new Thread(new Runnable() {
18 @Override
19 public void run() {
20 job.start();
21 }
22 });
23 thread.start();
24 }
25 }
接下来就是具体实现类的问题了,这里实现类主要实现的是三个功能:
1. 接收上一环的数据:属于Interactive接口的receive方法的实现,基于之前的设计,即是对象中有一个ConcurrentLinkedQueue类型的属性,用来接收上一环传来的数据。
2. 处理数据并传递给下一环:在每一个(有下一环的)对象属性中,放入下一环的对象。如Reader中要有Processor对象,Processor要有Writer,一旦有数据需要加入下一环的队列,调用其receiive方法即可。
3. 告诉下一环我结束了:本任务结束时,调用下一环对象的closeInteractive方法。而每个对象判断自身结束的方法视情况而定,比如Reader结束的条件是批量读取的数据超过了一开始设置的total,说明数据读取完毕,可以结束。而Processor结束的条件是,它被上一环通知了结束,并且从自己的队列中poll不出东西了,证明应该结束,结束后再通知下一环节。这样整个工序就安全有序地退出了。不过由于是多线程,所以Processor不能贸然通知Writer结束信号,需要在Processor内部弄一个计数器,只有计数器达到预期的数量的那个线程的Processor,才能发起结束通知。
效率问题:
正如上一版提出的,Processor的处理速度要慢于Writer,所以Writer并不需要用batch去处理数据的插入,该成逐条插入反而是提高性能的一种方式。
大数据量limit操作十分耗时,由于测试部分只是在前几百万条测试,所以还是大大低估了效率的损失。在后几百万条可以说每一次limit的读取都寸步难行。考虑到这个问题,我选去了唯一一个有索引并且稍稍易于排序的字段“用户的手机号”,(不想吐槽它们设计表的时候居然没有自增id。。。),每次全表将手机号排序,再limit查询。查询之后将最后一条的手机号保存起来,成为当前读取的最后一条数据的一个标识。下次再limit操作就可以从这个手机号之后开始查询了。这样每次查询不论从哪里开始,速度都是一样的。虽然前面部分的数据速度与之前的方案相比慢了不少,但却完美解决了大数据量limit操作的超长等待时间,预防了危险的发生。
至此,项目架构再次简洁起来,但同第一版相比,已经不是同一级别的简洁了。
关于继续优化的思考
1. Reader部分是单线程在处理,由于读取是从数据库中,并不是队列中,因此设计成多线程有些麻烦,但并不是不可,这里是优化点
2. 日志部分占有很大一部分比例,2000万条读、处理、写就要有至少6000万次日志输出。如果设计成异步处理,效率会提升不少。
这就是我本次项目优化的心得体会,还望各位大神予以指点。因为代码是公司为了避嫌,就不发到github了,感兴趣的大神可以私聊。
1. 本文的主题是“一个编程小白初探性能优化的经历”,最终的速度肯定不是很优的,主要是分享一下效率提升百分比这个过程。
2. 我说的最终是优化到了4小时,不大明白为什么有人拿出了6小时的解决方案说我的效率太慢。
3. 确实把三个job看成是一个job,或许会更好解决问题,我弄成三个job的确感觉有点多余
4. 用limit不用id的原因是它们没有自增id,而mysql数据库又没有rowid
5. 还望参考一下“项目要求”,这不是一个纯数据库迁移的任务,注册接口是别人写好的,你不能改又必须调用,这一部分时间决定了时间的基数,绝对不可能低于小时级别。所以本文重在分享不同的写法对效率的提升,是一个百分比的问题。当然如果有好的解决方案一定虚心请教。
除以两千万 约等于 260ms。也就是1s只处理4条不到的数据,不知道是走http慢,还是查数据库慢,而且还得是很慢。没有数据分析,就拍脑袋出时间总归让人不舒服。
2.
既然方案一已经批量先从A中查询了,为什么不能批量掉接口,批量插入关联表呢?这个方案连想没有,直接来了个面向对象的方案,号称提升了效率,但其实跟面向对象没有毛关系,只是搞了一堆松耦合的架构,来增加了复杂度(看楼主的业务需求也没有经常变化的需要,松耦合只会带来类的增加,完全没有必要)。
3.
方案三引入队列就更是一个很无用的设计了,你又不需要给前台快速响应结果,而是要执行完全部的结果,读完数据,接着批量走接口,接着批量插入数据库就足够了。增加了队列,那么入队、出队的时间都没有计算,只是因为后面变成了多线程处理就变快了而已,不是队列和解耦的功劳。
4. 多数数据库会有并发锁的限制,你后面的write做成多线程,但操作的是一张表,有可能出现死锁或者竞争锁的问题。
5.
我的解决方案:单线程批量查,然后批量掉接口,批量插入关系。日志用log4back,本来就会异步输出,不会影响效率的。假设数据库读20ms
1k条数据,插入关系表也就100ms足够了,中间调用接口时间长点按500ms计算,那么1k条大概620ms,12400s
约等于3.5小时。(1k条是拍脑袋,具体可以按照接口最大支持数量来算就好了)。如果还是慢,那么就多线程去读+去调用接口(意义不大,最后瓶颈估计还是在数据库操作上)。
6. 当然具体时间我也是在拍脑袋,但最起码有每步骤的时间做支撑,计算出来的数据跟实际的相比应该量级不会错,方案也很简单。
总结一下,楼主可能太注重架构了,各种松耦合的设计,但实际上如果你想速度快,性能高,代码越简单才会越快,不在于面向过程还是面向对象。真正的性能调优,首先要分析你的性能瓶颈,解决你的性能瓶颈就好了,就像你提到的limit问题一样。虚头巴脑的引入各种队列,各种架构,完全没有意义,不会提高你的性能。你的最后方案除了松耦合了,跟最开始的面向过程没啥区别(只是把原来在一个类一个方法写的,搞了多个类,多个方法而已,还增加了各种处理完了的通知,只是增加了系统的复杂度而已)。
初探性能优化——2个月到4小时的性能提升(copy)推荐阅读的更多相关文章
-
Java程序性能优化读书笔记(一):Java性能调优概述
程序性能的主要表现点: 执行速度:程序的反映是否迅速,响应时间是否足够短 内存分配:内存分配是否合理,是否过多地消耗内存或者存在内存泄漏 启动时间:程序从运行到可以正常处理业务需要花费多少时间 负载承 ...
-
【性能优化】404- 从 12.67s到1.06s 性能优化实战
作者:jerryOnlyZRJ 来源:https://juejin.im/post/5b6fa8c86fb9a0099910ac91 本文是对之前同名文章的修正,将所有webpack3的内容更新为we ...
-
SQL Server-聚焦存储过程性能优化、数据压缩和页压缩提高IO性能(一)
前言 关于SQL Server基础系列尚未结束,还剩下最后一点内容未写,后面会继续.有园友询问我什么时候开始写SQL Server性能系列,估计还得等一段时间,最近工作也比较忙,但是会陆陆续续的更新S ...
-
鲲鹏性能优化十板斧——鲲鹏处理器NUMA简介与性能调优五步法
TaiShan特战队六月底成立,至今百日有余,恰逢1024程序员节,遂整理此文,献礼致敬!希望能为广大在鲲鹏处理器上开发软件.性能调优的程序员们,提供一点帮助.从今天开始,将陆续推出性能调优专题文章. ...
-
Linux性能优化从入门到实战:01 Linux性能优化学习路线
我通过阅读各种相关书籍,从操作系统原理.到 Linux内核,再到硬件驱动程序等等. 把观察到的性能问题跟系统原理关联起来,特别是把系统从应用程序.库函数.系统调用.再到内核和硬件等不同的层级贯 ...
-
前端性能优化之利用 Chrome Dev Tools 进行页面性能分析
背景 我们经常使用 Chrome Dev Tools 来开发调试,但是很少知道怎么利用它来分析页面性能,这篇文章,我将详细说明怎样利用 Chrome Dev Tools 进行页面性能分析及性能报告数据 ...
-
java 性能优化:35 个小细节,让你提升 java 代码的运行效率
前言 代码 优化 ,一个很重要的课题.可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没 ...
-
Django ORM性能优化之count和len方法的选择(非常详细推荐干货)
接下来我将从源码层面分情况和应用分析我们在计算queryset数据集时是用orm的count函数计算长度还是用len函数计算数据集长度. 首先,我们知道ORM查询queryset数据集是惰性查询的,只 ...
-
转——Android应用开发性能优化完全分析
[工匠若水 http://blog.csdn.net/yanbober 转载请注明出处.] 1 背景 其实有点不想写这篇文章的,但是又想写,有些矛盾.不想写的原因是随便上网一搜一堆关于性能的建议,感觉 ...
随机推荐
-
CAD2010中文版安装问题记录
CAD2010中文版安装到简体中文语言包的时候提示[错误1606无法访问网络位置setup],直接打开cad2010 提示 [adui18res.dll not found]: 首先,进入注册表(re ...
-
URAL 1287. Mars Canals
题目链接 这题挺水,看懂了,就OK.卡了几下内存,还是卡过了. #include <iostream> #include <cstdio> #include <cstri ...
-
通过 ES6 Promise 和 jQuery Deferred 的异同学习 Promise
Deferred 和 Promise ES6 和 jQuery 都有 Deffered 和 Promise,但是略有不同.不过它们的作用可以简单的用两句话来描述 Deffered 触发 resolve ...
-
ASP.NET中的ViewState
曾经在两次面试中都遇到了这个问题,就是ViewState中存储的变量到底存储在哪里.由于基础比较差,以前在学习的时候,就没有注意 到这里的细节,包括Session中存储的变量,所以我想ViewStat ...
-
Objective-C开发编码规范
Objective-C 编码规范,内容来自苹果.谷歌的文档翻译,自己的编码经验和对其它资料的总结. 概要 Objective-C 是一门面向对象的动态编程语言,主要用于编写 iOS 和 Mac 应用程 ...
-
深入理解HTTP Session
深入理解HTTP Session session在web开发中是一个非常重要的概念,这个概念很抽象,很难定义,也是最让人迷惑的一个名词,也是最多被滥用的名字之一,在不同的场合,session一次的 ...
-
归并排序—Java版
一开始做算法的时候,感觉递归算法很绕,所以我就在阅读别人代码的基础上,对代码每一步都添加自己的注解,方便我以后的学习. public class MergeSort { /** * 归并排序 * @p ...
-
My SQL随记 003 数据表基础操作语法
数据表 查看数据表 修改表名 修改字段名 修改字段数据类型 添加删除-字段 约束(主外键默认检查) 查看表结构: 语法:DESRIBE(描述) table_Name; DESC table_Name ...
-
【BZOJ4005】[JLOI2015] 骗我呢(容斥,组合计数)
[BZOJ4005][JLOI2015] 骗我呢(容斥,组合计数) 题面 BZOJ 洛谷 题解 lalaxu #include<iostream> using namespace std; ...
-
CentOS 6.4在运行XFS时系统crash的bug分析
最近有一台CentOS 6.4的服务器发生多次crash,kernel version 是Linux 2.6.32-431.29.2.el6.x86_64.从vmcore-dmesg日志内容及cras ...