記一次代碼優化過程

這是轉的文章,俺的想法是蒐集一些人家開發心得好歸己以後開發參考,

有時間再把這些文章整理進自己的學習筆記本里,呵呵有點自私。

 

http://www.javaeye.com/topic/593572

 

記一次代碼優化過程
--- 大數據量的處理及存儲

1. 原始場景再現:
該模塊主要是客戶端負責上傳一個包含手機號碼的txt,其中一行一個手機號碼。服務端讀取並解析該文件,解析過程中需要做有效性驗證。例如:號碼位數,是有效數字及是否在有效號段之內。最後保存數據到DB。
該包含手機號碼文件數據在20W到200W之間。

2. 問題所在
在客戶端上傳20W數據的時候,後臺相應很慢,查看後臺的CPU及內存
mpstat -P ALL 1   //查看LINUX系統內存及CPU的消耗情況
發現CPU一直處在100%狀態下,而且消耗的時間很長。近十分鐘也沒有回覆到客戶端。

3. 問題詳解
首先查看代碼。列下原代碼思路:
1) 使用apache的公用包來處理文件的上傳,保存客戶端文件到服務器
2) 打開讀取文件IO,及寫日誌IO,讀取文件信息,到一個LIST中。
3) 雙重遍歷LIST,進行查重操作。重複數據記錄到日誌文件中
4) 遍歷LIST進行有效位數的校驗。重複數據記錄到日誌文件中
5) 遍歷LIST進行是否爲有效數字驗證。重複數據記錄到日誌文件中
6) 遍歷該LIST,組裝爲數據庫存儲對象傳遞到DAO層,DAO層再次遍歷對象容器,將插入對象添加到了批量提交的LIST中。最後將20W的數據一次性批量提交

4. 問題解決思路

第一次改動:仔細查看代碼我們可以很清晰的看出,問題主要在兩塊:
一是數據的有效性檢查,原代碼採用了多次循環遍歷的方式處理,很耗CPU。故首先將多次遍歷的處理應該縮減爲一次遍歷即可,但是仔細想想其中存在一個查重的處理操作,故我們將原本用list存儲的方式改爲SET存儲的方式,因爲set不會存儲重複的數據,這樣可以達到查重的效果。

Key:HashSet底層使用hash數組實現的,其原理就是當保存一個對象的時候,首先調用該對象的hashCode方法,獲得hash碼與原數組及數組子鏈表中的數據進行比較,若是相同的話則不進行插入操作,再不存在的情況下,才進行存儲。由於String 類型已經實現了hashCode方法,所以我們不需要實現該方法若是其他類型的對象我們則需要實現該方法。

二就是數組存儲的地方了,原代碼採用了幾十萬條數據的一次批量提交,當然很消耗資源,代碼回覆給客戶端慢。這裏我採用了ORACLE寫一個存儲過程,JAVA端傳遞一個數組給ORACLE,有存儲過程來處理大數據量,這樣就將服務器的壓力轉移到ORACLE安裝的的那臺服務器。
好的,第一次改動過完成。重啓TOMCAT試下。結果當我們只上傳20W數據的時候,發現還是很慢很慢,回頭又仔細看代碼,打斷點。再調試的過程中,發現在我只採用一個遍歷循環的時候,CPU就一直處理100%,原來在處理這20W數據的時候,CPU就一直處理很高的狀態了,那該怎麼辦呢?這個時候就是第二次改動了
------------------------------------這裏是華麗的分割線-------------------------------------
第二次改動:
個人經驗,一般處理這種大數據量有兩種方法,其實這兩次方法的本質是一樣的,就是爲了降低CPU。
第一種是在我們在遍歷循環的時候,在循環遍歷到一定數量的時候,進行Thread.sleep(5)操作,帶該線程睡眠片刻

Java代碼 複製代碼 收藏代碼
  1.   
  2. For (int I =0 ;I < list.size();i++)   
  3. {   
  4.     //…業務處理   
  5.     If (3000 == i)   
  6. {   
  7.         Thread.sleep(5)   
  8.     }   
  9. }  
For (int I =0 ;I < list.size();i++)
{
	//…業務處理
	If (3000 == i)
{
		Thread.sleep(5)
	}
}


強制CPU暫時不處理該線程。

