hash一致性算法 php,一致性hash原理及php实例

引入

在业务开发中,我们常把数据持久化到数据库中。如果需要读取这些数据,除了直接从数据库中读取外,为了减轻数据库的访问压力以及提高访问速度,我们更多地引入缓存来对数据进行存取。读取数据的过程一般为:

70d890ce75373b8611a4dddda879379f.png

图1:加入缓存的数据读取过程

对于分布式缓存,不同机器上存储不同对象的数据。为了实现这些缓存机器的负载均衡,可以使用式子1来定位对象缓存的存储机器:

m = hash(o) mod n ——式子1

其中,o为对象的名称,n为机器的数量,m为机器的编号,hash为一hash函数。图2中的负载均衡器(load balancer)正是使用式子1来将客户端对不同对象的请求分派到不同的机器上执行,例如,对于对象o,经过式子1的计算,得到m的值为3,那么所有对对象o的读取和存储的请求都被发往机器3执行。

13750b487ca13da4574e40f1dfbf57b0.png

图2:如何利用Hash取模实现负载均衡

式子1在大部分时候都可以工作得很好,然而,当机器需要扩容或者机器出现宕机的情况下,事情就比较棘手了。

当机器扩容,需要增加一台缓存机器时,负载均衡器使用的式子变成:

m = hash(o) mod (n + 1) ——式子2

当机器宕机,机器数量减少一台时,负载均衡器使用的式子变成:

m = hash(o) mod (n - 1) ——式子3

我们以机器扩容的情况为例,说明简单的取模方法会导致什么问题。假设机器由3台变成4台,对象o1由式子1计算得到的m值为2,由式子2计算得到的m值却可能为0,1,2,3(一个 3t + 2的整数对4取模,其值可能为0,1,2,3,读者可以自行验证),大约有75%(3/4)的可能性出现缓存访问不命中的现象。随着机器集群规模的扩大,这个比例线性上升。当99台机器再加入1台机器时,不命中的概率是99%(99/100)。这样的结果显然是不能接受的,因为这会导致数据库访问的压力陡增,严重情况,还可能导致数据库宕机。

一致性hash算法正是为了解决此类问题的方法,它可以保证当机器增加或者减少时,对缓存访问命中的概率影响减至很小。下面我们来详细说一下一致性hash算法的具体过程。

一致性Hash环

一致性hash算法通过一个叫作一致性hash环的数据结构实现。这个环的起点是0,终点是2^32 - 1,并且起点与终点连接,环的中间的整数按逆时针分布,故这个环的整数分布范围是[0, 2^32-1],如下图3所示:

75c032781c20db3cf0cee8afd8e0eb25.png

图3:一致性Hash环

将对象放置到Hash环

假设现在我们有4个对象,分别为o1,o2,o3,o4,使用hash函数计算这4个对象的hash值(范围为0 ~ 2^32-1):

hash(o1) = m1

hash(o2) = m2

hash(o3) = m3

hash(o4) = m4

把m1,m2,m3,m4这4个值放置到hash环上,得到如下图4:

ac1219234f41272f7ef42edad0983cb2.png

图4:放置了对象的一致性Hash环

将机器放置到Hash环

使用同样的hash函数,我们将机器也放置到hash环上。假设我们有三台缓存机器,分别为 c1,c2,c3,使用hash函数计算这3台机器的hash值:

hash(c1) = t1

hash(c2) = t2

hash(c3) = t3

把t1,t2,t3 这3个值放置到hash环上,得到如下图5:

f4ba2166125201baf6556c2e4c18eb2b.png

图5:放置了机器的一致性Hash环

为对象选择机器

将对象和机器都放置到同一个hash环后,在hash环上顺时针查找距离这个对象的hash值最近的机器,即是这个对象所属的机器。

例如,对于对象o2,顺序针找到最近的机器是c1,故机器c1会缓存对象o2。而机器c2则缓存o3,o4,机器c3则缓存对象o1。

29cc60c01837098694fdcf1cfe3e8238.png

图6:在一致性Hash环上为对象选择机器

处理机器增减的情况

对于线上的业务,增加或者减少一台机器的部署是常有的事情。

例如,增加机器c4的部署并将机器c4加入到hash环的机器c3与c2之间。这时,只有机器c3与c4之间的对象需要重新分配新的机器。对于我们的例子,只有对象o4被重新分配到了c4,其他对象仍在原有机器上。如图7所示:

028484b43072e4aedca8986e0e509ab0.png

图7:增加机器后的一致性Hash环的结构

如上文前面所述,使用简单的求模方法,当新添加机器后会导致大部分缓存失效的情况,使用一致性hash算法后这种情况则会得到大大的改善。前面提到3台机器变成4台机器后,缓存命中率只有25%(不命中率75%)。而使用一致性hash算法,理想情况下缓存命中率则有75%,而且,随着机器规模的增加,命中率会进一步提高,99台机器增加一台后,命中率达到99%,这大大减轻了增加缓存机器带来的数据库访问的压力。

再例如,将机器c1下线(当然,也有可能是机器c1宕机),这时,只有原有被分配到机器c1对象需要被重新分配到新的机器。对于我们的例子,只有对象o2被重新分配到机器c3,其他对象仍在原有机器上。如图8所示:

122ec00d8de7124bca7e856f241ee264.png

图8:减少机器后的一致性Hash环的结构

虚拟节点

上面提到的过程基本上就是一致性hash的基本原理了,不过还有一个小小的问题。新加入的机器c4只分担了机器c2的负载,机器c1与c3的负载并没有因为机器c4的加入而减少负载压力。如果4台机器的性能是一样的,那么这种结果并不是我们想要的。

为此,我们引入虚拟节点来解决负载不均衡的问题。

将每台物理机器虚拟为一组虚拟机器,将虚拟机器放置到hash环上,如果需要确定对象的机器,先确定对象的虚拟机器,再由虚拟机器确定物理机器。

说得有点复杂,其实过程也很简单。

还是使用上面的例子,假如开始时存在缓存机器c1,c2,c3,对于每个缓存机器,都有3个虚拟节点对应,其一致性hash环结构如图9所示:

20b454f1cdc212fbd56a47b05abc153f.png

图9:机器c1,c2,c3的一致性Hash环结构

假设对于对象o1,其对应的虚拟节点为c11,而虚拟节点c11对象缓存机器c1,故对象o1被分配到机器c1中。

新加入缓存机器c4,其对应的虚拟节点为c41,c42,c43,将这三个虚拟节点添加到hash环中,得到的hash环结构如图10所示:

185db4b97012246fdc0e075a6514282f.png

图10:机器c1,c2,c3,c4的一致性Hash环结构

新加入的缓存机器c4对应一组虚拟节点c41,c42,c43,加入到hash环后,影响的虚拟节点包括c31,c22,c11(顺时针查找到第一个节点),而这3个虚拟节点分别对应机器c3,c2,c1。即新加入的一台机器,同时影响到原有的3台机器。理想情况下,新加入的机器平等地分担了原有机器的负载,这正是虚拟节点带来的好处。而且新加入机器c4后,只影响25%(1/4)对象分配,也就是说,命中率仍然有75%,这跟没有使用虚拟节点的一致性hash算法得到的结果是相同的。

总结

一致性hash算法解决了分布式环境下机器增加或者减少时,简单的取模运算无法获取较高命中率的问题。通过虚拟节点的使用,一致性hash算法可以均匀分担机器的负载,使得这一算法更具现实的意义。正因如此,一致性hash算法被广泛应用于分布式系统中。

《大型网站技术架构——核心原理与安全分析》,李智慧著,电子工业出版社

作者:haozlee

来源:CSDN

原文:https://blog.csdn.net/lihao21/article/details/54193868

版权声明:本文为博主原创文章,转载请附上博文链接!

一致性hash算法的PHP实现

