有时一台Memcached服务器不能满足我们的需求,需要布置多台Memcached服务器。但是有个问题,怎么确定一个数据应该保存到哪台服务器上面呢?
有两种方案,第一种是普通的Hash分布,第二种是一致性Hash分布。下面通过PHP作为客户端来说明这两种方案。
普通Hash分布
function toHash($key){
$md5 = substr(md5($key), 0, 8);
$seed = 31;
$hash = 0;
for($i = 0; $i < 8; $i++){
$hash = $hash * $seed + ord($md5{$i});
$i++;
}
return $hash & 0x7FFFFFFF;
}
首先是通过md5把key处理成一个32位字符串,取其前8字符。再经过Hash算法处理成一个整数并返回,然后映射到其中一台Memcached服务器。
假设有两台Memcached服务器,可以使用下面的代码映射:
$servers = array(
array('host' => '192.168.1.1', 'port' => 1121),
array('host' => '192.168.1.2', 'port' => 1122)
);
$cache_key = 'cache_key';
$cache_val = 'cache_val';
$sc = $servers[toHash($cache_key) % 2];
$memcached = new Memcached($sc);
$memcached->set($cache_key, $value);
通过Hash函数把cache_key转化成整数后,利用这个整数与Memcached服务器的数量取模,这样就得到的是其中一台服务器的配置,利用这个配置连接Memcached服务器。这样就完成了分布式布置。取数据跟保存数据的方法一样。
一致性Hash分布
在服务器数量不发生改变的时候,普通的Hash分布可以很好地动作。当服务器数量发生变化时,问题就出来了。试想,增加一台服务器时,同一个key经过Hash之后,与服务器数取模的结果跟没增加之前的结果会不一样,这就导致之前保存的数据丢失。为了把丢失的数据减到最少,可以采取一致性Hash ( Consistent Hashing )分布算法解决。
一致性Hash分布算法分4个步骤,如下所示。
步骤1 将一个32位整数 ( 即 0 ~2^32 -1 )想象成一个环,当然这只是想象,将0作为圆环的头,2^32 -1 作为圆环的尾,把它连接起来,就成为一个环。
步骤2 通过toHash函数把key处理成整数。例如:
$key1 = toHash('key1');
$key2 = toHash('key2');
把key处理成整数后,就可以在环中找到一个位置与之对应。
步骤3 把Memcached群映射到环上,使用Hash函数处理服务器所使用的IP地址。例如有三台服务器:
$server1 = toHash('192.168.1.1');
$server2 = toHash('192.168.1.2');
$server3 = toHash('192.168.1.3');
经过上面几个步骤,我们把数据的key和服务器都映射到同一个不环上。下面考虑如何把数据映射到服务器上。
步骤4 把数据映射到服务器上,沿着圆环顺时针方向的key出发,直到遇到一个服务器为止,把key对应的数据保存到这个服务器上。
如果现在server2服务器崩溃了,那么受到影响的仅是那些沿着server2逆时针出发直到遇到下一个服务器server3之间的数据,也就是映射到server2上的那些数据。
如果现在需要增加一台服务器server4,例如增加之后server4在server3与server1之间,那部分原本映射到server1上面的数据现在会映射到server4上面。
经过这种一致性hash方式,在增加或者减少服务器的时候,只会改变很少一部分数据的存储服务器,从而减少了数据丢失的情况。
//多台缓存服务器一致性Hash分布算法实例
class FlexiHash{
private $serverList = array();
private $isSorted = false;
/**
* @des Hash方法,首先通过md5把key处理成一个32位字符串,取其前8字符,再经过Hash算法处理成一个整数并返回,然后映射到其中一台缓存服务器。
* @param string $key 键值
* @return int
*/
public function toHash($key){
$md5 = substr(md5($key), 0, 8);
$seed = 31;
$hash = 0;
for($i = 0; $i < 8; $i++){
$hash = $hash * $seed + ord($md5{$i});
$i++;
}
return $hash & 0x7FFFFFFF;
}
//添加服务器
/**
*
*首先通过toHash函数计算出服务器的Hash值,
通过此Hash值定位服务器到列表上的某个位置。
因为此时服务器列表发生了变化,所以应该把排序标识$isSorted设置为false。
*
*/
public function addServer($server){
$hash = $this->toHash($server);
if(!isset($this->serverList[$hash])){
$this->serverList[$hash] = $server;
}
$this->isSorted = false;
return true;
}
//移除服务器
/**
*首先通过toHash函数计算出服务器的Hash值,
以此Hash值作为要删除服务器的索引,
把此服务器从服务器列表中删除。
因为此时服务器列表发生了变化,所以把排序标识$isSorted设置为falsh。
*
*/
public function removeServer($server){
$hash = $this->toHash($server);
if(isset($this->serverList[$hash])){
unset($this->serverList[$hash]);
}
$this->isSorted = false;
return true;
}
//找到合适的服务器返回
/**
*首先通过toHash函数计算出key的Hash值,然后判断服务器列表是否排过序,
如果没有,就先对服务器列表进行倒序排序操作。
倒序排序的作用是把服务器列表转化成一个逆时针的圆环。
最后遍历服务器列表,找到一个合适的服务器返回。
*
*/
public function lookup($key){
$hash = $this->toHash($key);
if(!$this->isSorted){
krsort($this->serverList, SORT_NUMERIC);
$this->isSorted = true;
}
foreach($this->serverList as $pos => $server){
if($hash >= $pos){
return $server;
}
}
return $this->serverList[count($this->serverList) - 1];
}
}