使用场景
比如你有 N 个 cache 服务器(后面简称 cache ),那么如何将一个对象 object 映射到 N 个 cache 上呢,你很可能会采用类似下面的通用方法计算 object 的 hash 值,然后均匀的映射到到 N 个 cache ;求余算法: hash(object)%N
一切都运行正常,再考虑如下的两种情况;
1 一个 cache 服务器 m down 掉了(在实际应用中必须要考虑这种情况),这样所有映射到 cache m 的对象都会失效,怎么办,需要把 cache m 从 cache 中移除,这时候 cache 是 N-1 台,映射公式变成了 hash(object)%(N-1) ;
2 由于访问加重,需要添加 cache ,这时候 cache 是 N+1 台,映射公式变成了 hash(object)%(N+1) ;
1 和 2 意味着什么?这意味着突然之间几乎所有的 cache 都失效了。对于服务器而言,这是一场灾难,洪水般的访问都会直接冲向后台服务器;
Consistent Hashing 一致性hash的原理
consistent hashing 是一种 hash 算法,简单的说,在移除 / 添加一个 cache 时,它能够尽可能小的改变已存在key 映射关系,尽可能的满足单调性的要求。
code代码实现<?php
class ConsistentHash
{
protected $nodes;
protected $replicaNum;
private $positions = [];
/**
* 把字符串转为32位符号整数
* @param $str
* @return string
*/
public function hash($str)
{
return sprintf('%u', crc32($str));
}
/**
* ConsistentHash constructor.
* @param int $replicaNum 数值越大散列越好,推荐最小为16,数值为2的指数幂
*/
public function __construct($replicaNum)
{
$this->replicaNum = $replicaNum;
}
/**
* 定位key所在节点
* @param $key
* @return mixed
*/
public function locateKey($key)
{
$point = $this->hash($key);
// 先取圆环上最小的一个节点,当成结果
$node = current($this->positions);
// 循环获取相近的节点
foreach ($this->positions as $key => $val) {
if ($point <= $key) {
$node = $val;
break;
}
}
reset($this->positions);
return $node;
}
public function addNode($node)
{
if (isset($this->nodes[$node])) return;
// 添加节点和虚拟节点
for ($i = 0; $i < $this->replicaNum; $i++) {
$pos = $this->hash($node . '#' . $i);
$this->positions[$pos] = $node;
$this->nodes[$node][] = $pos;
}
// 重新排序
$this->sortPos();
}
public function removeNode($node)
{
if (!isset($this->nodes[$node])) return;
// 循环删除虚拟节点
foreach ($this->nodes[$node] as $val) {
unset($this->positions[$val]);
}
// 删除节点
unset($this->nodes[$node]);
}
public function sortPos()
{
ksort($this->positions, SORT_REGULAR);
}
}
改变replicaNum测试散列效果(测试代码)$hash = new ConsistentHash(256);
$hash->addNode('192.168.0.1');
$hash->addNode('192.168.0.2');
$hash->addNode('192.168.0.3');
$hash->addNode('192.168.0.4');
$hash->addNode('192.168.0.5');
$data = [];
$limit = 10000;
for ($i = 0; $i <= $limit; $i++) {
$key = uniqid(uniqid());
$data[$hash->locateKey($key)][] = $key;
}
$average = bcdiv($limit, count($data));
$variance = 0;
foreach ($data as $node => $keys) {
$num = count($keys);
$variance += pow(abs($num - $average), 2);
echo $node . ' - ' . $num . PHP_EOL;
}
echo '方差值为:' . $variance . PHP_EOL;
测试结果8
192.168.0.2 - 5374
192.168.0.3 - 4000
192.168.0.1 - 126
192.168.0.4 - 419
192.168.0.5 - 82
方差值为:25074037
16
192.168.0.3 - 2578
192.168.0.2 - 1569
192.168.0.1 - 2530
192.168.0.4 - 1539
192.168.0.5 - 1785
方差值为:1059491
32
192.168.0.3 - 1733
192.168.0.4 - 2112
192.168.0.5 - 1765
192.168.0.2 - 1632
192.168.0.1 - 2759
方差值为:850563
64
192.168.0.2 - 1411
192.168.0.3 - 2502
192.168.0.5 - 2141
192.168.0.4 - 1868
192.168.0.1 - 2079
方差值为:642471
128
192.168.0.1 - 2108
192.168.0.4 - 2032
192.168.0.3 - 2073
192.168.0.2 - 1417
192.168.0.5 - 2371
方差值为:495547
256
192.168.0.4 - 1922
192.168.0.1 - 2501
192.168.0.2 - 1969
192.168.0.5 - 1908
192.168.0.3 - 1701
方差值为:355911
结语
通过测试结果可知,推荐replicaNum最小值为16,再考虑性能的情况下,最大值设为64