既然看到一致性哈希了,那就必然有不一致性哈希,我们平时说的哈希算法都是不一致的哈希,这个不用多说了,大家都熟悉的不能再熟悉了
说不熟悉的拉出去打一顿
哈希一般都是将一个大数字取模然后分散到不同的桶里,假设我们只有两个桶,有 2、3、4、5 四个数字,那么模 2 分桶的结果就是:
这时我们嫌桶太少要给哈希表扩容加了一个新桶,这时候所有的数字就需要模 3 来确定分在哪个桶里,结果就变成了:
可以看到新加了一个桶后所有数字的分布都变了,这就意味着哈希表的每次扩展和收缩都会导致所有条目分布的重新计算,这个特性在某些场景下是不可接受的。
那一致性哈希是个什么东西呢,就是属于哈希的一个升级版
在解决分布式系统负载均衡这个问题上,可以使用哈希算法让固定的一部分请求落到同一台服务器上,就是根据服务器的个数来进行相应哈希算法的设计,这样每台服务器固定处理一部分请求,并且维护这些请求的信息,起到负载均衡的作用
但是呢,如果使用普通的哈希算法就存在一个很大的问题,就是算法的伸缩性很差,当新增或者下线的时候,服务器的个数改变了,用户ID和服务器的映射关系就会出现大量的失效
这样就会导致大量的请求出现服务器的迁移,比如我们redis的服务器还是是五台,经过%5之后映射到相应的服务器上
服务器变成了8台之后,那就变成了%8,那哈希值大多可能就改变了,这样导致映射的就不是原来的服务器上了
有服务器宕机的时候,这个负载均衡映射也会改变,服务器恢复之后,负载均衡映射还是会改变
一致性哈希算法
一致性Hash算法也是使用取模的方法,不过,上述的取模方法是对服务器的数量进行取模,而一致性的Hash算法是对2的32方取模。
即,一致性Hash算法将整个Hash空间组织成一个虚拟的圆环,Hash函数的值空间为0 ~ 2^32 - 1(一个32位无符号整型),整个哈希环如下:
整个圆环以顺时针方向组织,圆环正上方的点代表0,0点右侧的第一个点代表1,以此类推。
第二步,我们将各个服务器使用Hash进行一个哈希,具体可以选择服务器的IP或主机名作为关键字进行哈希,这样每台服务器就确定在了哈希环的一个位置上,比如我们有三台机器,使用IP地址哈希后在环空间的位置如下图所示:
现在,我们使用以下算法定位数据访问到相应的服务器:
将数据Key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置
从此位置沿环顺时针查找,遇到的服务器就是其应该定位到的服务器。
例如,现在有ObjectA,ObjectB,ObjectC三个数据对象,经过哈希计算后,在环空间上的位置如下:
根据一致性算法,Object -> NodeA,ObjectB -> NodeB, ObjectC -> NodeC
一致性Hash算法的容错性和可扩展性
现在,假设我们的Node C宕机了,我们从图中可以看到,A、B不会受到影响,只有Object C对象被重新定位到Node A。
所以我们发现,在一致性Hash算法中,如果一台服务器不可用,受影响的数据仅仅是此服务器到其环空间前一台服务器之间的数据(这里为Node C到Node B之间的数据),其他不会受到影响。
如下图所示:
另外一种情况,现在我们系统增加了一台服务器Node X,如图所示:
此时对象ObjectA、ObjectB没有受到影响,只有Object C重新定位到了新的节点X上。如上所述:一致性Hash算法对于节点的增减都只需重定位环空间中的一小部分数据,有很好的容错性和可扩展性。
数据倾斜问题
在一致性Hash算法服务节点太少的情况下,容易因为节点分布不均匀面造成数据倾斜(被缓存的对象大部分缓存在某一台服务器上)问题,如图特例:
这时我们发现有大量数据集中在节点A上,而节点B只有少量数据。
为了解决数据倾斜问题,一致性Hash算法引入了虚拟节点机制,即对每一个服务器节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。
具体操作可以为服务器IP或主机名后加入编号来实现,实现如图所示:
数据定位算法不变,只需要增加一步:虚拟节点到实际点的映射。
所以加入虚拟节点之后,即使在服务节点很少的情况下,也能做到数据的均匀分布。
既然一致性哈希有这么多好的特性,那为啥主流的哈希都是非一致的呢?
主要一个原因在于查找效率上,普通的哈希查询一次哈希计算就可以找到对应的桶了,算法时间复杂度是 O(1),而一致性哈希需要将排好序的桶组成一个链表,然后一路找下去,k 个桶查询时间复杂度是 O(k),所以通常情况下的哈希还是用不一致的实现。
这里再提一嘴
之所以说一致性哈希有用,这是一个思想,一个方案,我们在平时中很多地方都会涉及到这种思想,比如Redis集群,采用这种方案可以更好的去应对集群节点的动态的增删,再比如分库分表,这也是一种思路