事情由来
公司要做一个基于discuz的论坛,需要支持同时在线千万级别,而discuz用于判断用户是否登录依据”session“常常是保存在数据库里面的,并且基于一张表保存,那么,当同时有大量用户挤入,会不会造成数据库无法承受而导致运行缓慢?答案是肯定的。那么,基于这种原因,我打算用分布式redis来解决这个问题。按着不同的维度,这里可以是地区,活跃度等把用户登录信息分布存储在不同的redis中。
常用的负载均衡算法
在做服务器负载均衡时候可供选择的负载均衡的算法有很多,包括:轮循算法(Round Robin)、哈希算法(HASH)、最少连接算法(Least- Connection)、响应速度算法(Response Time)、加权法(Weighted )等。其中哈希算法是最为常用的算法。那么,如果换做是你,当一个数据需要存入redis中,你会怎么选择存储的redis?这里我们假设有四台redis1,redis2,redis3,redis4。
1.hashResult = hash();//这里hash算法是你自定义算法,总之会得出一个unsigned int型的数据
2.hashResult % 4 = 1;//假如余数为1,
好,当前这个要存储的数据存放在第一台redis中。以此类推,把所有的数据分别进行hash和取模,存放到余数的redis中。这样的方法会有一个致命的问题,当四台redis的其中一台redis1死机(down了),那么存放在这台redis1中的数据就丢失了,相当于有 (N-1)/ N,即 3/4 台的数据需要重新计算存放位置。这对系统是致命的打击。那么怎么能合理存放数据,并且能够任意的增加或删除redis机器呢?答案是 一致性hash.
一致性hash介绍
关于一致性hash的介绍,网上有很多,大家自行google即可。这里我简单的盗图说明一下,哈哈哈。省事!
由于hash算法结果一般为unsigned int型,因此对于hash函数的结果应该均匀分布在[0,232-1]间,如果我们把一个圆环用232 个点来进行均匀切割
,首先按照hash(key)函数算出服务器(节点)的哈希值, 并将其分布到0~232的圆上。用同样的hash(key)函数求出需要存储数据的键的哈希值,并映
射到圆上。然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器(节点)上。如下
新增一个redis节点的时候,只有在圆环上新增节点逆时针方向的第一个redis节点的数据会受到影响。删除一个redis节点的时候,只有在圆环上原来删除
节点顺时针方向的第一个节点的数据会受到影响,因此通过Consistent Hashing很好地解决了负载均衡中由于新增节点、删除节点引起的hash值颠簸问题
。如下图:
学习困惑
1.为什么hash算法结果会分布在1-2^32 -1的范围呢?
2.那一般企业redis大多不超过十台,因此每台redis之间的环的角度可能出现不平衡性(例如redis1占用了一半圆,其余的redis来分剩余一半圆环),所以当数据量较大的时候,运用一致性hash还是会出现数据分布不均匀的情况,从而破坏了hasn算法的ping。怎么办?
这里引入虚拟节点的概念:虚拟节点可以认为是实际节点的复制品(replicas),其实就是redis1又生了很多个孩子redis1-1,redis1-2,这样redis1家族通过hash算出来的位置会均匀的分布在圆环中各个位置。在进行负载均衡时候,落到虚拟节点的哈希值实际就落到了实际的节点上。由于所有的实际节点是按照相同的比例复制成虚拟节点的,因此解决了节点数较少的情况下哈希值在圆环上均匀分布的问题。
3.我们运用一致性hash的算法的最终目的是干什么?
简单的说,是用来取出一个节点。这个节点用来存放或者读取你想要存放或者读取的数据。
php代码实现
其实,很多人都了解过一致性hash算法,只是对它如何实际运用到场景中不太明白,那么我们就用php代码实际来实现一下该算法,并模拟一个实际场
景。
<?php
/**
* 一致性hash的算法实现,用于解决redis的分布问题
* @package default
* @author qichangchun<qichangchun@gomeplus.com>
* @date: 2016/5/25
* @time: 10:43
*/
class FiexHash {
/**
* @var $_hasher 加密方式
* 加密方式的最后加密结果实质上是为了确定节点或者虚拟节点的存储位置position
*/
private $_hasher;
/**
* @var $_targetCount
* 节点数。例如已挂载四个redis节点,则$_targetCount=4
*/
private $_targetCount;
/**
* @var $_replicas
* 虚拟节点,每个redis节点生成$_replicas个虚拟节点
*/
private $_replicas;
/**
* @var $_target2Position
* 每个redis节点在圆环上对应的位置
* type Array
*/
private $_target2Position;
/**
* @var $_position2Target
* 每个圆环位置范围对应的实际节点
* type Array
*/
private $_position2Target;
/**
* @var $_hasSorted
* 是否已经把圆环上的节点进行排序,每次增加或删除节点都需要进行节点排序,以便可以顺时针查找数据存放最近节点
* type bool
*/
private $_hasSorted;
public function __construct($hasher, $replicas = 64) {
if (!in_array(get_class($hasher), array('HasherType_Crc32'))) {
throw new Exception("不可用的加密");
}
$this->_hasher = $hasher;
$this->_replicas = $replicas;
$this->_target2Position = $this->_position2Target = array();
$this->_targetCount = 0;
}
/**
* 新增节点到集群中
* @param $targetNew 新增的节点IP(一般为IP)
*/
public function addTarget($targetNew) {
//判断是否已经存在该节点
if (!isset($this->_target2Position[$targetNew])) {
//不存在,增加虚拟节点,并记录虚拟节点的位置以及每个位置对应的节点
$this->_target2Position[$targetNew] = array();
for ($i = 0; $i < $this->_replicas; $i++) {
$position = $this->_hasher->hash($targetNew . $i);
$this->_position2Target[$position] = $targetNew;
$this->_target2Position[$targetNew][] = $position;
}
$this->_hasSorted = false;
$this->_targetCount++;
} else {
throw new Exception("该结点已经存在");
}
return $this;
}
/**
* 移除节点
* @param $targetRemove 要移除的节点IP
* @return mixed
*/
public function removeTarget($targetRemove) {
if (!empty($this->_target2Position[$targetRemove])) {
//移除位置信息和节点信息的对应关系
foreach ($this->_target2Position as $k => $p) {
unset($this->_target2Position[$k]);
unset($this->_position2Target[$p]);
}
$this->_hasSorted = false;
$this->_targetCount++;
}
return $this;
}
/**
* @param $resource 要读取或存的数据,例如“username”
* @param $resultCount 返回节点的个数
* @return 存放该数据的节点
*/
public function getTarget($resource,$resultCount = 1) {
$resourceHash = $this->_hasher->hash($resource);
if (!$this->_hasSorted) {
$this->_sortTargetAsc();
}
if($this->_targetCount == 1){
return array_unique(array_values($this->_position2Target));//返回当前唯一的节点
}
$result = array();//返回的节点,如 [192.168.1.1,192.168.1.2]
foreach($this->_position2Target as $position => $target){
//echo '</br>'.$position.'-->'.$target;
if($position > $resourceHash){
if(count($result) < $resultCount && !in_array($target,$result)){
$result[] = $target;
}
}
}
return $result;
}
/**
* 讲所有节点及虚拟节点进行排序
*/
private function _sortTargetAsc(){
ksort($this->_position2Target, SORT_REGULAR);
$this->_hasSorted = true;
}
}
/**
* Interface HasherType
* hash算法接口,实现hash加密方法
*/
interface HasherType {
public function hash($string);
}
/**
* Class HasherType_Crc32
* crc32hash算法实现
*/
class HasherType_Crc32 implements HasherType{
public function hash($string) {
return crc32($string);
}
}
/**
* 例子
*/
$redis = array('192.168.1.1','192.168.1.2','192.168.1.3','192.168.1.4');
$instance = new FiexHash(new HasherType_Crc32(),12);
$instance -> addTarget("192.168.1.101");
$instance -> addTarget("192.168.1.102");
$instance -> addTarget("192.168.1.103");
$instance -> addTarget("192.168.1.104");
$instance -> addTarget("192.168.1.105");
$instance -> addTarget("192.168.1.106");
$instance -> addTarget("192.168.1.107");
$instance -> addTarget("192.168.1.108");
$res = $instance ->getTarget('test2',1);
var_dump($res);