什么是一致性hash
一致性哈希算法在1997年由麻省理工学院提出,是一种特殊的哈希算法,目的是解决分布式缓存的问题。 在移除或者添加一个服务器时,能够尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系。
一致性哈希解决了简单哈希算法在分布式哈希表( Distributed Hash Table,DHT) 中存在的动态伸缩等问题 [2] 。引用百度百科
普通hash的缺陷
用过memcache和redis的人一定都对一致性hash应该都不陌生。在没有一致性hash时,我们原来的选择是hash取模的方式,
根据key进行hash然后与缓存服务器的数量进行取模,从而保存到对应的服务器上。如下图所示,我们有10台服务器,分别对key为1,5,8,13,17几个数据进行缓存,分别对应服务器为1,3,5,7,8。
当我们的缓存服务器不够用时,我们面临这需要增加服务器的这种情况,比如我增加一台服务器,现在有11台服务器,分别用key为1,5,8,13,17 key进行数据获取,你会发现他们分别对应服务器为1,2,5,6,8。
你仔细对比,会发现分别存在两个key未命中的情况,当缓存中存在大量数据时,当在高并发的时候可能会导致数据被击穿的风险。有没一种什么算法可以解决这个问题呢,那就是我们今天要说的一致性hahs。
核心设计思路:
1.我们分别对key:1,2,3的缓存数据进行存储,当我们分别对key进行hash,他们分别对应环中的位置,hash(1)离A服务器最近,所以他存在A服务器上。同样key:2,3也如同上述。
2.当我们需要对key:2的缓存进行获取时,当我们某一个缓存服务器(B)宕机时,会沿这个环顺时针找到C服务器。
3.当我们发现缓存服务器不够用时,例如我们需要新增一个缓存服务器D,他在hash环中的位于A-B之间,如下图所示当我们在对key:2的缓存进去获取时,会直接定位到D服务器,从新将一个新的结果缓存到D服务器。
4.服务器在环中分布不均衡的的情况,如下图所示有可能出现大规模的数据都集中在A服务器上,所以这时候我们需要引入虚拟节点的概念,让这些服务器在这个环上分配相对均衡。
具体java代码
public class Node<T> {
private String ip;
private String name;
public Node(String ip, String name) {
this.ip = ip;
this.name = name;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return ip;
}
}
public class ConsistentHash<T> {
// Hash函数接口
private final IHashService iHashService;
// 每个机器节点关联的虚拟节点数量
private final int numberOfReplicas;
// 环形虚拟节点
private final SortedMap<Long, T> circle = new TreeMap<Long, T>();
public ConsistentHash(IHashService iHashService, int numberOfReplicas, Collection<T> nodes) {
this.iHashService = iHashService;
this.numberOfReplicas = numberOfReplicas;
for (T node : nodes) {
add(node);
}
}
/**
* 增加真实机器节点
*
* @param node T
*/
public void add(T node) {
for (int i = 0; i < this.numberOfReplicas; i++) {
circle.put(this.iHashService.hash(node.toString() + i), node);
}
}
/**
* 删除真实机器节点
*
* @param node T
*/
public void remove(T node) {
for (int i = 0; i < this.numberOfReplicas; i++) {
circle.remove(this.iHashService.hash(node.toString() + i));
}
}
public T get(String key) {
if (circle.isEmpty()) return null;
long hash = iHashService.hash(key);
// 沿环的顺时针找到一个虚拟节点
if (!circle.containsKey(hash)) {
SortedMap<Long, T> tailMap = circle.tailMap(hash);
hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
}
return circle.get(hash);
}
}
public interface IHashService {
Long hash(String key);
}
public class HashService implements IHashService {
/**
* MurMurHash算法,性能高,碰撞率低
*
* @param key String
* @return Long
*/
public Long hash(String key) {
ByteBuffer buf = ByteBuffer.wrap(key.getBytes());
int seed = 0x1234ABCD;
ByteOrder byteOrder = buf.order();
buf.order(ByteOrder.LITTLE_ENDIAN);
long m = 0xc6a4a7935bd1e995L;
int r = 47;
long h = seed ^ (buf.remaining() * m);
long k;
while (buf.remaining() >= 8) {
k = buf.getLong();
k *= m;
k ^= k >>> r;
k *= m;
h ^= k;
h *= m;
}
if (buf.remaining() > 0) {
ByteBuffer finish = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
finish.put(buf).rewind();
h ^= finish.getLong();
h *= m;
}
h ^= h >>> r;
h *= m;
h ^= h >>> r;
buf.order(byteOrder);
return h;
}
}
public class Test {
private static final String IP_PREFIX = "192.168.1.";
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<String, Integer>();
List<Node<String>> nodes = new ArrayList<Node<String>>();
for (int i = 1; i <= 10; i++) {
map.put(IP_PREFIX + i, 0);
Node<String> node = new Node<String>(IP_PREFIX + i, "node" + i);
nodes.add(node);
}
IHashService iHashService = new HashService();
ConsistentHash<Node<String>> consistentHash = new ConsistentHash<Node<String>>(iHashService, 200, nodes);
for (int i = 0; i < 10000; i++) {
String data = UUID.randomUUID().toString() + i;
Node<String> node = consistentHash.get(data);
map.put(node.getIp(), map.get(node.getIp()) + 1);
}
for (int i = 1; i <= 10; i++) {
System.out.println(IP_PREFIX + i + "每个节点记录条数:" + map.get(IP_PREFIX + i));
}
}
}