
项目描述我将公司的项目内容抽象大概是要做这样一件事情。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库插入用户需要调用sdkHTTP请求接口那每一次调用都需要建立连接等待响应再释放链接。正如你要给朋友送一箱苹果你分成100次每次只送一个时间全搭载路上了。第二版面向对象——21天特征面向对象、单一线程、可拓展、略微耦合、批量插入、数据可恢复架构设计根据第一版设计的问题第二版有了一些改进。当然最明显的就是从面向过程的思想转变为面向对象。我将整个过程抽离出来分配给不同的对象去处理。这样我所分配的对象时这样的1. 一个配置对象BatchStrategy。负责从配置文件中读取本次任务的策略并传递给执行者配置包括基础配置如总条数每次批量查询的数量每次批量插入的数量。还有一些数据源方面的如来源表的表名、列名、等这样如果换成其他数据库的类似导入就能供通过配置进行拓展了。2. 三个执行者整个执行过程可以分成三个部分读数据--处理数据--写数据可以分别交给三个对象ReaderProcessorWriter进行。这样如果某一处逻辑变了可以单独进行改变而不影响其他环节。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操作架构的思考优雅的代码应该是整洁而美妙不应是冗长而复杂的。这一版将会设计出简洁度如第一版而性能和拓展性超越所有版本的架构。通过总结前三版特征我发现不论是ReaderProcessorWriter都有共同的特征启动任务、处理任务、结束任务。而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 InteractiveT { 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 }有了这样的接口设计不论实现类具体怎么写主方法已经可以写出了变得异常整洁有序。只提炼主干部分去掉了一些细枝末节如日志输出、时间记录等。