hash算法原理及應用漫談【加圖版】

原文:http://www.javashuo.com/article/p-geygixkm-ge.htmljava

提到hash,相信大多數同窗都不會陌生,以前很火如今也依舊很火的技術區塊鏈背後的底層原理之一就是hash,下面就從hash算法的原理和實際應用等幾個角度,對hash算法進行一個講解。git

一、什麼是Hash算法

Hash也稱散列、哈希,對應的英文都是Hash。基本原理就是把任意長度的輸入,經過Hash算法變成固定長度的輸出。這個映射的規則就是對應的Hash算法,而原始數據映射後的二進制串就是哈希值。活動開發中常用的MD5和SHA都是歷史悠久的Hash算法。數據庫

echo md5(「這是一個測試文案」);數組

// 輸出結果:2124968af757ed51e71e6abeac04f98d緩存

在這個例子裏,這是一個測試文案是原始值,2124968af757ed51e71e6abeac04f98d就是通過hash算法獲得的Hash值。整個Hash算法的過程就是把原始任意長度的值空間,映射成固定長度的值空間的過程。安全

二、Hash的特色服務器

一個優秀的hash算法,須要什麼樣的要求呢?網絡

  • a)、從hash值不能夠反向推導出原始的數據
  • 這個從上面MD5的例子裏能夠明確看到,通過映射後的數據和原始數據沒有對應關係
  • b)、輸入數據的微小變化會獲得徹底不一樣的hash值,相同的數據會獲得相同的值echo md5(「這是一個測試文案」);
  • // 輸出結果:2124968af757ed51e71e6abeac04f98d
  • echo md5(「這是二個測試文案」);
  • // 輸出結果:bcc2a4bb4373076d494b2223aef9f702
  • 能夠看到咱們只改了一個文字,可是整個獲得的hash值產生了很是大的變化。
  • c)、哈希算法的執行效率要高效,長的文本也能快速地計算出哈希值
  • d)、hash算法的衝突機率要小因爲hash的原理是將輸入空間的值映射成hash空間內,而hash值的空間遠小於輸入的空間。根據抽屜原理,必定會存在不一樣的輸入被映射成相同輸出的狀況。那麼做爲一個好的hash算法,就須要這種衝突的機率儘量小。

桌上有十個蘋果,要把這十個蘋果放到九個抽屜裏,不管怎樣放,咱們會發現至少會有一個抽屜裏面放很多於兩個蘋果。這一現象就是咱們所說的「抽屜原理」。抽屜原理的通常含義爲:「若是每一個抽屜表明一個集合,每個蘋果就能夠表明一個元素,假若有n+1個元素放到n個集合中去,其中一定有一個集合裏至少有兩個元素。」 抽屜原理有時也被稱爲鴿巢原理。它是組合數學中一個重要的原理數據結構

三、Hash碰撞的解決方案

前面提到了hash算法是必定會有衝突的,那麼若是咱們若是遇到了hash衝突須要解決的時候應該怎麼處理呢?比較經常使用的算法是鏈地址法和開放地址法。

3.1 鏈地址法

鏈表地址法是使用一個鏈表數組,來存儲相應數據,當hash遇到衝突的時候依次添加到鏈表的後面進行處理。

hash 算法原理及應用漫談

鏈地址法示意圖

鏈地址在處理的流程以下:

添加一個元素的時候,首先計算元素key的hash值,肯定插入數組中的位置。若是當前位置下沒有重複數據,則直接添加到當前位置。當遇到衝突的時候,添加到同一個hash值的元素後面,行成一個鏈表。這個鏈表的特色是同一個鏈表上的Hash值相同。java的數據結構HashMap使用的就是這種方法來處理衝突,JDK1.8中,針對鏈表上的數據超過8條的時候,使用了紅黑樹進行優化。因爲篇幅緣由,這裏不深刻討論相關數據結構,有興趣的同窗能夠參考這篇文章:

《Java集合之一—HashMap》

3.2 開放地址法

開放地址法是指大小爲 M 的數組保存 N 個鍵值對,其中 M > N。咱們須要依靠數組中的空位解決碰撞衝突。基於這種策略的全部方法被統稱爲「開放地址」哈希表。線性探測法,就是比較經常使用的一種「開放地址」哈希表的一種實現方式。線性探測法的核心思想是當衝突發生時,順序查看錶中下一單元,直到找出一個空單元或查遍全表。簡單來講就是:一旦發生衝突,就去尋找下 一個空的散列表地址,只要散列表足夠大,空的散列地址總能找到。

