數據結構與算法分析:(十三)哈希算法

前面我們花了兩篇把散列表搞清楚了。詳情請戳:

數據結構與算法分析:(十一)散列表(上)
數據結構與算法分析:(十二)散列表(下)

可見散列表的重要性!那講哈希算法爲啥把前兩篇的散列表的文章貼出了呢?難道它們有什麼關係?沒錯,有關係,而且關係還很大。我們經常聽到有人把散列表叫作哈希表或者Hash 表,把哈希算法叫作Hash 算法或者散列算法那到底什麼是哈希算法呢? 我們接下來就來進入正題。

一、什麼是哈希算法?

哈希算法書上和各種其他平臺上說的好複雜,讓初學者一臉懵逼。其實哈希算法的原理非常簡單:將任意長度的二進制值串映射爲固定長度的二進制值串,這個映射的規則就是哈希算法,而通過原始數據映射之後得到的二進制值串就是哈希值

有點抽象哈,我們拿MD5這種哈希算法來舉下例:

我們分別對RiemannRiemann的博客兩個文本計算MD5哈希值,得到兩串看起來毫無規律的字符串(MD5 的哈希值是 128 位的 Bit 長度,爲了方便表示,我把它們轉化成了 16 進制編碼)。可以看出來,無論要哈希的文本有多長、多短,通過 MD5 哈希之後,得到的哈希值的長度都是相同的,而且得到的哈希值看起來像一堆隨機數,完全沒有規律。

MD5("Riemann") = bb4767201ad42c74e650c1b6c03d78fa
MD5("Riemann的博客") = cd611a31ea969b908932d44d126d195b

我們再來看下兩個非常相似的文本,Riemannriemann,這兩個文本只有首字母大小寫不同,但你會發現這兩個的哈希值完全不同。

MD5("Riemann") = bb4767201ad42c74e650c1b6c03d78fa
MD5("riemann") = a1fb91ac128e6aa37fe42c663971ac3d

而且從哈希值不能反向推導出原始數據,比如上面的哈希值bb4767201ad42c74e650c1b6c03d78fa很難反推對應的文本Riemann

哈希算法的應用非常非常多,這裏選用了常見的七個,分別是安全加密、唯一標識、數據校驗、散列函數、負載均衡、數據分片、分佈式存儲。

二、哈希算法的應用

1、安全加密

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

這裏嚴格意義上來說,MD5 是一個摘要算法,用於生成字符串的摘要信息以及簽名校驗信息,可以理解爲數字簽名。因爲加密算法的話要有加、解密,而 MD5 是不可逆的,因此不能真正算加密算法。這裏考慮 MD5 有加密功能,這裏把它歸進來哈,槓精勿槓。

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

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

第一點好像現在不怎麼成立了,MD5SHA-1兩大應用最多的算法被我國密碼學家王小云破解了,小夥伴們可以看下這篇文章:密碼學家王小云:十年破解MD5和SHA-1兩大國際密碼

重點來看下第二點,實際上,不管是什麼哈希算法,我們只能儘量減少碰撞衝突的概率,理論上是沒辦法做到完全不衝突的。爲什麼這麼說呢?

這裏就基於組合數學中一個非常基礎的理論,鴿巢原理(也叫抽屜原理)。這個原理本身很簡單,它是說,如果有 10 個鴿巢,有 11 只鴿子,那肯定有 1 個鴿巢中的鴿子數量多於 1 個,換句話說就是,肯定有 2 只鴿子在 1 個鴿巢內。

有了鴿巢原理的鋪墊之後,我們再來看,爲什麼哈希算法無法做到零衝突?

我們知道,哈希算法產生的哈希值的長度是固定且有限的。比如前面舉的 MD5 的例子,哈希值是固定的 128 位二進制串,能表示的數據是有限的,最多能表示 2^128 個數據,而我們要哈希的數據是無窮的。基於鴿巢原理,如果我們對 2 ^128+1 個數據求哈希值,就必然會存在哈希值相同的情況。這裏你應該能想到,一般情況下,哈希值越長的哈希算法,散列衝突的概率越低。

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

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

