原来在学校的时候就看过关于一致性哈希算法的文章,但是由于当时没有实际工作经验更没有接触过分布式缓存的原因,并没能真正的理解一致性哈希算法。最近,接触到了这个算法,好好研究了一下,这里总结一波~~~
一致性哈希算法是目前分布式缓存系统中广泛采用的处理算法,该算法主要是用来解决分布式缓存系统中因为缓存服务器的数量的变化所造成的缓存命中率大幅度下滑的问题。下面,我们就来具体的分析一下。
首先,咱们先说说什么是哈希(Hash)。我这里直接将百度百科的定义拉过来:
Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的(又叫做预映射, pre-image),通过散列算法,变换成固定长度的,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的的函数。
上面的定义已经说的很清楚了,我们可以将任意的数据交给哈希函数进行处理,从而得到一个对该输入的数据进行描述的"简短"的"信息摘要"。这里需要说明的是,通过hash函数获取到的"信息摘要"与输入的数据之间并不是完全一对一的,可能两条不同的输入数据经过哈希函数处理后获取到的"信息摘要"是完全一样的,这种现象称之为"碰撞",发生"碰撞"的概率高低与所采用的哈希函数有关,一个好的哈希函数会尽可能的避免"碰撞"问题的发生。实现哈希函数的算法有很多,各有优缺点,下面是php实现hash函数所采用的算法,下面的代码很简单,这里就不赘述了:
复制代码
了解了哈希的基本原理以后,接着来看看目前分布式缓存系统中具体的问题。
1:假设,我们当前的业务比较小,整个项目只有一台缓存服务器(redis/memcached)。这种情况下,其实是不会存在问题的,因为就一台服务器嘛。
2:假设,我们的业务增长很快,一台缓存服务器已经不能满足我们的需求了,我们需要再添加一台缓存服务器来减轻项目的负载压力。这里,我们就遇到了第一个问题,当我们的项目存在两台(或者更过台)缓存服务器的时候,我们应该向那台服务器中写入数据那?ok,我们可以按照下面的方式,获取一个随机值,然后根据返回的随机值,向相对应的服务器中写入数据。
复制代码上面的代码完成数据的写入操作完全没有问题,但是如果随机的写入到任意一台服务中,又应该在那台服务器中读取数据那?写入的数据与服务器之间完全没有关系,我们为了获取写入的数据,总不能一台接着一台的遍历服务器吧?为了解决这个问题,我们需要将需要写入的数据与服务器之间建立一种一对一的关系,只要建立的一对一的关系,我们既可以做到方便的写入,也可以做到方便的读取了。
怎么才能做到上面的所说的一对一的关系那?我们可以将需要写入的数据进行哈希函数的处理,然后得到一个int类型的数据(这里需要注意的是,哈希算法的返回值一般都是int类型的数据,也就是在0~2^32-1之间的整数)。然后我们再用得到的这个整数对缓存服务器的数量n进行取余处理,返回的余数就是所对应的服务器本身了。(这里其实用到了取余的一个小技巧,假设我们用任意整数对2进行取余,结果只会有两个,分别是0和1)。如果我们需要存入的数据经过哈希函数处理以后,等到22222,然后对2(服务器数量)进行取余处理,得到结果0,也就是说这条数据与第一台服务器是相对应的,那么我们就需要将这条数据写入到第一台服务器中去。当我们要想读取这条数据的时候,同样的按照上面的步骤计算一遍,获取到相对应的服务器,从而快速的获取到数据。上面的实现方式称之为取余算法,这种算法在一致性哈希算法出现之前,被广泛的使用。
3:上面我们已经解决的数据的存储以及读取的问题,这样是不是就ok了?其实并不是的,上面的取余算法其实还存在着很大的问题。如果我们项目目前已经趋于稳定了,不会新增加缓存服务器了或者是我们的缓存服务器都很稳定不会出现宕机的问题,上面的取余算法没啥问题。但是一旦我们要增加服务器的数量或者服务器出现宕机,就会出现缓存命中率急剧下滑的问题。我们首先先分析一下,造成上面的情形的具体的原因。如果我们的服务器数量由原先的两台增加到现在的三台,那么我们在读取数据的时候就需要对3进行取余操作,还是拿上面的22222为例,本身是存放在第一台服务器上的,现在对3进行取余,返回的结果是1,也就说我们要去第二台缓存服务器上去取数据,可是数据本身是存放在第一台服务器上的,去第二台服务器上去查我们根本就得不到数据,这样就造成了缓存命中率的急剧的下滑。也就是说,取余算法对于缓存服务器增加或者减少的情况是不能很好的处理的。
4: 上面提到的问题,取余算法没有办法很好的解决,那怎么办那?这里就需要我们的一致性哈希算法登场了~~~先说一下,一致性哈希算法的原理:前面我们已经提到过了,哈希函数的返回值的一般都是在0-2^32-1之间,那我们就可以将整个区间想象成为一个封闭的圆环,圆环上一共有2^32个刻度,每一个刻度都用来存放这个刻度所对应的数据。假设我们存在多台服务器,我们可以根据各个服务器的参数(比如说ip地址),计算出其所对应的刻度,那么从该上一台服务器所对应的刻度到该服务器所对应的刻度这个区间之内,都属于该服务器的存储空间。具体的存储过程,如下图所示:
根据上面的分析以及上面的图片,我们来分析一下,假如Node2所对应的服务器宕机了,那么从Node1节点到Node2节点之间的缓存数据(也就是存储在Node2所对应的服务器上的缓存数据)会受到影响,但是其他各个区间的数据是不会受到影响的。当服务器数量增加的情况下也很类似。这里需要注意的是,一致性哈希算法也会在服务器数量增加或者减少的情况下出现缓存命中率下降的问题,但是并不会像取余算法一样,影响其余各个缓存服务器上的数据,其只会影响较小的一部分,所造成的损失相比取余算法减少很多。
5:通过上面的分析,我们可以采用一致性哈希算法解决取余算法的弊端。但是一致性哈希算法本身也存在弊端。一致性哈希算法相比于取余算法的弊端就是它不能就缓存数据均匀的分布到多台服务器上。这是因为我们通过服务器的相关信息(比如说ip地址)计算出的哈希值通常与最大哈希值(2^23-1)相差甚远,也就说哈希值最大的那台缓存服务器所要承担的缓存数据的读写压力将远远超过其他几台缓存服务器。这个问题怎么解决那?一般我们都是采用虚拟节点的方式进行解决,也就是说我们在缓存数据的哈希值与缓存服务器所对应的哈希值之间再加一层"代理",这层代理主要的作用就是将数据均匀的分配到各个区间之间。
上面就是整体的理解思路了,下面我将实现一致性哈希算法的代码贴出来,供大家参考:
serverList[$hash])) { $this->serverList[$hash] = $server; } return true; } //移除服务器 public function removeServer($server){ $hash = myHash($server); if (isset($this->serverList[$hash])) { unset($this->serverList[$hash]); } return true; } public function lookup($key) { $hash = myHash($key); // 对服务器列表进行排序 if (!$this->isSorted) { krsort($this->serverList, SORT_NUMERIC); $this->isSorted = true; $this->serverKeys = array_keys($this->serverList); } // 查找相邻的数据 foreach ($this->serverList as $pos => $server) { if ($hash >= $pos) { return $server; } } // 找不到,返回最后一个 return $this->serverList[$this->serverKeys[count($this->serverList) - 1]]; }}$hash_server = new ConsitentHash();$hash_server->addServer('192.168.1.101');$hash_server->addServer('192.168.1.102');$hash_server->addServer('192.168.1.103');$hash_server->addServer('192.168.1.104');$hash_server->addServer('192.168.1.105');$hash_server->addServer('192.168.1.106');?>复制代码