/**

Flexihash - A simple consistent hashing implementation for PHP.

The MIT License

Copyright (c) 2008 Paul Annesley

Permission is hereby granted, free of charge, to any person obtaining a copy

of this software and associated documentation files (the "Software"), to deal

in the Software without restriction, including without limitation the rights

to use, copy, modify, merge, publish, distribute, sublicense, and/or sell

copies of the Software, and to permit persons to whom the Software is

furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in

all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR

IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE

AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER

LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,

OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN

THE SOFTWARE.

@author Paul Annesley

@copyright Paul Annesley, 2008

/**

A simple consistent hashing implementation with pluggable hash algorithms.

@author Paul Annesley

@package Flexihash

The number of positions to hash each target to.

@var int

@comment 虚拟节点数,解决节点分布不均的问题

*/

private $_replicas = 64;

/**

The hash algorithm, encapsulated in a Flexihash_Hasher implementation.

@var object Flexihash_Hasher

@comment 使用的hash方法 : md5,crc32

*/

private $_hasher;

/**

Internal counter for current number of targets.

@var int

@comment 节点记数器

*/

private $_targetCount = 0;

/**

Internal map of positions (hash outputs) to targets

@var array { position => target, ... }

@comment 位置对应节点,用于lookup中根据位置确定要访问的节点

*/

private $_positionToTarget = array();

/**

Internal map of targets to lists of positions that target is hashed to.

@var array { target => [ position, position, ... ], ... }

@comment 节点对应位置,用于删除节点

*/

private $_targetToPositions = array();

/**

Whether the internal map of positions to targets is already sorted.

@var boolean

@comment 是否已排序

*/

private $_positionToTargetSorted = false;

/**

Constructor

@param object $hasher Flexihash_Hasher

@param int $replicas Amount of positions to hash each target to.

@comment 构造函数,确定要使用的hash方法和需拟节点数,虚拟节点数越多,分布越均匀,但程序的分布式运算越慢

*/

public function __construct(Flexihash_Hasher $hasher = null, $replicas = null)

{

$this->_hasher = $hasher ? $hasher : new Flexihash_Crc32Hasher();

if (!empty($replicas)) $this->_replicas = $replicas;

}

/**

Add a target.

@param string $target

@chainable

@comment 添加节点,根据虚拟节点数,将节点分布到多个虚拟位置上

*/

public function addTarget($target)

{

if (isset($this->_targetToPositions[$target]))

{

throw new Flexihash_Exception("Target '$target' already exists.");

}

$this->_targetToPositions[$target] = array();

// hash the target into multiple positions

for ($i = 0; $i < $this->_replicas; $i++)

{

$position = $this->_hasher->hash($target . $i);

$this->_positionToTarget[$position] = $target; // lookup

$this->_targetToPositions[$target] []= $position; // target removal

}

$this->_positionToTargetSorted = false;

$this->_targetCount++;

return $this;

}

/**

Add a list of targets.

@param array $targets

@chainable

*/

public function addTargets($targets)

{

foreach ($targets as $target)

{

$this->addTarget($target);

}

return $this;

}

/**

Remove a target.

@param string $target

@chainable

*/

public function removeTarget($target)

{

if (!isset($this->_targetToPositions[$target]))

{

throw new Flexihash_Exception("Target '$target' does not exist.");

}

foreach ($this->_targetToPositions[$target] as $position)

{

unset($this->_positionToTarget[$position]);

}

unset($this->_targetToPositions[$target]);

$this->_targetCount--;

return $this;

}

/**

A list of all potential targets

@return array

*/

public function getAllTargets()

{

return array_keys($this->_targetToPositions);

}

/**

A list of all potential targets

@return array

*/

public function getAll()

{

return array(

"targers"=>$this->_positionToTarget,

"positions"=>$this->_targetToPositions);

}

/**

Looks up the target for the given resource.

@param string $resource

@return string

*/

public function lookup($resource)

{

$targets = $this->lookupList($resource, 1);

if (empty($targets)) throw new Flexihash_Exception('No targets exist');

return $targets[0]; //0表示返回离资源位置最近的机器节点

}

