一、hash一致性解决的问题
如果一个缓存系统,出现某一个节点宕机,如果采用的是hash(key)%N的方式来操作具体某一台缓存机器,这时候N变成了N-1这样会导致大部分缓存失效(缓存雪崩),这个是很致命的。如果缓存失效会导致压力转移到数据库DB层,这时DB压力陡增可能导致整个服务不可用。
二、hash一致性的实现原理
大致是将缓存系统的机器(ip端口)通过hash算法[0, 2^32)将其放到这个hash环形空间里,请求过来计算其hash值,并将其指引到第一个比这个hash值大的节点。
三、如何解决当一个节点失效会导致这个节点的所有压力转移到同一个节点?
通过引入虚拟节点的方式,将这个节点宕机的压力分散到其他节点,防止压力全部转移到同一个节点的问题。
四、在动态Cache环境中,评价hash算法好坏的标准
1、平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。
2、单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。
3、分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。
4、负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同 的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。
五、java代码实现虚拟节点的hash一致性
package hash;
import java.util.LinkedList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* hash一致性,解决hash求模扩展性差以及无虚拟节点方案增删物理节点导致负载不均衡的问题
* 实现思路:将物理节点衍生出的虚拟节点通过FNV1_32_HASH算法相对均匀的放到hash环里,
* 当请求过来将请求的通过FNV1_32_HASH算法计算出hash值,根据这个hash值找到hash
* 环里第一个大于这个hash值的虚拟节点,再根据虚拟节点计算出真实的物理节点。从而实现负载
* 均衡
* @author ocean
*
*/
public class ConsistentHash {
// 待加入hash环的原始服务列表
private static String[] servers = { "192.168.30.1:1001", "192.168.30.1:1002", "192.168.30.1:1003",
"192.168.30.1:1004", "192.168.30.1:1005" };
// 真实的节点列表
private static List<String> realNodes = new LinkedList<>();
// 虚拟节点
private static TreeMap<Integer, String> virtualNodes = new TreeMap<>();
// 每一个真实节点挂载固定数量的虚拟节点
private static final int VIRTUAL_NODE = 5;
static {
for (int index = 0; index < servers.length; index++) {
realNodes.add(index, servers[index]);
}
for (String node : realNodes) {
//引入虚拟节点,解决增加或者删除物理节点导致负载不均衡的问题
for (int i = 0; i < VIRTUAL_NODE; i++) {
String virtualName = node + "&&VN" + i;
int hashValue = getHash(virtualName);
System.out.println("虚拟节点[" + virtualName + "]被添加,hash值为:"+hashValue+"," + virtualNodes.put(hashValue, virtualName));
}
}
}
/**
* 使用FNV1_32_HASH算法计算服务器的Hash值,这里不使用重写hashCode的方法,最终效果没区别
*/
private static int getHash(String str) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < str.length(); i++)
hash = (hash ^ str.charAt(i)) * p;
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
// 如果算出来的值为负数则取其绝对值
if (hash < 0)
hash = Math.abs(hash);
return hash;
}
/**
* 路由到指定的服务器
* @param node
* @return
*/
public static String getServer(String node) {
//得到带路由的结点的Hash值
int hashCode = getHash(node);
//获取大于该Hash值的所有Map
SortedMap<Integer,String> map = virtualNodes.tailMap(hashCode);
//获取第一个key
int firstKey = map.firstKey();
//获取第一个虚拟节点
String virtualName = map.get(firstKey);
return virtualName.substring(0, virtualName.indexOf("&&"));
}
//测试
public static void main(String[] args) {
String[] clientAdds = {"127.0.0.1:22","127.0.0.1:23","127.0.0.1:25"};
for(String clientAdd : clientAdds) {
System.out.println(getServer(clientAdd));
}
}
}