有個同學,說是系統中出現性能問題了,說是讓我幫助診斷一下。本來是不想花這時間的,結果耐不住對方的死纏亂打,只要答應幫看看。 故事發生的背景是,在文件上傳的時候,有時間會有人上傳了文件,但是最后沒有使用上傳的文件,這樣就會產生一些垃圾文件。 原來軟件作者就想寫一個后臺定時任務程序,來清除這些垃圾文件? 由于作者堅定的不讓我發她的SQL語句(這個我也理解,這么丑陋的SQL),所以這里就不發源代碼了,發偽代碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | void deleteMissLinkFile{ List fileList=getFileList(); List deleteFileList= new ArrayList(); for (file:fileList){ int count1=execute(select count(*) from ...); int count2=execute(select count(*) from ...); int count3=execute(select count(*) from ...); int count4=execute(select count(*) from ...); int count5=execute(select count(*) from ...); if (count1== 0 &&count2== 0 &&count3== 0 &&count4== 0 &&count5== 0 ){ deleteFileList.add(file); } } delete(deleteFileList); } |
當然,這里我已經給進行了一定的加工,使得看起一漂亮了許多,實際上,嗯嗯,實在是丑。 這個時候的性能情況是怎么樣的呢?說是表里的數據只有500多條,但是執行時間要100多秒,但是實際上實際的應用場景都遠不止這個數量級,而且隨著數據的增加,性能會呈指數級下降。 我說你去加10萬條記錄測試一下,保證你一晚上算不出來。 好吧,廢話少說,接下來看看怎么優化這段程序。 在開始之前,我們可以假設有N個文件,有M個文件引用表,而且假設所有的文件引用表中的記錄條數都一樣。 很顯然,原來的實現方法中執行了:1次文件數查詢+N*M次統計操作 最笨的優化方法 先用成本最低的方式來優化一把:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | void deleteMissLinkFile{ List fileList=getFileList(); List deleteFileList= new ArrayList(); for (file:fileList){ int count1=execute(select count(*) from ...); int count2=execute(select count(*) from ...); int count3=execute(select count(*) from ...); int count4=execute(select count(*) from ...); int count5=execute(select count(*) from ...); if (count1== 0 &&count2== 0 &&count3== 0 &&count4== 0 &&count5== 0 ){ deleteFileList.add(file); } } delete(deleteFileList); } |
嗯嗯,通過上面的重構,性能馬上就可以提升一倍。難看是難看了一點,但是1倍也是不小的提升哦。 原因,原來是要把所有的統計值都算出來,再進行判斷,通過上面的重構,平均只要查一半就可以退出了,所以性能會有1倍的提升。 1次文件數查詢+N*M/2次統計操作 一般的優化方法 偶當時提醒她說,你可以把內外換換,性能就會提升許多,結果死活聽不懂,<ignore_js_op>
。
實際上邏輯是這樣的,由于統計操作的執行效率是非常低的,而帶主鍵的查詢速度是非??斓模簿褪前堰壿嫃模罕闅v所有的文件看看引用次數是多少,改變成從所有文件列表中刪除所有已經引用的文件,其余就是要刪除的垃圾文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | void deleteMissLinkFile{ List fileList=getFileList(); List refList1=execute(select file from tb1…) for (ref:refList1){ fileList.remove(ref) } List refList2=execute(select file from tb2…) for (ref:refList2){ fileList.remove(ref) } …… delete(deleteFileList); } |
通過上面的優化,需要執行的SQL語句是: 1+m 條SQL語句,其它都是大量的內存數據比對,相對來說,性能會高太多,通過一定的技巧進行一些優化,會有更大的提升。 這種方式,我毛估估比原始的方式,可以提高兩個數量級以上。 為什么提高了兩個左右數量級還是說比較笨的方法呢? 因為這種方法雖然比原始的方法有了顯著的提升,但是還是存在嚴重的設計問題的。 首先,當數據量比較小的時候(這里的小是指與互聯網應用中的數據相比),做完全遍歷是沒有問題的,但是當數據量比較大的時候,用一條SQL來遍歷所有的數據,就是有非常大的問題的。這個時候就要引入一系列的復雜問題來解決,比如:把單機計算變成集群計算,把整個計算變成分段時間,不管怎么樣,都是非常復雜的處理過程。 無為而治的方法 下面就要推出最快的、最省事的、效率最高的方法。 其實一般來說,只要是算法都是有優化空間和余地的,因此一般來說本人很少把話說滿的。這次本人使用了“最”字,那就是用來表明未來已經沒有優化的空間了,那什么樣的算法才能沒有優化的空間呢?答案就是:啥也不做。 當然了,實際上也不可能啥也不做,問題就在哪里,你不做怎么可能好呢? 實際上就是把任務進行一定的分解。通過把架構進行合理的分析與設計,把所有的文件上傳、刪除都做成公共的方法(或服務),在需要與文件打交道的地方,凡是與文件打交道的時候,做如下處理:
- 文件上傳:在文件上傳數據中加一條數據,比如:文件相關信息,唯一標識,引用次數為0
- 文件關聯:當數據與文件關聯的時候,修改引用次數為+1
- 文件取消關聯:當數據與文件取消關聯的時候(一般來說是刪除或編輯的時候置為空或者換成另外一個的時候),修改引用次數為-1
自次,當要清理垃圾的時候,就非常簡單的了,只要: select ... from ... where ref_times=0 然后進行相應的清理工作就好。 這個時候就優化了處理模式,并且把文件引用數據的維護分解到業務工作的過程當中,可以極大幅度的提升清理垃圾的處理效率。當然有的人說了:如果這么做,會使得我的業務處理過程變慢,那怎么辦?其實也沒有關系了,你可以把這個變成異步消息的方式,通知文件引用處理去做這件事情就行了,這樣就不會影響到你的業務處理效率了。 總結 通過上面的分析,我們對文件上傳過程中的垃圾清理過程進行優化,并分析了原來的問題之所在,及后面3種優化方式及其優缺點對比。 當然,實際上許多朋友也會有更好的辦法來解決,歡迎大家參與討論,并批評指正。 如果,你喜歡我的博文,請關注我,以便收到我的最新動態。 如果對我的開源框架感興趣,可以從這里獲取到最新的代碼,也可以訪問Tiny官網獲取更多的消息,或到Tiny社區進行即時交流。