線性探測法的數學描述是:h(k, i) = (h(k, 0) + i) mod m,i表示當前進行的是第幾輪探查。i=1時,便是探查h(k, 0)的下一個;i=2,便是再下一個。這個方法是簡單地向下探查。mod m表示:到達了表的底下以後,回到頂端從頭開始。

對於開放尋址衝突解決方法,除了線性探測方法以外,還有另外兩種比較經典的探測方法,二次探測(Quadratic probing)和雙重散列(Double hashing)。可是無論採用哪一種探測方法,當散列表中空閒位置很少的時候,散列衝突的機率就會大大提升。爲了儘量保證散列表的操做效率,通常狀況下,咱們會盡量保證散列表中有必定比例的空閒槽位。咱們用裝載因子(load factor)來表示空位的多少。

散列表的裝載因子=填入表中的元素個數/散列表的長度。裝載因子越大,說明衝突越多,性能越差。

3.3 兩種方案的demo示例

假設散列長爲8,散列函數H(K)=K mod 7,給定的關鍵字序列爲{32,14,23,2, 20}

當使用鏈表法時,相應的數據結構以下圖所示:

hash 算法原理及應用漫談

鏈表法demo

當使用線性探測法時,相應的數據結果以下圖所示:

hash 算法原理及應用漫談

開放地址-線性探測法

這裏的兩種算法的區別是2這個元素,在鏈表法中仍是在節點2的位置上,可是在線性探測法遇到衝突時會將衝突數據放到下一個空的位置下面。

四、hash算法在平常活動中的應用

在平常運營活動中,咱們活動開發常常遇到的應用場景是信息加密、數據校驗、負載均衡。下面分別對這三種應用場景進行講解。

4.1 信息加密

首先咱們看一下信息加密的應用。2011年CSDN脫庫事件,致使超過600W的用戶的密碼泄露,讓人失望的是,CSDN是明文存儲用戶的註冊郵箱和密碼的。做爲用戶的很是隱私的信息,最簡單的保護措施就是對密碼進行hash加密。在客戶端對用戶輸入的密碼進行hash運算,而後在服務端的數據庫中保存用戶密碼的hash值。因爲服務器端也沒有存儲密碼的明文,因此目前不少網站也就再也不有找回密碼的功能了。

  • 這裏也友情提示一下你們:若是在使用中發現某網站還有提供找回密碼的功能,就要好好擔憂下這個網站的安全性了。

看到這裏有些同窗會以爲那麼咱們是否是對用戶輸入的密碼進行一次MD5加密就能夠了呢,這樣就算惡意用戶知道了hash值,也沒有辦法拿到用戶的真實密碼。假設用戶的密碼是123456789,通過一次md5之後獲得的值是:

25f9e794323b453885f5181f1b624d0b

那麼是否是使用了這個加密後的字符串來存密碼就萬無一失了呢,理想老是很豐滿,而現實老是很骨感的。

你們能夠看一下這個網站:

https://www.cmd5.com/

這裏是該網站的相關介紹:

本站針對md五、sha1等全球通用公開的加密算法進行反向查詢,經過窮舉字符組合的方式,建立了明文密文對應查詢數據庫,建立的記錄約90萬億條,佔用硬盤超過500TB,查詢成功率95%以上,不少複雜密文只有本站纔可查詢。已穩定運行十餘年,國內外享有盛譽

hash 算法原理及應用漫談

md5反查結果

那麼通常針對這種問題,咱們的解決之道就是引入salt(加鹽),即利用特殊字符(鹽)和用戶的輸入合在一塊兒組成新的字符串進行加密。經過這樣的方式,增長了反向查詢的複雜度。可是這樣的方式也不是萬無一失,若是發生了鹽被泄露的問題,就須要全部用到的地方來重置密碼。

針對salt泄露的問題,其實還有一種解決辦法,即便用HMAC進行加密(Hash-based Message Authentication Code)。這種算法的核心思路是加密使用的key是從服務器端獲取的,每個用戶的是不同的。若是發生了泄露,那麼也就是這一個用戶的會被泄露,不會影響到全局。

