日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

4 次版本迭代,我们将项目性能提升了 360 倍!

發布時間:2025/3/21 编程问答 25 豆豆
生活随笔 收集整理的這篇文章主要介紹了 4 次版本迭代,我们将项目性能提升了 360 倍! 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一直不知道性能優化都要做些什么,從哪方面思考,直到最近接手了一個公司的小項目,可謂麻雀雖小五臟俱全。讓我這個編程小白學到了很多性能優化的知識,或者說一些思考方式。真的感受到任何一點效率的損失放大一定倍數時,將會是天文數字。

最初我的程序計算下來需要跑2個月才能跑完,經過2周不斷地調整架構和細節,將性能提升到了4小時完成。整體性能提升了360倍

很多心得體會,希望和大家分享,也希望多多批評指正,共同進步。

一、項目描述

我將公司的項目內容抽象,大概是要做這樣一件事情:

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天

特征:面向對象、單一線程、可拓展、略微耦合、批量插入、數據可恢復

3.1、架構設計

根據第一版設計的問題,第二版有了一些改進。當然最明顯的就是從面向過程的思想轉變為面向對象。

我將整個過程抽離出來,分配給不同的對象去處理。這樣,我所分配的對象時這樣的:

1、一個配置對象:BatchStrategy。

負責從配置文件中讀取本次任務的策略并傳遞給執行者,配置包括基礎配置如總條數,每次批量查詢的數量,每次批量插入的數量。

還有一些數據源方面的,如來源表的表名、列名、等,這樣如果換成其他數據庫的類似導入,就能供通過配置進行拓展了。

2、三個執行者:

整個執行過程可以分成三個部分:讀數據--處理數據--寫數據,可以分別交給三個對象Reader,Processor,Writer進行。

這樣如果某一處邏輯變了,可以單獨進行改變而不影響其他環節。

3、一個失敗數據處理類:ErrorHandler。

這樣每當有數據出現異常時,便把改數據扔給這個類,在這給類中進行寫入日志,或者其他的處理辦法。在一定程度上將失敗數據的處理解耦。

這種設計很大程度上解除了耦合,尤其是失敗數據的處理基本上完全解耦。

但由于整個執行過程仍然是需要有一個main來分別調用三個對象處理任務,因此三者之間還是沒有完全解耦

main部分的邏輯依然是面向過程的思想,比較復雜。即使把main中執行的邏輯抽出一個service,這個問題依然沒有解決。

3.2、效率問題

由于將第一版的逐條插入改為批量插入。其中sdk接口部分是批量傳入一組數據,減少了http請求的次數。生成關聯表的部分是用了jdbc batch操作,將之前逐條插入的excute改為excuteBatch,效率提升很明顯。

這兩部分批量帶來的效率提升,將原本需要兩個月時間的代碼,提升到了21天,但依然是天文數字。

可以看出,本次效率提升僅僅是在減少http請求次數,優化sql的插入邏輯方面做出來努力,但依然沒有解決第一版的一個致命問題

即一次循環的速度依然受制于整個鏈條中最慢的一環,三者沒有解耦也可以從這一點看出,在其他兩者沒有將工作做完時,就只能傻等,這是效率損失最嚴重的地方了。

四、第三版:完全解耦(隊列+多線程)——3天

特征:面向對象、多線程、可拓展、完全解耦、批量插入、數據可恢復。

4.1、架構設計

該版并沒有代碼實現,但確是過度到下一版的重要思考過程,故記錄在次。這一版本較上一版的重大改進之處有兩點:隊列和多線程。

**隊列:**其中隊列的使用使上一版未完全解耦的執行類之間,實現了完全解耦,將同步過程變為異步,同時也是多線程能夠使用的前提。

Reader做的事就是讀取數據,并放入隊列,至于它的下一個環節Processor如何處理隊列的數據,它完全不用理會,

這時便可以繼續讀取數據。這便做到了完全解耦,處理隊列的數據也能夠使用多線程了。

**多線程:**Processor和Writer所做的事情,就是讀取自身隊列中的數據,然后處理。只不過Processor比Writer還承擔了一個往下一環隊列里放數據的過程。

此處的隊列用的是多線程安全隊列ConcurrentLinkedQueue。因此可以肆無忌憚地使用多線程來執行這兩者的任務。

由于各個環節之間的完全解耦,某一環上的偶爾卡主并不再影響整個過程的進度,所以效率提升不知一兩點。

還有一點就是數據的可恢復性在這個設計中有了保障,成功過的用戶被保存起來以便再次運行不會沖突,失敗的關聯表數據也被記錄下來

在下次運行時Writer會先將這一部分加入到自己的隊列里,整個數據的正確性就有了一個不是特別完善的方案,效率也有了可觀的提升。

4.2、效率問題

雖然效率從21天提升到了3天,但我們還要思考一些問題。實際在執行的過程中發現,Writer所完成的數據總是緊跟在Processor之后。

