一致性hash介紹和實現
http://www.cnblogs.com/zhangshiwen/p/5787232.html , 看了幾篇一致性hash的介紹, 這邊理論講的比較少,但是結合實際例子,容易理解。 mark.
像Memcache以及其它一些記憶體K/V資料庫一樣,Redis本身不提供分散式支援,所以在部署多臺Redis伺服器時,就需要解決如何把資料分散到各個伺服器的問題,並且在伺服器數量變化時,能做到最大程度的不令資料重新分佈。
通常使用的分散式方法是根據所要儲存資料的鍵的hash值與伺服器數量N,按 hash % N 取模的演算法來將資料分佈到各個伺服器。該演算法的優點是足夠簡單,而且資料分佈均勻。但是一旦伺服器數量N發生變化的時候,快取命中率會瞬間跌入谷底,因為 絕大多數的資料需要重新分佈。而且對於大型網站來說,此時會有巨大的壓力湧向後端服務,可能會導致效能故障和服務故障,甚至宕機。
本文介紹什麼是一致性雜湊,為什麼要使用以及如何使用一致性雜湊演算法實現Redis分散式部署,並且引入了虛擬節點以提高資料均勻分佈的平衡性。最後還提供了一個故障轉移策略,保證在部分伺服器故障的時候能迅速恢復命中率,提高Redis服務的可用性和穩定性。
一致性雜湊
由於hash演算法結果一般為unsigned int型,因此對於hash函式的結果應該均勻分佈在[0,2^32-1]區間,如果我們把一個圓環用2^32 個點來進行均勻切割,首先按照hash(key)函式算出伺服器(節點)的雜湊值, 並將其分佈到0~2^32的圓環上。
用同樣的hash(key)函式求出需要儲存資料的鍵的雜湊值,並對映到圓環上。然後從資料對映到的位置開始順時針查詢,將資料儲存到找到的第一個伺服器(節點)上。如圖所示:
key1、key2、key3和server1、server2通過hash都能在這個圓環上找到自己的位置,並且通過順時針的方式來將key定位到 server。按上圖來說,key1和key2儲存到server1,而key3儲存到server2。如果新增一臺server,hash後在key1 和key2之間,則只會影響key1(key1將會儲存在新增的server上),其它則不變。
虛擬節點
在上圖中,很容易看出一個問題,沿順時針方向看,server2到server1之間的區間跨度大,而server1到server2的區間跨度小,這就會導致一個問題:資料分佈不均勻。大部分資料都分配到server1了,只有小部分資料分佈在server2。在伺服器資料很少的時候,資料不均勻會表現的非常明顯。
解決這個問題的方法是使用虛擬節點,一個真實伺服器對應多個虛擬節點,所有虛擬節點按hash值分佈在一致性雜湊圓環上。具體實現方法可以這樣做,為真實伺服器設定副本數量,然後根據各真實伺服器的IP和埠號再加上一個遞增的索引數計算hash值。
class RedisCache {
public $servers = array(); //真實伺服器
private $_servers = array(); //虛擬節點
const SERVER_REPLICAS = 10000; //伺服器副本數量,提高一致性雜湊演算法的資料分佈均勻程度
public function __construct( $servers ){
$this->servers = $servers;
//Redis虛擬節點雜湊表
foreach ($this->servers as $k => $server) {
for ($i = 0; $i < self::SERVER_REPLICAS; $i++) {
$hash = crc32($server['host'] . '#' .$server['port'] . '#'. $i);
$this->_servers[$hash] = $k;
}
}
ksort($this->_servers);
// something else...
}
}
副本數量可以按真實伺服器的數量調節,真實伺服器 多則副本數量可以設定小一點,真實伺服器少則副本數量需要設定多一點。在虛擬節點數量很大的時候,出於效能考慮所以不能使用迴圈的方法查詢key對應的虛 擬節點,可以使用二分法快速查詢。一個優化的演算法是不論副本數量設定為10,100,還是10000的時候,對查詢所需要的時間基本是沒有影響的。
故障轉移
使用一次性雜湊實現Redis分散式部署了,還需要考慮系統的可用性和穩定性。需要做到,在某一臺或者多臺server故障的時候,程式能夠自動檢測到故障,並將資料重新定位到其它server。
我們可以考慮,根據key查詢到的虛擬節點所對應的真實伺服器故障的時候,我們在一次性雜湊圓環上沿順時針方向順移一步,找到下一點虛擬節點對應的 真實伺服器,將所要儲存的資料存放上去。但也很有可能下一個虛擬節點所對應的真實伺服器與前一個虛擬節點相同,還是那臺故障的伺服器,而每次嘗試連線故障 的redis服務是一個很大的效能開銷。所以在第一次檢測到故障伺服器的時候就需要記錄下來,然後在順移到下一個虛擬節點的時候先判斷是不是之前那一臺故 障的伺服器,如果是那就不要再嘗試進行連線,直接查詢下一個虛擬節點,直到找到可用的伺服器將資料儲存上去。
完整示例程式碼
class RedisCache {
public $servers = array(); //真實伺服器
private $_servers = array(); //虛擬節點
private $_serverKeys = array();
private $_badServers = array(); // 故障伺服器列表
private $_count = 0;
const SERVER_REPLICAS = 10000; //伺服器副本數量,提高一致性雜湊演算法的資料分佈均勻程度
public function __construct( $servers ){
$this->servers = $servers;
$this->_count = count($this-> servers);
//Redis虛擬節點雜湊表
foreach ($this ->servers as $k => $server) {
for ($i = 0; $i < self::SERVER_REPLICAS; $i++) {
$hash = crc32($server[ 'host'] . '#' .$server['port'] . '#'. $i);
$this->_servers [$hash] = $k;
}
}
ksort( $this->_servers );
$this->_serverKeys = array_keys($this-> _servers);
}
/**
* 使用一致性雜湊分派伺服器,附加故障檢測及轉移功能
*/
private function getRedis($key){
$hash = crc32($key);
$slen = $this->_count * self:: SERVER_REPLICAS;
// 快速定位虛擬節點
$sid = $hash > $this->_serverKeys [$slen-1] ? 0 : $this->quickSearch($this->_serverKeys, $hash, 0, $slen);
$conn = false;
$i = 0;
do {
$n = $this->_servers [$this->_serverKeys[$sid]];
!in_array($n, $this->_badServers ) && $conn = $this->getRedisConnect($n);
$sid = ($sid + 1) % $slen;
} while (!$conn && $i++ < $slen);
return $conn ? $conn : new Redis();
}
/**
* 二分法快速查詢
*/
private function quickSearch($stack, $find, $start, $length) {
if ($length == 1) {
return $start;
}
else if ($length == 2) {
return $find <= $stack[$start] ? $start : ($start +1);
}
$mid = intval($length / 2);
if ($find <= $stack[$start + $mid - 1]) {
return $this->quickSearch($stack, $find, $start, $mid);
}
else {
return $this->quickSearch($stack, $find, $start+$mid, $length-$mid);
}
}
private function getRedisConnect($n=0){
static $REDIS = array();
if (!$REDIS[$n]){
$REDIS[$n] = new Redis();
try{
$ret = $REDIS[$n]->pconnect( $this->servers [$n]['host'], $this->servers [$n]['port']);
if (!$ret) {
unset($REDIS[$n]);
$this->_badServers [] = $n;
return false;
}
} catch(Exception $e){
unset($REDIS[$n]);
$this->_badServers [] = $n;
return false;
}
}
return $REDIS[$n];
}
public function getValue($key){
try{
$getValue = $this->getRedis($key)->get($key);
} catch(Exception $e){
$getValue = null;
}
return $getValue;
}
public function setValue($key,$value,$expire){
if($expire == 0){
try{
$ret = $this->getRedis($key)->set($key, $value);
} catch(Exception $e){
$ret = false;
}
} else{
try{
$ret = $this->getRedis($key)->setex($key, $expire, $value);
} catch(Exception $e){
$ret = false;
}
}
return $ret;
}
public function deleteValue($key){
return $this->getRedis($key)->delete($key);
}
public function flushValues(){
//TODO
return true;
}
}
// Usage:
$redis_servers = array(
array(
'host' => '10.0.0.1',
'port' => 6379,
),
array(
'host' => '10.0.0.2',
'port' => 6379,
),
array(
'host' => '10.0.0.3',
'port' => 6379,
),
array(
'host' => '10.0.0.3',
'port' => 6928,
),
);
$redisCache = new RedisCache($redis_servers);
$testKey = 'test_key';
$testValue = 'test_value_object';
$redisCache->setValue($testKey, $testValue, 3600);
$value = $redisCache->getValue($testKey);