除此之外,沒有絕對安全的加密。越複雜、越難破解的加密算法,需要的計算時間也越長。比如 SHA-256 比 SHA-1 要更復雜、更安全,相應的計算時間就會比較長。密碼學界也一直致力於找到一種快速並且很難被破解的哈希算法。我們在實際的開發過程中,也需要權衡破解難度和計算時間,來決定究竟使用哪種加密算法。


2、唯一標識

我們來舉個例子。假如要你從海量圖庫裏找一張圖是否存在?你會怎麼做?你不可能用文件名來判斷吧,因爲有些可能存在名字相同但圖片內容不一樣的。那你可能會說,既然名字相同,那我就比較裏面的內容,把這種圖轉換成二進制串,再與圖庫中所有圖的二進制串進行一一比對。如果相同,則說明圖片在圖庫中存在。但是,每個圖片小則幾十 KB、大則幾 MB,轉化成二進制是一個非常長的串,比對起來非常耗時。有沒有比較快的方法呢?

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

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

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


3、數據校驗

像我們做後臺的經常會調用各種接口,那麼肯定會有網絡傳輸。我們知道,網絡傳輸是不安全的,我們調用第三方接口,有可能被黑客劫持了並把數據篡改了。我們該如何確保在網絡傳輸中重要的數據不被黑客篡改呢?

我們可以通過哈希算法,對數據進行校驗,前面我們說過,哈希算法非常敏感,有一丁點的改變,最後計算出來的哈希值完全不一樣。所以當調用完接口之後,我們可以通過相同的哈希算法,對調回來的數據求哈希值,然後與調用前的哈希值比較。如果不同,則數據被篡改了。


4、散列函數

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

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

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


5、負載均衡

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

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

  • 如果客戶端很多,映射表可能會很大,比較浪費內存空間;
  • 客戶端下線、上線,服務器擴容、縮容都會導致映射失效,這樣維護映射表的成本就會很大;

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


6、數據分片

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

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

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

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

實際上,這裏的處理過程也是 MapReduce 的基本設計思想。


7、分佈式存儲

現在互聯網面對的都是海量的數據、海量的用戶。我們爲了提高數據的讀取、寫入能力,一般都採用分佈式的方式來存儲數據,比如分佈式緩存。我們有海量的數據需要緩存,所以一個緩存機器肯定是不夠的。於是,我們就需要將數據分佈在多臺機器上。

該如何決定將哪個數據放到哪個機器上呢?我們可以借用前面數據分片的思想,即通過哈希算法對數據取哈希值,然後對機器個數取模,這個最終值就是應該存儲的緩存機器編號。

但是,如果數據增多,原來的 10 個機器已經無法承受了,我們就需要擴容了,比如擴到 11 個機器,這時候麻煩就來了。因爲,這裏並不是簡單地加個機器就可以了。

原來的數據是通過與 10 來取模的,現在是與 11 取模,有些之前的數據就會分配到另一臺機器上去。

因此,所有的數據都要重新計算哈希值,然後重新搬移到正確的機器上。這樣就相當於,緩存中的數據一下子就都失效了。所有的數據請求都會穿透緩存,直接去請求數據庫。這樣就可能發生雪崩效應,壓垮數據庫。

所以,我們需要一種方法,使得在新加入一個機器後,並不需要做大量的數據搬移。這時候,一致性哈希算法就要登場了。

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

一致性哈希算法的基本思想就是這麼簡單。除此之外,它還會藉助一個虛擬的環和虛擬結點,更加優美地實現出來。

關於一致性哈希算法可以參考這兩篇文章:

白話解析:一致性哈希算法 consistent hashing
漫畫:什麼是一致性哈希?

除了我們上面講到的分佈式緩存,實際上,一致性哈希算法的應用非常廣泛,在很多分佈式存儲系統中,都可以見到一致性哈希算法的影子。

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