計算與數據結構篇 - 哈希算法 (Hash)

計算與數據結構篇 - 哈希算法 (Hash)

哈希算法的定義和原理非常簡單,基本上一句話就可以概括了。將任意長度的二進制值串映射爲固定長度的二進制值串,這個映射的規則就是哈希算法,而通過原始數據映射之後得到的二進制值串就是哈希值

構成哈希算法的條件:

  • 從哈希值不能反向推導出原始數據(所以哈希算法也叫單向哈希算法)

  • 對輸入數據非常敏感,哪怕原始數據只修改了一個 Bit,最後得到的哈希值也大不相同;

  • 散列衝突的概率要很小,對於不同的原始數據,哈希值相同的概率非常小;

  • 哈希算法的執行效率要儘量高效,針對較長的文本,也能快速地計算出哈希值。

哈希算法的應用(上篇)

安全加密

說到哈希算法的應用,最先想到的應該就是安全加密。最常用於加密的哈希算法是 MD5(MD5 Message-Digest Algorithm,MD5 消息摘要算法)和 SHA(Secure Hash Algorithm,安全散列算法)。

除了這兩個之外,當然還有很多其他加密算法,比如 DES(Data Encryption Standard,數據加密標準)、AES(Advanced Encryption Standard,高級加密標準)。

前面我講到的哈希算法四點要求,對用於加密的哈希算法來說,有兩點格外重要。第一點是很難根據哈希值反向推導出原始數據,第二點是散列衝突的概率要很小。

不過,即便哈希算法存在散列衝突的情況,但是因爲哈希值的範圍很大,衝突的概率極低,所以相對來說還是很難破解的。像 MD5,有 2^128 個不同的哈希值,這個數據已經是一個天文數字了,所以散列衝突的概率要小於 1/2^128。

如果我們拿到一個 MD5 哈希值,希望通過毫無規律的窮舉的方法,找到跟這個 MD5 值相同的另一個數據,那耗費的時間應該是個天文數字。所以,即便哈希算法存在衝突,但是在有限的時間和資源下,哈希算法還是被很難破解的。

對於加密知識點的補充,md5這個算法固然安全可靠,但網絡上也有針對MD5中出現的彩虹表,最常見的思路是在密碼後面添加一組 鹽碼(salt), 比如可以使用md5(1234567.'2019@STARK-%$#&-idje-789'),2019@STARK-%$#&-idje-789作爲鹽碼起到了一定的保護和安全的作用。

唯一標識(uuid)

我們可以給每一個圖片取一個唯一標識,或者說信息摘要。比如,我們可以從圖片的二進制碼串開頭取 100 個字節,從中間取 100 個字節,從最後再取 100 個字節,然後將這 300 個字節放到一塊,通過哈希算法(比如 MD5),得到一個哈希字符串,用它作爲圖片的唯一標識。通過這個唯一標識來判定圖片是否在圖庫中,這樣就可以減少很多工作量。

如果還想繼續提高效率,我們可以把每個圖片的唯一標識,和相應的圖片文件在圖庫中的路徑信息,都存儲在散列表中。當要查看某個圖片是不是在圖庫中的時候,我們先通過哈希算法對這個圖片取唯一標識,然後在散列表中查找是否存在這個唯一標識。

如果不存在,那就說明這個圖片不在圖庫中;如果存在,我們再通過散列表中存儲的文件路徑,獲取到這個已經存在的圖片,跟現在要插入的圖片做全量的比對,看是否完全一樣。如果一樣,就說明已經存在;如果不一樣,說明兩張圖片儘管唯一標識相同,但是並不是相同的圖片。

數據校驗

BT 下載的原理是基於 P2P 協議的。我們從多個機器上並行下載一個 2GB 的電影,這個電影文件可能會被分割成很多文件塊(比如可以分成 100 塊,每塊大約 20MB)。等所有的文件塊都下載完成之後,再組裝成一個完整的電影文件就行了。Nginx上有個分片技術,大概就是這個意思。

我們通過哈希算法,對 100 個文件塊分別取哈希值,並且保存在種子文件中。我們在前面講過,哈希算法有一個特點,對數據很敏感。只要文件塊的內容有一丁點兒的改變,最後計算出的哈希值就會完全不同。所以,當文件塊下載完成之後,我們可以通過相同的哈希算法,對下載好的文件塊逐一求哈希值,然後跟種子文件中保存的哈希值比對。如果不同,說明這個文件塊不完整或者被篡改了,需要再重新從其他宿主機器上下載這個文件塊。

散列函數

前面講了很多哈希算法的應用,實際上,散列函數也是哈希算法的一種應用。

我們前兩節講到,散列函數是設計一個散列表的關鍵。它直接決定了散列衝突的概率和散列表的性能。不過,相對哈希算法的其他應用,散列函數對於散列算法衝突的要求要低很多。即便出現個別散列衝突,只要不是過於嚴重,我們都可以通過開放尋址法或者鏈表法解決。

不僅如此,散列函數對於散列算法計算得到的值,是否能反向解密也並不關心。散列函數中用到的散列算法,更加關注散列後的值是否能平均分佈,也就是,一組數據是否能均勻地散列在各個槽中。除此之外,散列函數執行的快慢,也會影響散列表的性能,所以,散列函數用的散列算法一般都比較簡單,比較追求效率。

