1. 程式人生 > 程式設計 >為什麼我們要儘量避免FileSort(檔案排序)

為什麼我們要儘量避免FileSort(檔案排序)

故事

現在,假設閱讀此文的你穿越回了小學二年級的時光,此時的你正在不斷的追求著隔壁班的班長小紅,恨不得把家裡所有東西都送給TA。那麼問題來了,如果你要把家裡東西都搬光送給小紅,你有幾種辦法?以下是我想到

  • 一件一件的搬,如果搬不動那就拆分(不排除你被你父母揍一頓的可能性)
  • 試圖通過吃藥讓自己變成大力士

上述例子看似滑稽,但其實這是一直以來人類解決大規模數量問題的解決方案,即要麼提升自身的能力以應付大規模的數量,要麼進行拆分,分而治之。

對應到IT行業,由於傳統小型機處理能力有限於是便有了大型機。如果不用大型機那咋辦嗎?只好拆分服務,於是便有了微服務。

經典面試題

面試官:假設你只有100M的記憶體可用,現在有一個大小為1G的檔案,裡面存放著整數,每個整數用4個位元組來儲存,要你對這個這個檔案中資料進行排序,你有什麼解決方案?

:打電話找行政的妹子跟她要一條8G的DDR4記憶體條,為了表示感謝順便約她去吃飯,說不定還能順利脫單。

面試官:emmm…..,回去等通知吧

解決方案

:要解決這個問題,首先我們需要分為兩種情況:

  • 資料不重複 如果資料不重複我們可以使用點陣圖來標記相應的資料,在需要輸出結果的時候遍歷點陣圖即可(此方案較為簡單,不在本文的討論範圍內)

  • 資料重複 由於只有100M的記憶體可用,完全利用這100M記憶體的情況下意味著我們一次可以對26214400個整數(100 * 1024 * 1024 / 4 ) 進行排序,這意味著我們要分次讀取檔案並對讀取的內容進行排序,並將每一次排序的結果儲存到檔案系統中,之後再對這些檔案進行合併。

面試官:可以用畫圖表示一下嗎?

:過程如下圖所示

面試官:可以,要不你現場寫一下程式碼吧

解決方案的實現

解決方案的實現總的來說有以下幾步

  • 根據緩衝區的大小讀入相應的資料量,並把他們轉為整數陣列,進行排序,並寫入檔案,重複這一步直到原始資料檔案中沒有資料可讀。

  • 合併這些已排序的檔案直到只剩一個檔案

將問題拆分開來看的話,我們需要解決以下子問題

  • 由於我們採用4個位元組的資料來儲存整數,因此我們需要解決整數按位元組存取的問題

你可以考慮一下為什麼我們要用四個位元組來存取整數?而不是將其轉為字串

  • 合併已排序的檔案的演演算法

方案一、預讀取一部分的資料寫入快取中,然後進行歸併排序(拆分之後的檔案中的資料都是有序的),當資料用完時再去檔案中讀取,重複此步驟直到沒有資料可讀

方案二、每次只從兩個檔案中讀取一個整數,進行比較,然後將較大/較小(取決於你要增序還是降序)的資料寫入檔案中

方案一,相對來說比較簡單並且速度比較快留給大家實現。

對於方案二,由於最近開發中有涉及狀態機,因此對於方案二我採用了狀態機的設計模式來實現。

該狀態機如下所示

外部排序的實現

給大夥提供個參考,我實現的方案還有進一步優化的空間?

測試

為了有一個直觀的印象,我們對一個16MB的檔案進行排序,緩衝區設定為512kb.

以下為測試結果

  • 檔案分割階段,可以看出檔案分割的時候所用時間都是差不多的

  • 合併階段,可以看出合併已排序的檔案所用的耗時是不斷遞增的因為併合並的檔案體積在不斷的遞增

如果我們直接將緩衝區設定為16MB呢?以下為測試結果,連合並階段都不用了。

面試官: 很好,那你能說出應用場景嗎?

:利用檔案(file)進行排序(sort)工作 = filesort,好像在哪裡見過…

面試官: 提示你一個單詞explain

FileSort

:想起來了,假設我們有一張表

CREATE TABLE `users` (
  `id` int(11NOT NULL,
  `account` varchar(45COLLATE utf8mb4_bin DEFAULT `nickname` `password` KEY (`id`)
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
複製程式碼

如果我們需要根據某一個欄位繼續排序並且沒有新增索引的話,那麼使用explain對該SQL進行查詢的話就會在Extra中看到filesort

如下圖所示

這意味著,MySQL無法根據索引對資料進行排序(如果有索引的話直接取就好了,不需要排序操作)。

只好對要排序欄位的進行排序了,但是生產環境中資料量可能會非常大,如果全部載入到記憶體中,必然會引起記憶體不足進而導致資料庫崩潰,因此必須劃出一塊專門的記憶體區域以供排序,而這塊記憶體區域很可能裝不下這巨大的資料量,必然要藉助外部檔案系統進行排序,這就是filesort的由來

面試官:很好,那你知道怎麼看這塊記憶體的大小嗎?

: 緩衝區 = buffer,根據mysql的一貫傳統,以下語句應該可以查到

show variables like '%buffer%'
複製程式碼

(圖中藍色標註的區域,即sort_buffer_size)

面試官:很好,那你知道怎麼優化嗎?

: 加索引唄,還能咋樣,要不叫運維給伺服器再加個記憶體條?或者把牙膏廠(Intel)的CPU換成農廠的CPU(AMD!YES)

面試官:只要你喜歡AMD,我們就是異父異母的親兄弟。哦,不對我是想問該怎麼加索引

:我們知道索引是有順序的,如果索引上資訊已經滿足了我們的需求,那麼就不需要使用filsort了。

比如上文中所提到的users表
我們建立了一個index

alter table users add index(nickname, account)
複製程式碼

考慮以下語句是否需要filesort

select nickname,account from order by nickname
複製程式碼
select * by account
複製程式碼

答案是

  • 第一條語句不需要filesort,因為索引中已經包含了我們所需要的資訊
  • 第二條語句可以直接使用索引(索引有序儲存),在讀取到索引對應的主鍵值後取相應的資料並直接返回給客戶端即可,不需要使用到sort_buffer
  • 第三條語句需要filesort,但由於account和nickname組合成了索引,每一個nickname對應的account都是有序,因此不同的nickname對應的account可以用來做歸併排序(如上文所提到的合併階段)

總結

今天的總結就三張圖

附錄

Q1: 為什麼用4個位元組來存整數

節省空間,用字串來存的話,你整數多長就得多少個位元組

Q2: 怎麼使用本文提供得外部排序DEMO


原始碼中的三個檔案分別是

  • 列印檔案中的資料
  • 對指定檔案進行排序
  • 生成隨機數檔案

Q3: 為什麼要使用狀態機來實現歸併排序

不得不說,用狀態機來梳理邏輯是比較清晰的,建議你也嘗試一下。但在本例中如果你使用緩衝區來儲存整數陣列的話效能會快很多。

參考資料

《高效能MySQL(第三版)》

索引相關的部分

《MySQL王者晉級之路》

3.4節