這裏也留給你們一個思考點,若是惡意用戶直接抓取了你的活動參與連接,也就是拿到了你計算後的hash值,那從技術的角度上說,咱們還有沒有其餘能夠提高惡意用戶的違法成本呢?

4.2 數據校驗

– git commit id

使用過git的同窗都應該清楚,每次git提交後都有一個commit id,好比:

19d02d2cc358e59b3d04f82677dbf3808ae4fc40

就是一次git commit的結果,那麼這個id是如何生成出來的呢?查閱了相關資料,使用以下代碼能夠進行查看:

printf 「commit %s0」 $(git cat-file commit HEAD | wc -c); git cat-file commit HEAD

git的commit id主要包括瞭如下幾部份內容:Tree 哈希,parent哈希、做者信息和本次提交的備註。

hash 算法原理及應用漫談

單次git commit相關信息

針對這些信息進行SHA-1 算法後獲得值就是本次提交的commit id。簡單來說,就是對於單次提交的頭信息的一個校驗和。

Linux kernel開創者和Git的開發者——Linus說,Git使用了sha1並不是是爲了安全性,而是爲了數據的完整性;它能夠保證,在不少年後,你從新checkout某個commit時,必定是它多年前的當時的狀態,徹底一摸同樣,徹底值得信任。

但最新研究代表,理論上對其進行哈希碰撞(hash collision,不一樣的兩塊數據有相同的hash值)的攻擊能夠在2^51(2的51次方)左右的次數內實現。不過因爲commit id 是針對單個倉庫裏的,因此實際應用中咱們能夠認爲若是兩個文件的SHA-1值是相同的,那麼它們確是徹底相同的內容。

注:對於git裏tree、parent等結構感興趣的同窗,能夠參考下這篇文章《Git 內部原理 – Git 對象》,這裏因爲篇幅緣由就不進行深刻分析了。

  • 版權校驗
  • 在數據校驗方面的另外一個應用場景就是版權的保護或者違禁信息的打擊,好比某個小視頻,第一個用戶上傳的時候,咱們認爲是版權全部者,計算一個hash值存下來。當第二個用戶上傳的時候,一樣計算hash值,若是hash值同樣的話,就算同一個文件。這種方案其實也給用戶傳播違禁文件提升了一些門檻,不是簡單的換一個名字或者改一下後綴名就能夠躲避掉打擊了。(固然這種方式也是能夠繞過的,圖片的你隨便改一下顏色,視頻去掉一幀就又是徹底不一樣的hash值了。注意:我沒有教你變壞,我只是和你在討論這個技術。。。)另外咱們在社區裏,也會遇到玩家重複上傳同一張圖片或者視頻的狀況,使用這種校驗的方式,能夠有效減小cos服務的存儲空間。
  • 大文件分塊校驗
  • 使用過bt的同窗都有經驗,在p2p網絡中會把一個大文件拆分紅不少小的數據各自傳輸。這樣的好處是若是某個小的數據塊在傳輸過程當中損壞了,只要從新下載這個塊就好。爲了確保每個小的數據塊都是發佈者本身傳輸的,咱們能夠對每個小的數據塊都進行一個hash的計算,維護一個hash List,在收到全部數據之後,咱們對於這個hash List裏的每一塊進行遍歷比對。這裏有一個優化點是若是文件分塊特別多的時候,若是遍歷對比就會效率比較低。能夠把全部分塊的hash值組合成一個大的字符串,對於這個字符串再作一次Hash運算,獲得最終的hash(Root hash)。在實際的校驗中,咱們只須要拿到了正確的Root hash,便可校驗Hash List,也就能夠校驗每個數據塊了。
hash 算法原理及應用漫談

大文件分塊示意圖

4.3 負載均衡

活動開發同窗在應對高星級業務大用戶量參與時,都會使用分庫分表,針對用戶的openid進行hashtime33取模,就能夠獲得對應的用戶分庫分表的節點了。

hash 算法原理及應用漫談

活動分庫分表示意圖

如上圖所示,這裏實際上是分了10張表,openid計算後的hash值取模10,獲得對應的分表,在進行後續處理就好。對於通常的活動或者系統,咱們通常設置10張表或者100張表就好。

