1要解決的問題:
像Memcache以及其它一些內存K/V數據庫一樣,Redis本身不提供分布式支持,所以在部署多臺Redis服務器時,就需要解決如何把數據分散到各個服務器的問題,并且在服務器數量變化時,能做到最大程度的不令數據重新分布。
通常使用的分布式方法是根據所要存儲數據的鍵的hash值與服務器數量N,按 hash % N 取模的算法來將數據分布到各個服務器。該算法的優點是足夠簡單,而且數據分布均勻。但是一旦服務器數量N發生變化的時候,緩存命中率會瞬間跌入谷底,因為絕大多數的數據需要重新分布。而且對于大型網站來說,此時會有巨大的壓力涌向后端服務,可能會導致性能故障和服務故障,甚至宕機。
2.實現步驟:
1)首先求出redis服務器(節點)的哈希值,并將其配置到0~2^32的圓上。
2)然后采用同樣的方法求出存儲數據的鍵的哈希值,并映射到相同的圓上。
3)然后從數據映射到的位置開始順時針查找,將數據保存到找到的第一個服務器上。如果超過2^32仍然找不到服務器,就會保存到第一臺redis服務器上。
3.舉例:
1508919009.jpg
key1、key2、key3和server1、server2通過hash都能在這個圓環上找到自己的位置,并且通過順時針的方式來將key定位到 server。按上圖來說,key1和key2存儲到server1,而key3存儲到server2。如果新增一臺server,hash后在key1 和key2之間,則只會影響key1(key1將會存儲在新增的server上),其它則不變。
4.存在的問題:
在上圖中,很容易看出一個問題,沿順時針方向看,server2到server1之間的區間跨度大,而server1到server2的區間跨度小,這就會導致一個問題:數據分布不均勻。大部分數據都分配到server1了,只有小部分數據分布在server2。在服務器數據很少的時候,數據不均勻會表現的非常明顯。
5.解決辦法:
使用虛擬節點,一個真實服務器對應多個虛擬節點,所有虛擬節點按hash值分布在一致性哈希圓環上。具體實現方法可以這樣做,為真實服務器設置副本數量,然后根據各真實服務器的IP和端口號再加上一個遞增的索引數計算hash值。
6.故障轉移:
使用一次性哈希實現Redis分布式部署了,還需要考慮系統的可用性和穩定性。需要做到,在某一臺或者多臺server故障的時候,程序能夠自動檢測到故障,并將數據重新定位到其它server。
我們可以考慮,根據key查找到的虛擬節點所對應的真實服務器故障的時候,我們在一次性哈希圓環上沿順時針方向順移一步,找到下一點虛擬節點對應的真實服務器,將所要存儲的數據存放上去。但也很有可能下一個虛擬節點所對應的真實服務器與前一個虛擬節點相同,還是那臺故障的服務器,而每次嘗試連接故障的redis服務是一個很大的性能開銷。所以在第一次檢測到故障服務器的時候就需要記錄下來,然后在順移到下一個虛擬節點的時候先判斷是不是之前那一臺故障的服務器,如果是那就不要再嘗試進行連接,直接查找下一個虛擬節點,直到找到可用的服務器將數據存儲上去。
7.示例代碼:
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);