這就說明Processor的處理速度要慢于Writer,因為Processor插入數據庫之前還要走一段注冊用戶的業務邏輯。

這就有個問題,當上一環的速度慢過下一環時,還有必要進行批量的操作么?

答案是不需要的。

試想一下,如果你在生產線上,你的上一環2秒鐘處理一個零件,而你的速度是1秒鐘一個。這時即使你的批量處理速度更快,從系統最優的角度考慮,你也應該來一個零件就馬上處理,而不是等積攢到100個再批量處理。

還有一個問題是,我們從未考慮過Reader的性能。實際上我用的是limit操作來批量讀取數據庫

而mysql的limit是先全表查再截取,當起始位置很大時,就會越來越慢。0-1000萬還算輕松,但1000萬到2000萬簡直是“寸步難行”。所以最終效率的瓶頸反而落到了讀庫操作上。

五、第四版:高度抽象(一鍵啟動)——4小時

特征:面向接口、多線程、可拓展、完全解耦、批量或逐條插入、數據可恢復、優化查詢的limit操作

5.1、架構的思考

優雅的代碼應該是整潔而美妙,不應是冗長而復雜的。這一版將會設計出簡潔度如第一版,而性能和拓展性超越所有版本的架構。

通過總結前三版特征,我發現不論是Reader,Processor,Writer,都有共同的特征:啟動任務、處理任務、結束任務。

而Reader和Processor又有一個共同的可以向下一道工序傳遞數據,通知下一道工序數據傳遞結束的功能。

他們就像生產線上的一個個工序,相互關聯而又各自獨立地運行著。每一道工序都可以啟動,瘋狂地處理任務,直到上一道工序通知結束為止。

而第一個發起通知結束的便是Reader,之后便一個通知下一個,直到整個工序停止,這個過程就是美妙的。

因此我們可以將這三者都看做是Job,除了Reader外又都有與上一道工序交互的能力(其實Reader的上一道工序就是數據庫),因此便有了如下的接口設計。

?

有了這樣的接口設計,不論實現類具體怎么寫,主方法已經可以寫出了,變得異常整潔有序。

只提煉主干部分,去掉了一些細枝末節,如日志輸出、時間記錄等。

接下來就是具體實現類的問題了,這里實現類主要實現的是三個功能:

1、接收上一環的數據

屬于Interactive接口的receive方法的實現,基于之前的設計,即是對象中有一個ConcurrentLinkedQueue類型的屬性,用來接收上一環傳來的數據。

2、處理數據并傳遞給下一環:

在每一個(有下一環的)對象屬性中,放入下一環的對象。如Reader中要有Processor對象,Processor要有Writer,一旦有數據需要加入下一環的隊列,調用其receiive方法即可。

3、告訴下一環我結束了:

本任務結束時,調用下一環對象的closeInteractive方法。而每個對象判斷自身結束的方法視情況而定

比如Reader結束的條件是批量讀取的數據超過了一開始設置的total,說明數據讀取完畢,可以結束。

而Processor結束的條件是,它被上一環通知了結束,并且從自己的隊列中poll不出東西了,證明應該結束,結束后再通知下一環節。

這樣整個工序就安全有序地退出了。不過由于是多線程,所以Processor不能貿然通知Writer結束信號,需要在Processor內部弄一個計數器,只有計數器達到預期的數量的那個線程的Processor,才能發起結束通知。

5.2、效率問題:

正如上一版提出的,Processor的處理速度要慢于Writer,所以Writer并不需要用batch去處理數據的插入,該成逐條插入反而是提高性能的一種方式。

大數據量limit操作十分耗時,由于測試部分只是在前幾百萬條測試,所以還是大大低估了效率的損失。在后幾百萬條可以說每一次limit的讀取都寸步難行。

考慮到這個問題,我選去了唯一一個有索引并且稍稍易于排序的字段“用戶的手機號”,(不想吐槽它們設計表的時候居然沒有自增id。。。)

每次全表將手機號排序,再limit查詢。查詢之后將最后一條的手機號保存起來,成為當前讀取的最后一條數據的一個標識。下次再limit操作就可以從這個手機號之后開始查詢了。

這樣每次查詢不論從哪里開始,速度都是一樣的。雖然前面部分的數據速度與之前的方案相比慢了不少,但卻完美解決了大數據量limit操作的超長等待時間,預防了危險的發生。

至此,項目架構再次簡潔起來,但同第一版相比,已經不是同一級別的簡潔了。

六、關于繼續優化的思考

1、Reader部分是單線程在處理,由于讀取是從數據庫中,并不是隊列中,因此設計成多線程有些麻煩,但并不是不可,這里是優化點

2、日志部分占有很大一部分比例,2000萬條讀、處理、寫就要有至少6000萬次日志輸出。如果設計成異步處理,效率會提升不少。

總結

以上是生活随笔為你收集整理的4 次版本迭代,我们将项目性能提升了 360 倍!的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。