- 一致性hash算法的实现
-
1.hashCode的计算,字符串hash算法
有很多hash算法,字符串hash需要达到的两个目标就是冲突要少,也就是分散和考虑到每个字符。
参考:字符串hash分析
JDK中使用的累乘因子为31的BKDRHash算法,这是一个经验数字,具有相对比较好的效率和分散。暴雪使用的hash算法主要是针对效率的。
为了尽量发散,使用JDK中HashMap的方法。
2.一致性Hash的思想public int hash(String str) { if(str == null) return 0; int h = 0; h ^= str.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
假如现在是客户需要服务器来服务,但是有多个服务器,需要选择一个,客户hash后得到一个int型的数h,但是现在只有n个服务器,怎么来选择服务器呢?
传统的取余方法是:h%n 得到一个小于n的数,再对应是那一个服务器就行。
但是这样带来一个很严重的问题,扩展性。当需要添加一个服务器时(或者崩溃),重新计算h%(n+1),其中有些客户端就需要变更服务器。这将会给系统带来很多不必要的损失。
所以一致性Hash就是为了生存在动态服务器内的。
首先求出服务器的哈希值, 并将其配置到0~2^32的圆上。 然后用同样的方法求出客户端的哈希值,并映射到圆上。 然后从数据映射到的位置开始顺时针查找,第一个服务器服务它。 如果超过2^32找不到服务器,就会保存到第一台服务器上。
使用虚拟节点的思想,为每个服务器在圆上分配100~200个点。这样就能抑制分布不均匀, 最大限度地减小服务器增减时的缓存重新分布。
//C为client客户端,S为service服务器 public class ConsistentHash { //虚拟节点的个数,默认200 private int virtualNum; private long unit;//移动 //存储服务器的容器 private TreeMap map = new TreeMap<>(); public ConsistentHash() { this(200); } public ConsistentHash(int virtualNum) { this.virtualNum = virtualNum; unit = ((long)(Integer.MAX_VALUE/virtualNum)<<1)-2; } //添加服务器结点 public void add(S s){ int hash = hash(s); //保证分散到Integer的范围内 for (int i = 0; i < virtualNum; i++) map.put((int)(hash+i*unit), s); } //删除服务器结点 public void remove(S s){ int hash = hash(s); //保证分散到Integer的范围内,先转换为int再转换为Integer for (int i = 0; i < virtualNum; i++) map.remove((Integer)(int)(hash+i*unit)); } //获取服务器 public S get(C c){ if(map.isEmpty()) return null; int hash = hash(c); //TreeMap独有的取上和取下方法 //ceiling 和 floor (Math中是ceil) Entry entry = map.ceilingEntry(hash); if(entry == null) //空就代表需要回环了 return map.firstEntry().getValue(); else return entry.getValue(); } //再次分散的hash方法 private int hash(Object o) { if(o == null) return 0; int h = 0; h ^= o.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } }