随着memcache、Redis以及其它一些内存K/V数据库的流行,一致性哈希也愈来愈被开发者所了解。由于这些内存K/V数据库大多不提供分布式支持(本文以redis为例),因此若是要提供多台redis server来提供服务的话,就须要解决如何将数据分散到redis server,而且在增减redis server时如何最大化的不令数据从新分布,这将是本文讨论的范畴。
取模算法
取模运算一般用于获得某个半开区间内的值:m % n = v,其中n不为0,值v的半开区间为:[0, n)。取模运算的算法很简单:有正整数k,并令k使得k和n的乘积最大但不超过m,则v的值为:m - kn。好比1 % 5,令k = 0,则k * 5的乘积最大并不超过1,故结果v = 1 - 0 * 5 = 1。
咱们在分表时也会用到取模运算。如一个表要划分三个表,则可对3进行取模,由于结果老是在[0, 3)以内,也就是取值为:0、一、2。
可是对于应用到redis上,这种方式就不行了,由于太容易冲突了。
哈希(Hash)
Hash,通常翻译作“散列”,也有直接音译为"哈希"的,就是把任意长度的输入(又叫作预映射, pre-image),经过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是散列值的空间一般远小于输入的空间,不一样的输入可能会散列成相同的输出,而不可能从散列值来惟一的肯定输入值。
简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
目前广泛采用的哈希算法是time33,又称DJBX33A (Daniel J. Bernstein, Times 33 with Addition)。这个算法被普遍运用于多个软件项目,Apache、Perl和Berkeley DB等。对于字符串而言这是目前所知道的最好的哈希算法,缘由在于该算法的速度很是快,并且分类很是好(冲突小,分布均匀)。
PHP
内核就采用了time33算法来实现HashTable,来看下time33的定义:
- hash(i) = hash(i-1) * 33 + str[i]
有了定义就容易实现了:
- <?php
- function myHash($str) {
- // hash(i) = hash(i-1) * 33 + str[i]
- $hash = 0;
- $s = md5($str);
- $seed = 5;
- $len = 32;
- for ($i = 0; $i < $len; $i++) {
- // (hash << 5) + hash 至关于 hash * 33
- //$hash = sprintf("%u", $hash * 33) + ord($s{$i});
- //$hash = ($hash * 33 + ord($s{$i})) & 0x7FFFFFFF;
- $hash = ($hash << $seed) + $hash + ord($s{$i});
- }
-
- return $hash & 0x7FFFFFFF;
- }
-
- echo myHash("test"); //输出 786776064
利用取模实现
如今有2台redis server,因此须要计算键的hash并跟2取模。好比有键key1和key2,代码以下:
- <?php
- function myHash($str) {
- // hash(i) = hash(i-1) * 33 + str[i]
- $hash = 0;
- $s = md5($str);
- $seed = 5;
- $len = 32;
- for ($i = 0; $i < $len; $i++) {
- // (hash << 5) + hash 至关于 hash * 33
- //$hash = sprintf("%u", $hash * 33) + ord($s{$i});
- //$hash = ($hash * 33 + ord($s{$i})) & 0x7FFFFFFF;
- $hash = ($hash << $seed) + $hash + ord($s{$i});
- }
-
- return $hash & 0x7FFFFFFF;
- }
-
- echo "key1: " . (myHash("key1") % 2) . "\n";
- echo "key2: " . (myHash("key2") % 2) . "\n";
对于key1和key2来讲,同时存储到一台服务器上,这彷佛没什么问题,但正由于key1和key2是始终存储到这台服务器上,一旦这台服务器下线了,则这台服务器上的数据所有要从新定位到另外一台服务器。对于增长服务器也是相似的状况。并且从新hash(以前跟2进行hash,如今是跟3进行hash)以后,结果就变掉了,致使大多数数据须要从新定位到redis server。
在服务器数量不变的时候,这种方式也是能很好的工做的。
一致性哈希
因为hash算法结果通常为unsigned int型,所以对于hash函数的结果应该均匀分布在[0,2^32-1]区间,若是咱们把一个圆环用2^32 个点来进行均匀切割,首先按照hash(key)函数算出服务器(节点)的哈希值, 并将其分布到0~2^32的圆环上。
用一样的hash(key)函数求出须要存储数据的键的哈希值,并映射到圆环上。而后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器(节点)上。如图所示:
key一、key二、key3和server一、server2经过hash都能在这个圆环上找到本身的位置,而且经过顺时针的方式来将key定位到server。按上图来讲,key1和key2存储到server1,而key3存储到server2。若是新增一台server,hash后在key1和key2之间,则只会影响key1(key1将会存储在新增的server上),其它不变。
上图这个圆环至关因而一个排好序的数组,咱们先经过代码来看下key一、key二、key三、server一、server2的hash值,而后再做分析:
- <?php
- function myHash($str) {
- // hash(i) = hash(i-1) * 33 + str[i]
- $hash = 0;
- $s = md5($str);
- $seed = 5;
- $len = 32;
- for ($i = 0; $i < $len; $i++) {
- // (hash << 5) + hash 至关于 hash * 33
- //$hash = sprintf("%u", $hash * 33) + ord($s{$i});
- //$hash = ($hash * 33 + ord($s{$i})) & 0x7FFFFFFF;
- $hash = ($hash << $seed) + $hash + ord($s{$i});
- }
-
- return $hash & 0x7FFFFFFF;
- }
-
- //echo myHash("却道天凉好个秋~");
- echo "key1: " . myHash("key1") . "\n";
- echo "key2: " . myHash("key2") . "\n";
- echo "key3: " . myHash("key3") . "\n";
- echo "serv1: " . myHash("server1") . "\n";
- echo "serv2: " . myHash("server2") . "\n";
如今咱们根据hash值从新画一张在圆环上的分布图,以下所示:
key一、key2和key3都存储到了server1上,这是正确的,由于是按顺时针来定位。咱们想像一下,全部的server其实就是一个排好序的数组(降序):[server2, server1],而后经过计算key的hash值来获得处于哪一个server上。来分析下定位过程:若是只有一台server,即[server],则直接定位,取数组的第一个元素。若是有多台server,则要先看经过key计算的hash值是否落在[server2, server1, ...]这个区间上,这个直接跟数组的第一个元素和最后一个元素比较就知道了。而后就能够经过查找来定位了。
利用一致性哈希实现
下面是一个实现一致性哈希的例子,仅仅实现了addServer和find。其实对于remove的实现跟addServer是相似的。代码以下:
- <?php
- function myHash($str) {
- // hash(i) = hash(i-1) * 33 + str[i]
- $hash = 0;
- $s = md5($str);
- $seed = 5;
- $len = 32;
- for ($i = 0; $i < $len; $i++) {
- // (hash << 5) + hash 至关于 hash * 33
- //$hash = sprintf("%u", $hash * 33) + ord($s{$i});
- //$hash = ($hash * 33 + ord($s{$i})) & 0x7FFFFFFF;
- $hash = ($hash << $seed) + $hash + ord($s{$i});
- }
-
- return $hash & 0x7FFFFFFF;
- }
-
- class ConsistentHash {
- // server列表
- private $_server_list = array();
- // 延迟排序,由于可能会执行屡次addServer
- private $_layze_sorted = FALSE;
-
- public function addServer($server) {
- $hash = myHash($server);
- $this->_layze_sorted = FALSE;
-
- if (!isset($this->_server_list[$hash])) {
- $this->_server_list[$hash] = $server;
- }
-
- return $this;
- }
-
- public function find($key) {
- // 排序
- if (!$this->_layze_sorted) {
- asort($this->_server_list);
- $this->_layze_sorted = TRUE;
- }
-
- $hash = myHash($key);
- $len = sizeof($this->_server_list);
- if ($len == 0) {
- return FALSE;
- }
-
- $keys = array_keys($this->_server_list);
- $values = array_values($this->_server_list);
-
- // 若是不在区间内,则返回最后一个server
- if ($hash <= $keys[0] || $hash >= $keys[$len - 1]) {
- return $values[$len - 1];
- }
-
- foreach ($keys as $key=>$pos) {
- $next_pos = NULL;
- if (isset($keys[$key + 1]))
- {
- $next_pos = $keys[$key + 1];
- }
-
- if (is_null($next_pos)) {
- return $values[$key];
- }
-
- // 区间判断
- if ($hash >= $pos && $hash <= $next_pos) {
- return $values[$key];
- }
- }
- }
- }
-
- $consisHash = new ConsistentHash();
- $consisHash->addServer("serv1")->addServer("serv2")->addServer("server3");
- echo "key1 at " . $consisHash->find("key1") . ".\n";
- echo "key2 at " . $consisHash->find("key2") . ".\n";
- echo "key3 at " . $consisHash->find("key3") . ".\n";
- $ php -f test.php
- key1 at server3.
- key2 at server3.
- key3 at serv2.
即便新增或下线服务器,也不会影响所有,只要根据hash顺时针定位就能够了。
结束语
常常有人问在有多台redis server时,新增或删除节点如何通知其它节点。之因此会这么问,是由于不了解redis的部署方式。