下面咱們來看一點複雜的問題,假設咱們活動初始分表了10張,運營一段時間之後發現須要10張不夠,須要改到100張。這個時候咱們若是直接擴容的話,那麼全部的數據都須要從新計算Hash值,大量的數據都須要進行遷移。若是更新的是緩存的邏輯,則會致使大量緩存失效,發生雪崩效應,致使數據庫異常。形成這種問題的緣由是hash算法自己的緣故,只要是取模算法進行處理,則沒法避免這種狀況。針對這種問題,咱們就須要利用一致性hash進行相應的處理了。

一致性hash的基本原理是將輸入的值hash後,對結果的hash值進行2^32取模,這裏和普通的hash取模算法不同的點是在一致性hash算法裏將取模的結果映射到一個環上。將緩存服務器與被緩存對象都映射到hash環上之後,從被緩存對象的位置出發,沿順時針方向遇到的第一個服務器,就是當前對象將要緩存於的服務器,因爲被緩存對象與服務器hash後的值是固定的,因此,在服務器不變的狀況下,一個openid一定會被緩存到固定的服務器上,那麼,當下次想要訪問這個用戶的數據時,只要再次使用相同的算法進行計算,便可算出這個用戶的數據被緩存在哪一個服務器上,直接去對應的服務器查找對應的數據便可。這裏的邏輯其實和直接取模的是同樣的。以下圖所示:

hash 算法原理及應用漫談

初始3臺機器的狀況

初始狀況以下:用戶1的數據在服務器A裏,用戶二、3的數據存在服務器C裏,用戶4的數據存儲在服務器B裏

下面咱們來看一下當服務器數量發生變化的時候,相應影響的數據狀況:

  • 服務器縮容
hash 算法原理及應用漫談

服務器縮容

服務器B發生了故障,進行剔除後,只有用戶4的數據發生了異常。這個時候咱們須要繼續按照順時針的方案,把緩存的數據放在用戶A上面。

  • 服務器擴容
  • 一樣的,咱們進行了服務器擴容之後,新增了一臺服務器D,位置落在用戶2和3之間。按照順時針原則,用戶2依然訪問的是服務器C的數據,而用戶3順時針查詢後,發現最近的服務器是D,後續數據就會存儲到d上面。
hash 算法原理及應用漫談

服務器擴容示意圖

  • 虛擬節點
  • 固然這只是一種理想狀況,實際使用中,因爲服務器節點數量有限,有可能出現分佈不均勻的狀況。這個時候會出現大量數據都被映射到某一臺服務器的狀況,以下圖左側所示。爲了解決這個問題,咱們採用了虛擬節點的方案。虛擬節點是實際節點(實際的物理服務器)在hash環上的複製品,一個實際節點能夠對應多個虛擬節點。虛擬節點越多,hash環上的節點就越多,數據被均勻分佈的機率就越大。
hash 算法原理及應用漫談

虛擬節點示意圖

 

 

如右圖所示,B、C、D 是原始節點複製出來的虛擬節點,本來都要訪問機器D的用戶一、4,分別被映射到了B,D。經過這樣的方式,起到了一個服務器均勻分佈的做用。

五、幾種hash算法的擴展應用

下面介紹幾種你們可能不常常遇到的應用,因爲篇幅緣由,不作深刻介紹,只拋磚引玉。

5.1 SimHash

simHash是google用於海量文本去重的一種方法,它是一種局部敏感hash。那什麼叫局部敏感呢,假定兩個字符串具備必定的類似性,在hash以後,仍然能保持這種類似性,就稱之爲局部敏感hash。普通的hash是不具備這種屬性的。simhash被Google用來在海量文本中去重。

simHash算法的思路大體以下:

  • 將Doc進行關鍵詞抽取(其中包括分詞和計算權重),抽取出n個(關鍵詞,權重)對, 即圖中的多個(feature, weight)。記爲 feature_weight_pairs = [fw1, fw2 … fwn],其中 fwn = (feature_n,weight_n)。
  • 對每一個feature_weight_pairs中的feature進行hash。而後對hash_weight_pairs進行位的縱向累加,若是該位是1,則+weight,若是是0,則-weight,最後生成bits_count個數字,大於0標記1,小於0標記0
  • 最後轉換成一個64位的字節,判斷重複只須要判斷他們的特徵字的距離是否是