/**

Get a list of targets for the resource, in order of precedence.

Up to $requestedCount targets are returned, less if there are fewer in total.

@param string $resource

@param int $requestedCount The length of the list to return

@return array List of targets

@comment 查找当前的资源对应的节点,

节点为空则返回空,节点只有一个则返回该节点,

对当前资源进行hash,对所有的位置进行排序,在有序的位置列上寻找当前资源的位置

当全部没有找到的时候,将资源的位置确定为有序位置的第一个(形成一个环)

返回所找到的节点

*/

public function lookupList($resource, $requestedCount)

{

if (!$requestedCount)

throw new Flexihash_Exception('Invalid count requested');

// handle no targets

if (empty($this->_positionToTarget))

return array();

// optimize single target

if ($this->_targetCount == 1)

return array_unique(array_values($this->_positionToTarget));

// hash resource to a position

$resourcePosition = $this->_hasher->hash($resource);

$results = array();

$collect = false;

$this->_sortPositionTargets();

// search values above the resourcePosition

foreach ($this->_positionToTarget as $key => $value)

{

// start collecting targets after passing resource position

if (!$collect && $key > $resourcePosition)

{

$collect = true;

}

// only collect the first instance of any target

if ($collect && !in_array($value, $results))

{

$results []= $value;

//var_dump($results);

}

// return when enough results, or list exhausted

//var_dump(count($results));

//var_dump($requestedCount);

if (count($results) == $requestedCount || count($results) == $this->_targetCount)

{

return $results;

}

}

// loop to start - search values below the resourcePosition

foreach ($this->_positionToTarget as $key => $value)

{

if (!in_array($value, $results))

{

$results []= $value;

}

// return when enough results, or list exhausted

if (count($results) == $requestedCount || count($results) == $this->_targetCount)

{

return $results;

}

}

// return results after iterating through both "parts"

return $results;

}

public function __toString()

{

return sprintf(

'%s{targets:[%s]}',

get_class($this),

implode(',', $this->getAllTargets())

);

}

// ----------------------------------------

// private methods

/**

Sorts the internal mapping (positions to targets) by position

*/

private function _sortPositionTargets()

{

// sort by key (position) if not already

if (!$this->_positionToTargetSorted)

{

ksort($this->_positionToTarget, SORT_REGULAR);

$this->_positionToTargetSorted = true;

}

}

}

/**

Hashes given values into a sortable fixed size address space.

@author Paul Annesley

@package Flexihash

/**

Hashes the given string into a 32bit address space.

Note that the output may be more than 32bits of raw data, for example

hexidecimal characters representing a 32bit value.

The data must have 0xFFFFFFFF possible values, and be sortable by

PHP sort functions using SORT_REGULAR.

@param string

@return mixed A sortable format with 0xFFFFFFFF possible values

*/

public function hash($string);

}

/**

Uses CRC32 to hash a value into a signed 32bit int address space.

Under 32bit PHP this (safely) overflows into negatives ints.

@author Paul Annesley

@package Flexihash

@licence http://www.opensource.org/licenses/mit-license.php

*/

class Flexihash_Crc32Hasher

implements Flexihash_Hasher

{

/* (non-phpdoc)

@see Flexihash_Hasher::hash()

*/

public function hash($string)

{

return crc32($string);

}

}

/**

Uses CRC32 to hash a value into a 32bit binary string data address space.

@author Paul Annesley

@package Flexihash

@licence http://www.opensource.org/licenses/mit-license.php

*/

class Flexihash_Md5Hasher

implements Flexihash_Hasher

{

/* (non-phpdoc)

@see Flexihash_Hasher::hash()

*/

public function hash($string)

{

return substr(md5($string), 0, 8); // 8 hexits = 32bit

// 4 bytes of binary md5 data could also be used, but

// performance seems to be the same.

}

}

/**

An exception thrown by Flexihash.

@author Paul Annesley

@package Flexihash

@licence http://www.opensource.org/licenses/mit-license.php

*/

class Flexihash_Exception extends Exception

{

}

复制代码

测试代码

复制代码

$hash = new Flexihash();

