10分鐘瞭解一致性hash演算法

成天發表於2019-08-06

應用場景

當我們的資料表超過500萬條或更多時,我們就會考慮到採用分庫分表;當我們的系統使用了一臺快取伺服器還是不能滿足的時候,我們會使用多臺快取伺服器,那我們如何去訪問背後的庫表或快取伺服器呢,我們肯定不會使用迴圈或者隨機了,我們會在存取的時候使用相同的雜湊演算法定位到具體的位置。

簡單的雜湊演算法

我們可以根據某個欄位(比如id)取模,然後將資料分散到不同的資料庫或表中。

例如前期規劃,我們某個業務資料5個庫就能滿足了,根據id取模 如下圖

我們通過hash取模很方便的路由到對應的庫上,但是上述的簡單的hash演算法還是有一些缺陷的,假如,5個庫也無法滿足業務的時候,我們需要9個庫,那麼原來的取模公式mod 5要變成 mod 9了,並且大部分資料都要重新分佈,涉及到資料轉移工作量也是巨大的。有沒有一勞永逸的方法,答案是有的一致性hash演算法

一致性雜湊演算法

演算法概述

一致性雜湊演算法(Consistent Hashing),是MIT的karge及其合作者在1997年發表的學術論文提出的,最早在論文《Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web》中被提出。簡單來說,一致性雜湊將整個雜湊值空間組織成一個虛擬的圓環,如假設某雜湊函式H的值空間為0 - 2^32-1(即雜湊值是一個32位無符號整形),整個雜湊空間環如下:

伺服器(ip或者主機名)本身進行雜湊,確認每臺機器在雜湊環上的位置,例如ip:192.168.4.101,192.168.4.102,192.168.4.103 分別對應節點node1-101,node2-102,node3-103 如圖

 

資料key使用相同的函式計算出雜湊值h,根據h確定此資料在環上的位置,從此位置沿環順時針“行走”,最近的伺服器就是其應該定位到的伺服器。 例如 我們使用"10","11","12","13","14" 四個資料物件對應key10,key11,key12,key13,key14,經過雜湊計算後,在環空間的位置如下:

 

根據一致性雜湊演算法,資料key10,key14會被定位到節點node3-103上,key12,key13被定位到節點node1-10上,而key11會被定位到節點node2-102上。

擴充套件性
節點新增

如果我們新增一個節點node4-104 對應的ip:192.168.4.104通過對應的雜湊演算法得到雜湊值,並對映到環中,如下圖

通過按順時針遷移的規則,那麼key10被遷移到了node4-104中,其它資料還保持這原有的儲存位置

節點刪除

如果刪除一個節點node3-103,那麼按照順時針遷移的方法,key10,key14將會被遷移到node1-10上,其它的物件沒有任何的改動。如下圖:

如果服務節點太少的時候,會出現資料分配不均,比如極端情況下所有資料都落到node1-101節點上,如何解決資料傾斜問題,需要引入虛擬節點

虛擬節點

如果節點比較少的情況下,在0到2^32-1形成的環中,會出每個節點存放的資料不均勻;一致性雜湊演算法提出虛擬節點的解決方案。即虛擬節點時實際節點(物理機器)在hash環中的複製品,一個實際節點對應N多個虛擬節點,這個對應個數也成為了複製個數,虛擬節點在hash環中以hash值排列。

例如 我們以刪除了一個點,只剩下 node1 和node2 兩個節點的圖;我們新增4個虛擬節點,兩個節點 則對應8個節點,最後對映關係 如圖

核心程式碼
 public class KetamaNodeLocator
    {
        private SortedList<long, string> ketamaNodes = new SortedList<long, string>();
        private HashAlgorithm hashAlg;
        private int numReps = 160;

        public KetamaNodeLocator(List<string> nodes, int nodeCopies)
        {
            ketamaNodes = new SortedList<long, string>();

            numReps = nodeCopies;
            //對所有節點,生成nCopies個虛擬結點
            foreach (string node in nodes)
            {
                //每四個虛擬結點為一組
                for (int i = 0; i < numReps / 4; i++)
                {
                    //getKeyForNode方法為這組虛擬結點得到惟一名稱 
                    byte[] digest = HashAlgorithm.computeMd5(node + i);
                    /** Md5是一個16位元組長度的陣列,將16位元組的陣列每四個位元組一組,分別對應一個虛擬結點,這就是為什麼上面把虛擬結點四個劃分一組的原因*/
                    for (int h = 0; h < 4; h++)
                    {
                        long m = HashAlgorithm.hash(digest, h);
                        ketamaNodes[m] = node;
                    }
                }
            }
        }

        public string GetPrimary(string k)
        {
            byte[] digest = HashAlgorithm.computeMd5(k);
            string rv = GetNodeForKey(HashAlgorithm.hash(digest, 0));
            return rv;
        }

        string GetNodeForKey(long hash)
        {
            string rv;
            long key = hash;
            //如果找到這個節點,直接取節點,返回   
            if (!ketamaNodes.ContainsKey(key))
            {
                //得到大於當前key的那個子Map,然後從中取出第一個key,就是大於且離它最近的那個key 說明詳見: http://www.javaeye.com/topic/684087
                var tailMap = from coll in ketamaNodes
                              where coll.Key > hash
                              select new { coll.Key };
                if (tailMap == null || tailMap.Count() == 0)
                    key = ketamaNodes.FirstOrDefault().Key;
                else
                    key = tailMap.FirstOrDefault().Key;
            }
            rv = ketamaNodes[key];
            return rv;
        }
    }
public class HashAlgorithm
    {
        public static long hash(byte[] digest, int nTime)
        {
            long rv = ((long)(digest[3 + nTime * 4] & 0xFF) << 24)
                    | ((long)(digest[2 + nTime * 4] & 0xFF) << 16)
                    | ((long)(digest[1 + nTime * 4] & 0xFF) << 8)
                    | ((long)digest[0 + nTime * 4] & 0xFF);

            return rv & 0xffffffffL; /* Truncate to 32-bits */
        }

        /**
         * Get the md5 of the given key.
         */
        public static byte[] computeMd5(string k)
        {
            MD5 md5 = new MD5CryptoServiceProvider();
           
            byte[] keyBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(k));
            md5.Clear();
            //md5.update(keyBytes);
            //return md5.digest();
            return keyBytes;
        }

最後貼上了實現程式碼,可以執行跑跑,加深理解,希望對您有所幫助,碼字不易請多多支援。

參考

代震軍----https://www.cnblogs.com/daizhj/archive/2010/08/24/1807324.html

相關文章