哈希函數(Hash Function),也稱爲散列函數。是將一個大文件映射成一個小串字符。與指紋同樣,就是以較短的信息來保證文件的惟一性的標誌,這種標誌與文件的每個字節都相關,並且難以找到逆向規律。html
舉個例子: 前端
服務器存了10個文本文件,你如今想判斷一個新的文本文件和那10個文件有沒有一個是同樣的。你不可能去比對每一個文本里面的每一個字節,頗有可能,兩個文本文件都是5000個字節,可是隻有最後一位有所不一樣,但這樣的,你前面4999位的比較就是毫無心義。那一個解決辦法,就是在存儲那10個文本文件的時候,都將每一個文件映射成一個hash字符串。服務器只須要存儲10個hash字符串,在判斷的時候,只須要判斷新的這個文本文件的hash值是否和那10個文件的hash值一致,那就能夠解決這個問題了。算法
簡單點說,hash就是將任意長度的消息壓縮成某一固定長度的消息摘要的函數。後端
因爲文件是無限的,而映射後的字符串能表示的位數是有限的。所以可能會存在不一樣的key對應相同的Hash值。這就是存在碰撞的可能。數組
Hash算法是不可逆的,即不一樣經過Hash值逆向推出key的值。緩存
對於經典哈希函數來講,它具備如下5點性質:服務器
例如輸入域是0-98這99個數字,而咱們使用的哈希函數的輸出域爲0,1,2,當咱們將0-98這99個數字經過該哈希函數,獲得的返回值,0,1,2數量都會接近33個,不會出現某個返回值數量特別多,而某個返回值特別少。
注意:對於哈希函數來講,有規律的輸入並不能獲得有規律的輸入,例如十個1Mb的字符串,只有最後1byte的內容不同,在通過哈希函數後獲得的返回值會千差萬別,而不會有規律,因此它能夠來打亂輸入規律。網絡
一般哈希函數的輸出域都很大,例如常見的MD5算法,它的輸出域是0到264-1,可是每每咱們都會將哈希函數的返回值模上一個較小的數m,讓哈希函數的輸出域縮減爲0到m-1,而且模完後的0到m-1這個域上也是均勻分佈的。數據結構
假如你急須要1000個哈希函數,而且這1000個哈希函數都要求相互獨立,不能有相關性。這時,錯誤的方法是去在網上尋找1000個哈希函數。咱們能夠經過一個哈希函數來生成這樣的1000個獨立的哈希函數。dom
假如,你有一個哈希函數f,它的輸出域是264,也就是16字節的字符串,每一個位置上是16進制的數字0-9,a-f。
咱們將這16字節的輸出域分爲兩半,高八位和低八位是相互獨立的(這16位都相互獨立)。這樣,咱們將高八位做爲新的哈希函數f1的輸出域,低八位做爲新的哈希函數f2的輸出域,獲得兩個新的哈希函數,它們之間相互獨立。
故此能夠經過如下算式獲得1000個哈希函數:
f1+1*f2=f3 f1+2*f2=f4 f1+3*f2=f5 ……
需求:咱們有一個10TB的大文件存在分佈式文件系統上,存的是100億行字符串,而且字符串無序排列,如今咱們要統計該文件中重複的字符串。
總體思路:利用哈希函數分流,以及哈希表的性質:相同輸入致使相同輸出,不一樣輸入均勻分佈。
假設,咱們能夠調用100臺機器來計算該文件。
那麼,如今咱們須要怎樣經過哈希函數來統計重複字符串呢。
首先,咱們須要將這一百臺機器分別從0-99標好號,而後咱們在分佈式文件系統中一行行讀取文件(多臺機器並行讀取),經過哈希函數計算hashcode,將計算出的hashcode模以100,根據模出來的值,將該行存入對應的機器中。
根據哈希函數的性質,咱們很容易看出,相同的字符串會存入相同的機器中。
而後咱們就能並行100臺機器,每臺機器各自統計有哪些重複的字符串,這樣就能大大加加快統計的速度。
若是還嫌單個機器處理的數據過大,能夠把機器裏的文件再經過哈希函數按照一樣的方法把它分紅小文件,而後在一臺機器中並行多個進程,處理數據。
注意:這10TB文件並非均分紅100GB,分給100臺機器,而是將這10TB文件中不一樣字符串的種類,均分到100臺機器中。
咱們知道,哈希表中存入的數據是key,value類型的,哈希表可以put(key,value),一樣也能get(key,value)或者remove(key,value)。當咱們須要向哈希表中put(插入記錄)時,咱們將key拿出,經過哈希函數計算hashcode。假設咱們預先留下的空間大小爲17,咱們就須要將經過key計算出的hashcode模以17,獲得0-16之間的任意整數,而後咱們將記錄掛在相應位置的下面(包括key,value)。
假如咱們要將(zhangsan,20)這樣一條記錄插入哈希表中,首先咱們經過哈希函數,計算出zhangsan的hashcode,而後模以17,假如咱們獲得的值是3,哈希表會先去檢查6位置下是否存在數據。若是有,檢查該節點中的key是否等於zhangsan,若是等於,則將該節點中的value替換爲20;若是不等於,則在鏈表的最後新添加一個節點,保存咱們的記錄。
因爲哈希函數的性質,獲得的hashcode會均勻分佈在輸出域上,因此模上17以後,即使不一樣的輸入,也會在0到16上均勻分佈。這就意味着咱們哈希表每一個位置下面的鏈表長度相近。
在實際哈希表應用中,它的查詢速度近乎O(1),這是由於經過key計算hashcode的時間是常數項時間,而數組尋址的時間也是常數時間。
怎麼保證哈希表的效率爲O(1)呢?這裏的哈希表長度只有17,若是N很大的話,每條鏈的長度就是N/17,那就是O(N)的複雜度了,根本就作不到O(1)啊?
因此,當樣本量逼近一個數量,好比說我發現某條鏈的長度已經到3了,那我能夠認爲其它鏈的長度也差很少到3了,若是接下來仍是這樣操做,效率可能就不行了,此時就會經歷哈希表的擴容。
假設咱們將哈希表的範圍由原來的17擴到104,擴容的時候就是把每個數據都拿出來,從新用哈希函數算完以後,再模上104,而後從新分配在新表的哪個位置上,這樣就完成了哈希表的擴容。
你可能會說,擴容代價不用計算嗎?爲何可以作到O(1)呢?
一方面,樣本量爲N時,若是每次擴容過程當中,都讓原來的長度增長一倍,擴到N須要擴logN次;若是說每次擴容時長度增長5倍,那麼擴容的平均複雜度就是log5N,因此複雜度能夠壓得很低,並且,擴容這個代價不是時刻都發生的,雖然某一次擴容會消耗一些代價,但問題在於你是成倍擴容的,擴容一次以後可能好久都不用擴容了,因此平均下來這個複雜度就很是低了。
你又會有疑問,這也不足以說是O(1)吧?
實際在使用過程當中,還能夠離線擴容。好比說這個哈希表的長度是1000,在用的時候發現某條鏈上的長度爲5了,長度爲5並不影響使用,增刪改查仍是O(1),只是再往上加的時候它的效率快不行了,可是你在get或put時還讓你使用原來的結構,於此同時,我在後臺給你分配一個更大的區域,好比容量爲3000。一個數據通過哈希函數算完後,拿到新結構裏放,若是用戶有put行爲,就同步往新老結構上塞,用戶使用get時,從老結構上拿,也就是說不讓使用者等待。當後臺完全擴容完成後,用戶再用的時候,就把請求切換到新的結構上,而後把老結構銷燬,這就是離線擴容。
由於有這麼多的優化技巧,因此咱們說哈希表的增刪改查是O(1)的。
【題目】
設計一種結構,在該結構中有以下三個功能:insert(key):將某個key加入到該結構,作到不重複加入。delete(key):將本來在結構中的某個key移除。 getRandom():等機率隨機返回結構中的任何一個key。
要求:Insert、delete和getRandom方法的時間複雜度都是O(1)
【分析】
這個題的結構和哈希表的結構很像,不一樣的是哈希表是get(key,value),而這題沒有value,只有key,但有getRandom()這個函數。
若是用一張哈希表不能作到等機率隨機返回任何一個key,哈希表的結構是,表中的每一個位置上都掛一些鏈,若是樣本量不多,必然會出現某一個位置上有數據,其它位置沒數據的狀況,此時又不能遍歷,由於遍歷就不是O(1)了;樣本量不少的時候,雖說會均勻分佈, 每一個位置鏈的長度幾乎差很少,但也不是嚴格同樣,因此只用一張哈希表是作不到嚴格等機率返回一個key的。
準備兩個哈希表,假設A~Z依次進入,哈希表的結構就是:
若是要等機率隨機返回一個,可使用Math.random() * size
隨機產生[0,25)中等機率的一個數,隨機出哪一個數字,就在map2裏把該數字對應的字符串返回,這就能作到絕對等機率。
以上是insert(key)和getRandom()的行爲。
delete(key)又該怎麼作呢?
若是直接在map1和map2中進行刪除操做的話,會產生一個個的「洞」,若是0~999這1000個數中,執行了999個刪除操做,那麼0~999中產生了999個「洞」,只有一個位置上有數據,此時,若是getRandom()的話就會很是慢,這樣就不能保證O(1)的時間複雜度。
正確的作法應該是:假設刪除了map1中str2上的數據,map2對應的數據也會同步刪除,而後把str999放到str2的位置上,再讓str2對應的字符串改成999,而後刪掉最後一條記錄,size變爲999。
即產生「洞」的時候,拿最後一個數據「填」這個「洞」,再把最後幾個記錄刪掉,這樣就能保證size的index區域仍是連續的,此時getRandom()產生的隨機數的位置就不會爲空了。
注意:value上的0~999這個順序並非有序的,由於map自己就是亂序的,咱們也不須要value是有序的,咱們只要保證map上不存在「洞」,每條記錄都是連續的,這樣在getRandom()時就不會找不到數。
public class RandomPool { public static class Pool<K> { private HashMap<K, Integer> keyIndexMap; private HashMap<Integer, K> indexKeyMap; private int size; public Pool() { this.keyIndexMap = new HashMap<>(); this.indexKeyMap = new HashMap<>(); this.size = 0; } public void insert(K key) { if (!this.keyIndexMap.containsKey(key)) { this.keyIndexMap.put(key, this.size); this.indexKeyMap.put(this.size++, key); } } public void delete(K key) { if (this.keyIndexMap.containsKey(key)) { int deleteIndex = this.keyIndexMap.get(key); int lastIndex = --this.size; K lastKey = this.indexKeyMap.get(lastIndex); //把最後一條記錄"填"到要刪除的位置 this.keyIndexMap.put(lastKey, deleteIndex); this.indexKeyMap.put(deleteIndex, lastKey); this.keyIndexMap.remove(key); this.indexKeyMap.remove(lastIndex); } } public K getRandom() { if (this.size == 0) { return null; } //0 ~ size -1 int random = (int) (Math.random() * this.size); return this.indexKeyMap.get(random); } } public static void main(String[] args) { Pool<String> pool = new Pool<>(); pool.insert("A"); pool.insert("B"); pool.insert("C"); System.out.println(pool.getRandom()); System.out.println(pool.getRandom()); System.out.println(pool.getRandom()); System.out.println(pool.getRandom()); System.out.println(pool.getRandom()); System.out.println(pool.getRandom()); } }
在平常生活中,包括在設計計算機軟件時,咱們常常要判斷一個元素是否在一個集合中。好比在字處理軟件中,須要檢查一個英語單詞是否拼寫正確(也就是要判斷它是否在已知的字典中);在 FBI,一個嫌疑人的名字是否已經在嫌疑名單上;在網絡爬蟲裏,一個網址是否被訪問過等等。最直接的方法就是將集合中所有的元素存在計算機中,遇到一個新元素時,將它和集合中的元素直接比較便可。通常來說,計算機中的集合是用哈希表(hash table)來存儲的。它的好處是快速準確,缺點是費存儲空間。當集合比較小時,這個問題不顯著,可是當集合巨大時,哈希表存儲效率低的問題就顯現出來了。好比說,一個象 Yahoo,Hotmail 和 Gmai 那樣的公衆電子郵件(email)提供商,老是須要過濾來自發送垃圾郵件的人(spamer)的垃圾郵件。一個辦法就是記錄下那些發垃圾郵件的 email 地址。因爲那些發送者不停地在註冊新的地址,全世界少說也有幾十億個發垃圾郵件的地址,將他們都存起來則須要大量的網絡服務器。若是用哈希表,每存儲一億個 email 地址, 就須要 1.6GB 的內存。所以存貯幾十億個郵件地址可能須要上百 GB 的內存。除非是超級計算機,通常服務器是沒法存儲的。接下來,咱們介紹一種稱做布隆過濾器的數學工具,它只須要哈希表 1/8 到 1/4 的大小就能解決一樣的問題。
布隆過濾器 (Bloom Filter)是由Burton Howard Bloom於1970年提出,它是一種space efficient的機率型數據結構,用於判斷一個元素是否在集合中。在垃圾郵件過濾的黑白名單方法、爬蟲(Crawler)的網址判重模塊中等等常常被用到。哈希表也能用於判斷元素是否在集合中,可是布隆過濾器只須要哈希表的1/8或1/4的空間複雜度就能完成一樣的問題。布隆過濾器能夠插入元素,但不能夠刪除已有元素。其中的元素越多,false positive rate(誤報率)越大,可是false negative (漏報)是不可能的。即布隆過濾器能夠用來告訴你 「某樣東西必定不存在或者可能存在」。
Bloom-Filter算法的核心思想就是利用多個不一樣的Hash函數來解決「衝突」。
計算某元素x是否在一個集合中,首先能想到的方法就是將全部的已知元素保存起來構成一個集合R,而後用元素x跟這些R中的元素一一比較來判斷是否存在於集合R中;咱們能夠採用鏈表等數據結構來實現。可是,隨着集合R中元素的增長,其佔用的內存將愈來愈大。試想,若是有幾千萬個不一樣網頁須要下載,所需的內存將足以佔用掉整個進程的內存地址空間。即便用MD5,UUID這些方法將URL轉成固定的短小的字符串,內存佔用也是至關巨大的。
因而,咱們會想到用Hash table的數據結構,運用一個足夠好的Hash函數將一個URL映射到二進制位數組(位圖數組)中的某一位。若是該位已經被置爲1,那麼表示該URL已經存在。
可是Hash存在一個衝突(碰撞)的問題,用同一個Hash獲得的兩個URL的值有可能相同。爲了減小衝突,咱們能夠多引入幾個Hash,若是經過其中的一個Hash值咱們得出某元素不在集合中,那麼該元素確定不在集合中。只有在全部的Hash函數告訴咱們該元素在集合中時,才能肯定該元素存在於集合中。這即是Bloom-Filter的基本思想。
首先須要準備:一個位數組、K個獨立hash函數
1)位數組:
假設Bloom Filter使用一個m比特的數組來保存信息,初始狀態時,Bloom Filter是一個包含m位的位數組,每一位都置爲0,即BF整個數組的元素都設置爲0。
2)添加元素,k個獨立hash函數
爲了表達S={x1, x2,…,xn}這樣一個n個元素的集合,Bloom Filter使用k個相互獨立的哈希函數(Hash Function),它們分別將集合中的每一個元素映射到{1,…,m}的範圍中。2)添加元素,k個獨立hash函數
當咱們往Bloom Filter中增長任意一個元素x時候,咱們使用k個哈希函數獲得k個哈希值(能夠對應數組下標),而後將數組中對應的比特位設置爲1。即第i個哈希函數映射的位置hashi(x)就會被置爲1(1≤i≤k)。
注意,若是一個位置屢次被置爲1,那麼只有第一次會起做用,後面幾回將沒有任何效果。在下圖中,k=3,且有兩個哈希函數選中同一個位置(從左邊數第五位,即第二個「1「處)。
3)判斷元素是否存在集合
在判斷y是否屬於這個集合時,咱們只須要對y使用k個哈希函數獲得k個哈希值,若是全部hashi(y)的位置都是1(1≤i≤k),即k個位置都被設置爲1了,那麼咱們就認爲y是集合中的元素,不然就認爲y不是集合中的元素。下圖中y1就不是集合中的元素(由於y1有一處指向了「0」位)。y2或者屬於這個集合,或者恰好是一個false positive。
很顯然這個判斷並不能保證查找的結果是100%正確的。
接下來再舉個具體的例子來解釋上面過程:
準備一個布隆過濾器( bit 數組),長這樣:
Ok,咱們如今再存一個值 「tencent」,若是哈希函數返回 三、四、8 的話,圖繼續變爲:
很顯然,太小的布隆過濾器很快全部的 bit 位均爲 1,那麼查詢任何值都會返回「可能存在」,起不到過濾的目的了。布隆過濾器的長度會直接影響誤報率,布隆過濾器越長其誤報率越小。
另外,哈希函數的個數也須要權衡,個數越多則布隆過濾器 bit 位置位 1 的速度越快,且布隆過濾器的效率越低;可是若是太少的話,那咱們的誤報率會變高。
k 爲哈希函數個數,m 爲布隆過濾器長度,n 爲插入的元素個數,p 爲誤報率。
接下來介紹設計和應用布隆過濾器的方法:
實際應用時首先要先由用戶決定要插入的元素數n和但願的偏差率P。這也是一個設計完整的布隆過濾器須要用戶輸入的僅有的兩個參數,以後的全部參數將由系統計算,並由此創建布隆過濾器。
系統首先要計算須要的內存大小m bits:(若是m算出來是小數的話,向上取整若是m算出來是小數的話,向上取整)
再由m,n獲得哈希函數的個數:
當m和k向上取整肯定了後,真實的失誤率是:
在瞭解一致性哈希算法以前,最好先了解一下緩存中的一個應用場景,瞭解了這個應用場景以後,再來理解一致性哈希算法,就容易多了,也更能體現出一致性哈希算法的優勢,那麼,咱們先來描述一下這個經典的分佈式緩存的應用場景。
假設,咱們有三臺緩存服務器,用於緩存圖片,咱們爲這三臺緩存服務器編號爲0號、1號、2號,如今,有3萬張圖片須要緩存,咱們但願這些圖片被均勻的緩存到這3臺服務器上,以便它們可以分攤緩存的壓力。也就是說,咱們但願每臺服務器可以緩存1萬張左右的圖片,那麼,咱們應該怎樣作呢?若是咱們沒有任何規律的將3萬張圖片平均的緩存在3臺服務器上,能夠知足咱們的要求嗎?能夠!可是若是這樣作,當咱們須要訪問某個緩存項時,則須要遍歷3臺緩存服務器,從3萬個緩存項中找到咱們須要訪問的緩存,遍歷的過程效率過低,時間太長,當咱們找到須要訪問的緩存項時,時長多是不能被接受的,也就失去了緩存的意義,緩存的目的就是提升速度,改善用戶體驗,減輕後端服務器壓力,若是每次訪問一個緩存項都須要遍歷全部緩存服務器的全部緩存項,想一想就以爲很累,那麼,咱們該怎麼辦呢?原始的作法是對緩存項的鍵進行哈希,將hash後的結果對緩存服務器的數量進行取模操做,經過取模後的結果,決定緩存項將會緩存在哪一臺服務器上,這樣說可能不太容易理解,咱們舉例說明,仍然以剛纔描述的場景爲例,假設咱們使用圖片名稱做爲訪問圖片的key,假設圖片名稱是不重複的,那麼,咱們可使用以下公式,計算出圖片應該存放在哪臺服務器上。
hash(圖片名稱)% N
由於圖片的名稱是不重複的,因此,當咱們對同一個圖片名稱作相同的哈希計算時,得出的結果應該是不變的,若是咱們有3臺服務器,使用哈希後的結果對3求餘,那麼餘數必定是0、1或者2,沒錯,正好與咱們以前的服務器編號相同,若是求餘的結果爲0, 咱們就把當前圖片名稱對應的圖片緩存在0號服務器上,若是餘數爲1,就把當前圖片名對應的圖片緩存在1號服務器上,若是餘數爲2,同理,那麼,當咱們訪問任意一個圖片的時候,只要再次對圖片名稱進行上述運算,便可得出對應的圖片應該存放在哪一臺緩存服務器上,咱們只要在這一臺服務器上查找圖片便可,若是圖片在對應的服務器上不存在,則證實對應的圖片沒有被緩存,也不用再去遍歷其餘緩存服務器了,經過這樣的方法,便可將3萬張圖片隨機的分佈到3臺緩存服務器上了,並且下次訪問某張圖片時,直接可以判斷出該圖片應該存在於哪臺緩存服務器上,這樣就能知足咱們的需求了,咱們暫時稱上述算法爲HASH算法或者取模算法,取模算法的過程能夠用下圖表示。
可是,使用上述HASH算法進行緩存時,會出現一些缺陷,試想一下,若是3臺緩存服務器已經不能知足咱們的緩存需求,那麼咱們應該怎麼作呢?沒錯,很簡單,多增長兩臺緩存服務器不就好了,假設,咱們增長了一臺緩存服務器,那麼緩存服務器的數量就由3臺變成了4臺,此時,若是仍然使用上述方法對同一張圖片進行緩存,那麼這張圖片所在的服務器編號一定與原來3臺服務器時所在的服務器編號不一樣,由於除數由3變爲了4,被除數不變的狀況下,餘數確定不一樣,這種狀況帶來的結果就是當服務器數量變更時,全部緩存的位置都要發生改變,換句話說,當服務器數量發生改變時,全部緩存在必定時間內是失效的,當應用沒法從緩存中獲取數據時,則會向後端服務器請求數據,同理,假設3臺緩存中忽然有一臺緩存服務器出現了故障,沒法進行緩存,那麼咱們則須要將故障機器移除,可是若是移除了一臺緩存服務器,那麼緩存服務器數量從3臺變爲2臺,若是想要訪問一張圖片,這張圖片的緩存位置一定會發生改變,之前緩存的圖片也會失去緩存的做用與意義,因爲大量緩存在同一時間失效,形成了緩存的雪崩,此時前端緩存已經沒法起到承擔部分壓力的做用,後端服務器將會承受巨大的壓力,整個系統頗有可能被壓垮,因此,咱們應該想辦法不讓這種狀況發生,可是因爲上述HASH算法自己的緣故,使用取模法進行緩存時,這種狀況是沒法避免的,爲了解決這些問題,一致性哈希算法誕生了。
咱們來回顧一下使用上述算法會出現的問題。
問題1:當緩存服務器數量發生變化時,會引發緩存的雪崩,可能會引發總體系統壓力過大而崩潰(大量緩存同一時間失效)。
問題2:當緩存服務器數量發生變化時,幾乎全部緩存的位置都會發生改變,怎樣才能儘可能減小受影響的緩存呢?
其實,上面兩個問題是一個問題,那麼,一致性哈希算法可以解決上述問題嗎?
咱們如今就來了解一下一致性哈希算法。
其實,一致性哈希算法也是使用取模的方法,只是,剛纔描述的取模法是對服務器的數量進行取模,而一致性哈希算法是對2^32取模,什麼意思呢?咱們慢慢聊。
首先,咱們把二的三十二次方想象成一個圓,就像鐘錶同樣,鐘錶的圓能夠理解成由60個點組成的圓,而此處咱們把這個圓想象成由2^32個點組成的圓,示意圖以下:
圓環的正上方的點表明0,0點右側的第一個點表明1,以此類推,二、三、四、五、6……直到232-1,也就是說0點左側的第一個點表明232-1
咱們把這個由2的32次方個點組成的圓環稱爲hash環。
那麼,一致性哈希算法與上圖中的圓環有什麼關係呢?咱們繼續聊,仍然以以前描述的場景爲例,假設咱們有3臺緩存服務器,服務器A、服務器B、服務器C,那麼,在生產環境中,這三臺服務器確定有本身的IP地址,咱們使用它們各自的IP地址進行哈希計算,使用哈希後的結果對2^32取模,可使用以下公式示意。
hash(服務器A的IP地址) % 232
經過上述公式算出的結果必定是一個0到2^32-1之間的一個整數,咱們就用算出的這個整數,表明服務器A,既然這個整數確定處於0到2^32-1之間,那麼,上圖中的hash環上一定有一個點與這個整數對應,而咱們剛纔已經說明,使用這個整數表明服務器A,那麼,服務器A就能夠映射到這個環上,用下圖示意
同理,服務器B與服務器C也能夠經過相同的方法映射到上圖中的hash環中
hash(服務器B的IP地址) % 232
hash(服務器C的IP地址) % 232
假設3臺服務器映射到hash環上之後如上圖所示(固然,這是理想的狀況,咱們慢慢聊)。
好了,到目前爲止,咱們已經把緩存服務器與hash環聯繫在了一塊兒,咱們經過上述方法,把緩存服務器映射到了hash環上,那麼使用一樣的方法,咱們也能夠將須要緩存的對象映射到hash環上。
映射後的示意圖以下,下圖中的橘黃色圓形表示圖片
好了,如今服務器與圖片都被映射到了hash環上,那麼上圖中的這個圖片到底應該被緩存到哪一臺服務器上呢?上圖中的圖片將會被緩存到服務器A上,爲何呢?由於從圖片的位置開始,沿順時針方向遇到的第一個服務器就是A服務器,因此,上圖中的圖片將會被緩存到服務器A上,以下圖所示。
沒錯,一致性哈希算法就是經過這種方法,判斷一個對象應該被緩存到哪臺服務器上的,將緩存服務器與被緩存對象都映射到hash環上之後,從被緩存對象的位置出發,沿順時針方向遇到的第一個服務器,就是當前對象將要緩存於的服務器,因爲被緩存對象與服務器hash後的值是固定的,因此,在服務器不變的狀況下,一張圖片一定會被緩存到固定的服務器上,那麼,當下次想要訪問這張圖片時,只要再次使用相同的算法進行計算,便可算出這個圖片被緩存在哪一個服務器上,直接去對應的服務器查找對應的圖片便可。
剛纔的示例只使用了一張圖片進行演示,假設有四張圖片須要緩存,示意圖以下
1號、2號圖片將會被緩存到服務器A上,3號圖片將會被緩存到服務器B上,4號圖片將會被緩存到服務器C上。
通過上述描述,我想兄弟你應該已經明白了一致性哈希算法的原理了,可是話說回來,一致性哈希算法可以解決以前出現的問題嗎,咱們說過,若是簡單的對服務器數量進行取模,那麼當服務器數量發生變化時,會產生緩存的雪崩,從而頗有可能致使系統崩潰,那麼使用一致性哈希算法,可以避免這個問題嗎?咱們來模擬一遍,便可獲得答案。
假設,服務器B出現了故障,咱們如今須要將服務器B移除,那麼,咱們將上圖中的服務器B從hash環上移除便可,移除服務器B之後示意圖以下。
在服務器B未移除時,圖片3應該被緩存到服務器B中,但是當服務器B移除之後,按照以前描述的一致性哈希算法的規則,圖片3應該被緩存到服務器C中,由於從圖片3的位置出發,沿順時針方向遇到的第一個緩存服務器節點就是服務器C,也就是說,若是服務器B出現故障被移除時,圖片3的緩存位置會發生改變
可是,圖片4仍然會被緩存到服務器C中,圖片1與圖片2仍然會被緩存到服務器A中,這與服務器B移除以前並無任何區別,這就是一致性哈希算法的優勢,若是使用以前的hash算法,服務器數量發生改變時,全部服務器的全部緩存在同一時間失效了,而使用一致性哈希算法時,服務器的數量若是發生改變,並非全部緩存都會失效,而是隻有部分緩存會失效,前端的緩存仍然能分擔整個系統的壓力,而不至於全部壓力都在同一時間集中到後端服務器上。
這就是一致性哈希算法所體現出的優勢。
在介紹一致性哈希的概念時,咱們理想化的將3臺服務器均勻的映射到了hash環上,以下圖所示
可是,理想很豐滿,現實很骨感,咱們想象的與實際狀況每每不同。
在實際的映射中,服務器可能會被映射成以下模樣。
聰明如你必定想到了,若是服務器被映射成上圖中的模樣,那麼被緩存的對象頗有可能大部分集中緩存在某一臺服務器上,以下圖所示。
上圖中,1號、2號、3號、4號、6號圖片均被緩存在了服務器A上,只有5號圖片被緩存在了服務器B上,服務器C上甚至沒有緩存任何圖片,若是出現上圖中的狀況,A、B、C三臺服務器並無被合理的平均的充分利用,緩存分佈的極度不均勻,並且,若是此時服務器A出現故障,那麼失效緩存的數量也將達到最大值,在極端狀況下,仍然有可能引發系統的崩潰,上圖中的狀況則被稱之爲hash環的偏斜,那麼,咱們應該怎樣防止hash環的偏斜呢?一致性hash算法中使用"虛擬節點"解決了這個問題,咱們繼續聊。
話接上文,因爲咱們只有3臺服務器,當咱們把服務器映射到hash環上的時候,頗有可能出現hash環偏斜的狀況,當hash環偏斜之後,緩存每每會極度不均衡的分佈在各服務器上,聰明如你必定已經想到了,若是想要均衡的將緩存分佈到3臺服務器上,最好能讓這3臺服務器儘可能多的、均勻的出如今hash環上,可是,真實的服務器資源只有3臺,咱們怎樣憑空的讓它們多起來呢,沒錯,就是憑空的讓服務器節點多起來,既然沒有多餘的真正的物理服務器節點,咱們就只能將現有的物理節點經過虛擬的方法複製出來,這些由實際節點虛擬複製而來的節點被稱爲"虛擬節點"。加入虛擬節點之後的hash環以下。
"虛擬節點"是"實際節點"(實際的物理服務器)在hash環上的複製品,一個實際節點能夠對應多個虛擬節點。
從上圖能夠看出,A、B、C三臺服務器分別虛擬出了一個虛擬節點,固然,若是你須要,也能夠虛擬出更多的虛擬節點。引入虛擬節點的概念後,緩存的分佈就均衡多了,上圖中,1號、3號圖片被緩存在服務器A中,5號、4號圖片被緩存在服務器B中,6號、2號圖片被緩存在服務器C中,若是你還不放心,能夠虛擬出更多的虛擬節點,以便減少hash環偏斜所帶來的影響,虛擬節點越多,hash環上的節點就越多,緩存被均勻分佈的機率就越大。
參考:http://www.javashuo.com/article/p-axexgnxk-kw.html
http://www.javashuo.com/article/p-swsemhsp-dx.html
https://www.jianshu.com/p/8004c2d2ad59
https://china.googleblog.com/2007/07/bloom-filter_7469.html
http://www.cnblogs.com/zengdan-develpoer/p/4425167.html
https://www.cnblogs.com/allensun/archive/2011/02/16/1956532.html