$targets=array(

"192.168.1.1:11011",

"192.168.1.1:11012",

"192.168.1.1:11013",

"192.168.1.1:11014",

"192.168.1.1:11015",

);

$hash->addTargets($targets);

for ($i=0; $i < 25; $i++) {

$resource = sprintf("format %d",$i);

var_dump($resource." --> ".$hash->lookup($resource));

}

输出

复制代码

string(30) "format 0 --> 192.168.1.1:11015"

string(30) "format 1 --> 192.168.1.1:11015"

string(30) "format 2 --> 192.168.1.1:11015"

string(30) "format 3 --> 192.168.1.1:11015"

string(30) "format 4 --> 192.168.1.1:11011"

string(30) "format 5 --> 192.168.1.1:11011"

string(30) "format 6 --> 192.168.1.1:11011"

string(30) "format 7 --> 192.168.1.1:11011"

string(30) "format 8 --> 192.168.1.1:11012"

string(30) "format 9 --> 192.168.1.1:11013"

string(31) "format 10 --> 192.168.1.1:11013"

string(31) "format 11 --> 192.168.1.1:11011"

string(31) "format 12 --> 192.168.1.1:11012"

string(31) "format 13 --> 192.168.1.1:11011"

string(31) "format 14 --> 192.168.1.1:11014"

string(31) "format 15 --> 192.168.1.1:11014"

string(31) "format 16 --> 192.168.1.1:11014"

string(31) "format 17 --> 192.168.1.1:11014"

string(31) "format 18 --> 192.168.1.1:11012"

string(31) "format 19 --> 192.168.1.1:11012"

string(31) "format 20 --> 192.168.1.1:11013"

string(31) "format 21 --> 192.168.1.1:11012"

string(31) "format 22 --> 192.168.1.1:11012"

string(31) "format 23 --> 192.168.1.1:11014"

string(31) "format 24 --> 192.168.1.1:11012"

[Finished in 0.1s]

复制代码

redis分布式代码设计

require_once("Flexihash.php");

$config=array(

"127.0.0.1:6371",

"127.0.0.1:6372",

"127.0.0.1:6373",

"127.0.0.1:6374",

);

class RedisCollect {

//redis实例

private $_redis = null;

//hash实例

private $_hash = null;

//初始化

public function __construct() {

global $config;

$this->_redis = new Redis();

$this->_hash = new Flexihash();

$this->_hash->addTargets($config);

}

public function set($key="", $value="") {

$m = $this->switchConncetion($key);

return $m->set($key, $value);

}

public function get($key) {

$m = $this->switchConncetion($key);

return $m->get($key);

}

private function switchConncetion($key) {

$hostinfo = $this->_hash->lookup($key);

$m = $this->connect($hostinfo);

return $m;

}

private function connect($hostinfo) {

list($host, $port) = explode(":", $hostinfo);

//printf("host = %s, port = %s\n",$host,$port);

if(empty($host) || empty($port)) {

return false;

}

try {

$this->_redis->connect($host, $port);

return $this->_redis;

} catch(Exception $e) {

die($e->getMessage());

}

}

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答: Redis一致性哈希是一种分布式算法,用于解决在缓存系统中增加或减少实例时可能出现的缓存击穿和缓存雪崩问题。这种算法使用哈希函数将整个哈希值空间组织成一个虚拟的圆环,将不同的缓存数据映射到圆环上的不同位置。具体而言,Redis实例的个数可能会发生变化,当增加或减少实例时,映射关系就会改变,导致大量的Redis请求找不到对应的实例。而一致性哈希算法通过对232取模的方式,将哈希值映射到圆环上的位置,从而实现了负载均衡和缓存数据的一致性存储。相比传统的取模操作,一致性哈希算法能够更好地应对增减Redis实例的情况,提高缓存命中率。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Redis-Hash一致性算法](https://blog.csdn.net/WangLi1201/article/details/79270073)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [Redis:一致性Hash算法](https://blog.csdn.net/qq_21125183/article/details/90019034)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [Redis 一致性哈希](https://blog.csdn.net/m0_54921756/article/details/125987939)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值