hash 算法原理及應用漫談

SimHash計算流程

以下圖所示,當兩個文本只有一個字變化時,若是使用普通Hash則會致使兩次的結果發生較大改變,而SimHash的局部敏感特性,會致使只有部分數據發生變化。

hash 算法原理及應用漫談

SimHash結果

5.2 GeoHash

GeoHash將地球做爲爲一個二維平面進行遞歸分解。每一個分解後的子塊在必定經緯度範圍內擁有相同的編碼。如下圖爲例,這個矩形區域內全部的點(經緯度座標)都共享相同的GeoHash字符串,這樣既能夠保護隱私(只表示大概區域位置而不是具體的點),又比較容易作緩存。

hash 算法原理及應用漫談

GeoHash示意圖

下面以一個例子來理解下這個算法,咱們對緯度39.3817進行逼近編碼 :

  • 地球緯度區間是[-90,90],對於這個區間進行二分劃分左區間[-90,0), 右區間[0,90]。39.3817屬於右區間,標記爲1
  • 將右區間[0,90]繼續進行劃分,左區間[0,45) ,右區間[45,90]。39.3817屬於左區間,標記爲0
  • 遞歸上面的過程,隨着每次迭代,區間[a,b]會不斷接近39.3817。遞歸的次數決定了生成的序列長度。
  • 對於經度作一樣的處理。獲得的字符串,偶數位放經度,奇數位放緯度,把2串編碼組合生成新串。對於新串轉成對應10進制查出實際的base32編碼就是相似WX4ER的hash值。

總體遞歸過程以下表所示:

hash 算法原理及應用漫談

這裏有一篇文章詳細介紹了GeoHash,有興趣的同窗能夠移步這裏:

是什麼能讓 APP 快速精準定位到咱們的位置?

5.3 布隆過濾器

布隆過濾器被普遍用於黑名單過濾、垃圾郵件過濾、爬蟲判重系統以及緩存穿透問題。對於數量小,內存足夠大的狀況,咱們能夠直接用hashMap或者hashSet就能夠知足這個活動需求了。可是若是數據量很是大,好比5TB的硬盤上放滿了用戶的參與數據,須要一個算法對這些數據進行去重,取得活動的去重參與用戶數。這種時候,布隆過濾器就是一種比較好的解決方案了。

布隆過濾器實際上是基於bitmap的一種應用,在1970年由布隆提出的。它其實是一個很長的二進制向量和一系列隨機映射函數,用於檢索一個元素是否在一個集合中。它的優勢是空間效率和查詢時間都遠遠超過通常的算法,缺點是有必定的誤識別率和刪除困難,主要用於大數據去重、垃圾郵件過濾和爬蟲url記錄中。核心思路是使用一個bit來存儲多個元素,經過這樣的方式來減小內存的消耗。經過多個hash函數,將每一個數據都算出多個值,存放在bitmap中對應的位置上。

布隆過濾器的原理見下圖所示:

hash 算法原理及應用漫談

布隆過濾器原理示意

上圖所示的例子中,數據a、b、c通過三次hash映射後,對應的bit位都是1,表示這三個數據已經存在了。而d這份數據通過映射後有一個結果是0,則代表d這個數據必定沒有出現過。布隆過濾器存在假陽率(斷定存在的元素可能不存在)的問題,可是沒有假陰率(判斷不存在的緣由可能存在)的問題。即對於數據e,三次映射的結果都是1,可是這份數據也可能沒有出現過。

誤判率的數據公式以下所示:

hash 算法原理及應用漫談

其中,p是誤判率,n是容納的元素,m是須要的存儲空間。由公示能夠看出,布隆過濾器的長度會直接影響誤報率,布隆過濾器越長其誤報率越小。哈希函數的個數也須要權衡,個數越多則布隆過濾器 bit 位置位 1 的速度越快,且布隆過濾器的效率越低;可是若是太少的話,則會致使誤報率升高。

六、總結

Hash算法做爲一種活動開發常常遇到的算法,咱們在使用中不只僅要知道這種算法背後真正的原理,才能夠在使用上作到有的放矢。Hash的相關知識還有不少,有興趣的同窗能夠繼續深刻研究。

相關文章
相關標籤/搜索