哈希算法的應用(下篇)

負載均衡

我們知道,負載均衡算法有很多,比如輪詢、隨機、加權輪詢等。那如何才能實現一個會話粘滯(session sticky)的負載均衡算法呢?也就是說,我們需要在同一個客戶端上,在一次會話中的所有請求都路由到同一個服務器上。

最直接的方法就是,維護一張映射關係表,這張表的內容是客戶端 IP 地址或者會話 ID 與服務器編號的映射關係。客戶端發出的每次請求,都要先在映射表中查找應該路由到的服務器編號,然後再請求編號對應的服務器。這種方法簡單直觀,但也有幾個弊端:

  • 如果客戶端很多,映射表可能會很大,比較浪費內存空間;

  • 客戶端下線、上線,服務器擴容、縮容都會導致映射失效,這樣維護映射表的成本就會很大;

如果藉助哈希算法,這些問題都可以非常完美地解決。我們可以通過哈希算法,對客戶端 IP 地址或者會話 ID 計算哈希值,將取得的哈希值與服務器列表的大小進行取模運算,最終得到的值就是應該被路由到的服務器編號。 這樣,我們就可以把同一個 IP 過來的所有請求,都路由到同一個後端服務器上。

數據分片

1. 如何統計“搜索關鍵詞”出現的次數?

假如我們有 1T 的日誌文件,這裏面記錄了用戶的搜索關鍵詞,我們想要快速統計出每個關鍵詞被搜索的次數,該怎麼做呢?

我們來分析一下。這個問題有兩個難點,第一個是搜索日誌很大,沒辦法放到一臺機器的內存中。第二個難點是,如果只用一臺機器來處理這麼巨大的數據,處理時間會很長。

我們可以先對數據進行分片,然後採用多臺機器處理的方法,來提高處理速度。具體的思路是這樣的:爲了提高處理的速度,我們用 n 臺機器並行處理。我們從搜索記錄的日誌文件中,依次讀出每個搜索關鍵詞,並且通過哈希函數計算哈希值,然後再跟 n 取模,最終得到的值,就是應該被分配到的機器編號。

這樣,哈希值相同的搜索關鍵詞就被分配到了同一個機器上。也就是說,同一個搜索關鍵詞會被分配到同一個機器上。每個機器會分別計算關鍵詞出現的次數,最後合併起來就是最終的結果。

2. 如何快速判斷圖片是否在圖庫中?

如何快速判斷圖片是否在圖庫中?上一節我們講過這個例子,不知道你還記得嗎?當時我介紹了一種方法,即給每個圖片取唯一標識(或者信息摘要),然後構建散列表。

假設現在我們的圖庫中有 1 億張圖片,很顯然,在單臺機器上構建散列表是行不通的。因爲單臺機器的內存有限,而 1 億張圖片構建散列表顯然遠遠超過了單臺機器的內存上限。

我們同樣可以對數據進行分片,然後採用多機處理。我們準備 n 臺機器,讓每臺機器只維護某一部分圖片對應的散列表。我們每次從圖庫中讀取一個圖片,計算唯一標識,然後與機器個數 n 求餘取模,得到的值就對應要分配的機器編號,然後將這個圖片的唯一標識和圖片路徑發往對應的機器構建散列表。

當我們要判斷一個圖片是否在圖庫中的時候,我們通過同樣的哈希算法,計算這個圖片的唯一標識,然後與機器個數 n 求餘取模。假設得到的值是 k,那就去編號 k 的機器構建的散列表中查找。

散列表中每個數據單元包含兩個信息,哈希值和圖片文件的路徑。假設我們通過 MD5 來計算哈希值,那長度就是 128 比特,也就是 16 字節。文件路徑長度的上限是 256 字節,我們可以假設平均長度是 128 字節。如果我們用鏈表法來解決衝突,那還需要存儲指針,指針只佔用 8 字節。所以,散列表中每個數據單元就佔用 152 字節(這裏只是估算,並不準確)。

假設一臺機器的內存大小爲 2GB,散列表的裝載因子爲 0.75,那一臺機器可以給大約 1000 萬(2GB*0.75/152)張圖片構建散列表。所以,如果要對 1 億張圖片構建索引,需要大約十幾臺機器。在工程中,這種估算還是很重要的,能讓我們事先對需要投入的資源、資金有個大概的瞭解,能更好地評估解決方案的可行性。

實際上,針對這種海量數據的處理問題,我們都可以採用多機分佈式處理。藉助這種分片的思路,可以突破單機內存、CPU 等資源的限制。

分佈式存儲

一致性哈希算法登場

假設我們有 k 個機器,數據的哈希值的範圍是[0, MAX]。我們將整個範圍劃分成 m 個小區間(m 遠大於 k),每個機器負責 m/k 個小區間。當有新機器加入的時候,我們就將某幾個小區間的數據,從原來的機器中搬移到新的機器中。這樣,既不用全部重新哈希、搬移數據,也保持了各個機器上數據數量的均衡。

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