第二種方法就是採用流控的方式,流控的原理就是我們有一個初始量,循環過程中我們累加這個量,當達到一定量的時候,該線程進行wait()操作,同時我們啓動一個定時器,定時間週期對該變量進行清零操作,並喚醒該線程。其中可能會出現兩種情況,一是我們定時器的清零操作還沒到,累積量就已經到了,那麼該線程就會處在等待狀態,等待清零時間到,喚醒線程。二是累積量還沒到,清零時間就到了,對線程進行清零,那麼這個時候線程會一直處在一個運行狀態,如果這個時候CPU使用率很高的話,就達不到我們需要的效果,所以該方式的控制就需要實際調整定時器的掃描週期、記累積量的值設定。實際過程中需要調整兩參數。
因爲我們是將流控寫成了一個工具類,所以不好貼出來,具體也就是上面所所得思路。
比較兩種方法我們明顯可以看出,採用流控的方式更佳,因爲它是不阻塞線程,wait的方式。第一次則是sleep,阻塞線程。
好,第二次改動已經完成了,我們就開工試試了。當你懷着美好的願望時,老天總是不能讓你如意。結果很失望,還是慢的一塌糊塗,然後不斷的調整參數,結果還是沒降下來。這個時候我就開始反思了,難道這種方式有問題,再去打斷點調試的過程發現,採用了流控後,在處理數據的時候是CPU確實是降下來了,但是還是存在一個問題,再沒處理完一條記錄後,都會有一個日誌的記錄,這個時候用的是bufferRead,結果我們在最後的close()IO的時候,一次行將內存緩存中的數據庫刷到文件,導致CPU及內存的過高。知道原因後我開始了第三次的改動。
-------------------------------------分割線又出現了--------------------------------------
第三次改動:
這個時候,我採用了流控相同的思路,就是在處理完一定數據量的數據後,我們進行一次flush操作,將內存中數據刷到文件中,免得一次性刷,同時因爲我們一般是整數的W級數據,所以定義一個常量爲3000(全局多個地方需要刷數據,好改動),作爲flush標示。因爲採用3000的話,最後一次我們刷的只有1000的數據量,同時關閉IO需要開銷,所以能夠在一定程度上降低CPU,同時發現在第一次flush的時候CPU會較高,越後CPU會越低,不知道什麼原因。

Java代碼 複製代碼 收藏代碼
  1. If (…)   
  2. {   
  3.         if (3000 == i)   
  4. {   
  5.         read.flush()l   
  6. }   
  7. }  
If (…)
{
		if (3000 == i)
{
		read.flush()l
}
}


OK ,又可以開工進行測試了,我是費了九牛二虎之力開動了TOMCAT(機子太差),哈哈,終於成功了。CPU從原來的100%降到了10%左右,很有成就感,嘿嘿。這樣就將給測試區測試了。但是,但是,測試告訴我一個很不幸的消息,CPU還是很高,這個是爲什麼呢?
仔細看看了,原來TMD應用的服務器跟DB在一臺服務器上,氣死老夫了。採用一次性傳遞數據的片刻,ORACLE會使CPU會飆到100%,而且處理這麼多數據的時候也會持續5s左右的時間一直處在該位。這個時候咋辦呢?這個就需要第四次改動了。
-----------------------------我是分割線,大家好!北邊的朋友在哪裏?--------------------------
第四次改動:
這個時候,首先改動的是存儲過程,採用了動態變量綁定,同時調整commit;的次數,在1000,3000,5000,10000條記錄的時候提交數據,結果調試後,沒什麼改觀,沒則,只好換個思路。
啓用存儲過程,在java端想辦法,最簡單的方法還是啓用原來的批量提交方式,修改爲一定數量數據後提交,因爲這個提交,並在提交後sleep,方式CPU一直處在這個狀態。
僞代碼就是

Java代碼 複製代碼 收藏代碼
  1. If ()   
  2. {   
  3.     If (3000 == i)   
  4. {   
  5.         //提交數據到DB   
  6.     //清零標示,清空臨時list   
  7.     Thread.sleep();   
  8. }   
  9. }  
If ()
{
	If (3000 == i)
{
		//提交數據到DB
	//清零標示,清空臨時list
	Thread.sleep();
}
}


就這麼簡單。提交調試後,發現java端不會消耗很高的CPU,在第一次提交批量數據時會到30%左右,第二三次會降到15%左右,同時ORACLE消耗的CPU在第一次會100%,但是持續時間很短,就1s的時間,在達到這樣的效果後,領導說可以了。總算結束了這段優化過程。
簡單的說,我這種方式不過是用時間換CPU的性能,只使用與對於時間要求不是很高,更要求CPU的性能的場景。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章