1. 程式人生 > 其它 >有趣的演算法(四)——一致性Hash演算法模擬redis叢集

有趣的演算法(四)——一致性Hash演算法模擬redis叢集

有趣的演算法(四)——一致性Hash演算法模擬redis叢集

(原創內容,轉載請註明來源,謝謝)

一、概述

redis的叢集,對key儲存在哪個伺服器的問題上,採用的是一致性hash的原理。本文試著實現一致性hash演算法, 以模擬redis的叢集。

一致性hash是一種分散式雜湊(DHT)實現演算法,設計目標是為了解決因特網中的熱點(Hot spot)問題,在memcache和redis中也使用廣泛。

當redis採用叢集(cluster)時,對於伺服器的節點分配和資料儲存位置就是採用一致性hash的方式來實現。

redis的一致性hash如下圖所示:

1、轉換

假設一個圓圈,上面均勻分佈了0-232-1。當redis的叢集伺服器數量是n,就通過某種hash演算法,將這n臺伺服器轉換成數字,放置到圓環中,當作節點。

此時,客戶端需要設定key=>value時,把key也通過同一個hash演算法,轉換成數字,也分佈到這個圓環上,再通過順時針的 方式,將key儲存到與其最接近的一個節點中。

如上圖所示,三個綠色的節點就是三臺伺服器經過hash,被轉換到圓環上後的值;黃色的點是key經過hash轉換到圓環上的值。則上圖的key1順時針最近的節點是node1,其就儲存在node1上;同理,key3儲存在node2;key2和key4儲存在node3。

2、變動

如果此時任意一臺伺服器宕機,也只是部分資料減少,再來的資料仍可以儲存在其順時針最近的伺服器上。如果新增伺服器,則也是部分資料無法找到,但後續新增的資料仍按規則進行儲存。

3、注意

需要注意的是,如果hash演算法設計的不好,伺服器都集中在圓圈的一小部分,則會有大量的資料儲存在個別伺服器,而很多伺服器又空閒。當某個承載巨大的伺服器宕機,會發生雪崩現象。

為避免此情況,在上述的基礎上,如果伺服器轉成hash後的節點太集中,還需要採用虛擬節點的方式。

如上圖所示,假設伺服器上述key1、key2、key4,則圓的另外大半邊都沒有節點,按照概率大部分的資料將儲存在key2。這是需要避免的現象。因此,就可以製造虛擬節點,可以讓node1、key3、node2作為key1、key2、key4的虛擬節點。

二、設計

1)hash函式轉換數字

hash函式用於將伺服器轉換成數字,也用於將key轉換成數字。採用php的crc32函式,可以將任意字串轉換成10位的數字,而232和1010接近,因此採用此方式。

         //將字串轉成10位數字,取正數
         privatefunction changeStrToNum($str){
                   returnabs(crc32($str));
         }

2)伺服器轉換

對於伺服器,採用md5(伺服器ip:埠號:虛擬序號),虛擬序號從0開始到預定的虛擬數量。

         publicfunction setVitualServers(array $servers){
                   //所有主機一起從0生成到vitualnum
                   for($i=0;$i<$this->vitualNum;$i++){
                            foreach($serversas $server){
                                     $tmpStr= $server['ip'] . ':' . $server['port'] . ':' . $i;
                                     $tmpNum= $this->changeStrToNum($tmpStr);
                                     $index= $server['ip'] . ':' . $server['port'];
                                     //避免hash後重復
                                     if(!in_array($tmpNum,$this->allCrcServers)){
                                               $this->servers[$tmpNum][]= $index;
                                               array_push($this->allCrcServers,$tmpNum);
                                     }
                            }
                   }
                   //對allcrcservers排序,從低到高排序
                   sort($this->allCrcServers);
                   return$this;
         }

3)鍵轉換

對於鍵,則是採用crc32將其直接轉成數字。

         publicfunction setHashedKey($str){
                   if(!is_array($str)){
                            $this->keys[$str]= $this->changeStrToNum($str);
                   }else{
                            foreach($stras $s){
                                     $this->keys[$s]= $this->changeStrToNum($s);
                            }
                   }
                   sort($this->keys);
                   return$this;
         }

4)注意事項

其中,無論是伺服器還是鍵,轉換完都呼叫php的sort進行從小到大的排序,以便於後續的查詢。

5)獲取key對應的server

         publicfunction ensureKeyServer(){
                   if(empty($this->servers)|| empty($this->keys)){
                            returnnull;
                   }
                   $start= 0;
                   $length= count($this->allCrcServers);
                   foreach($this->keysas $key){
                            $keyToServer= $this->getKeyServer($key, $start, $length-1);
                            //如果比最大值還大,說明其應設定為第一個伺服器
                            if(null== $keyToServer){
                                     $keyToServer= 0;
                            }
                            $this->keyToServer[$key]= $keyToServer;
                   }
                   return$this->keyToServer;
         }

6)二分法,快速判斷key對於的server是哪個

         privatefunction getKeyServer($key, $start, $length){
                   if(1== $length){
                            return$this->allCrcServers[$start];
                   }
                   if(2== $length){
                            $start= $key <= $this->allCrcServers[$start] ? $start : ($start +1);
                            return$this->allCrcServers[$start];
                   }
                   $mid= floor($length/2);
                   if($key<= $this->allCrcServers[$start+$mid-1]){
                            return$this->getKeyServer($key, $start, $mid);
                   }
                   return$this->getKeyServer($key, $start+$mid, $length-$mid);
         }

7)呼叫

$servers = array(
         array('ip'=>'127.0.0.1','port'=>'5678'),
         array('ip'=>'127.0.0.1','port'=>'6789'),
         array('ip'=>'127.0.0.1','port'=>'7890'),
);
$keys = array(
         'key1',
         'key2',
         'key3',
         'key4',
         'key5'
);
$hash = new ConsistencyHash();
$hash->setVitualServers($servers)->setHashedKey($keys);
var_dump($hash->ensureKeyServer());

8)結果(瀏覽器輸出)

array(5) {
[724672585]=> int(725715703)
[744252496]=>int(745411194)
[1034812036]=>int(1035024362)
[1252706838]=> int(1253443456)
[1547079903]=> int(1548139105)
}

——written by linhxx 2017.08.22