有趣的演算法(四)——一致性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