转载自:http://blog.csdn.net/lovingprince/article/details/4645448

    网站为了支撑更大的用户访问量,往往需要对用户访问的数据做cache,对于访 问量特别大的门户网站,一般都提供专门的cache服务机群和负载均衡来专门处理缓存,负载均衡的算法很多,轮循算法、哈希算法、最少连接算法、响应速度 算法等,hash算法是比较常用的一种,它的常用思想是先计算出一个hash值,然后使用 CRC余数算法将hash值和机器数mod后取余数,机器的编号可以是0到N-1(N是机器数),计算出的结果一一对应即可。

    我们知道缓存最关键的就是命中率这个因素,如果命中率非常低,那么缓存也就失去了它的意义,因此实际生产环境中我们的一个重要目标就是提高缓存命中率。如 上所述,采用一般的CRC取余的hash算法虽然能达到负载均衡的目的,但是它存在一个严重的问题,那就是如果我们其中一台服务器down掉,那么我们就 需要在计算缓存过程中将这台服务器去掉,即N台服务器,目前就只有N-1台提供缓存服务,此时需要一个rehash过程,而reash得到的结果将导致正 常的用户请求不能找到原来缓存数据的正确机器,其他N-1台服务器上的缓存数据将大量失效,此时所有的用户请求全部会集中到数据库上,严重可能导致整个生 产环境挂掉.

 

      举个例子,有5台服务器,他们编号分别是0(A),1(B),2(C),3(D),4(E)  ,正常情况下,假设用户数据hash值为12,那么对应的数据应该缓存在12%5=2号服务器上,假设编号为3的服务器此时挂掉,那么将其移除后就得到一 个新的0(A),1(B),2(C),3(E)(注:这里的编号3其实就是原来的4号服务器)服务器列表,此时用户来取数据,同样hash值为 12,rehash后的得到的机器编号12%4=0号服务器,可见,此时用户到0号服务器去找数据明显就找不到,出现了cache不命中现象,如果不命中 此时应用会从后台数据库重新读取数据再cache到0号服务器上,如果大量用户出现这种情况,那么后果不堪设想。同样,增加一台缓存服务器,也会导致同样 的后果,感兴趣的读者可以自行推敲。

 

     可以有一种设想,要提高命中率就得减少增加或者移除服务器rehash带来的影响,那么有这样一种算法么?Consistent hashing算法就是这样一种hash算法,它的算法思想是:首先求出服务器(节点)的哈希值,并将其配置到0~2^32的圆上。然后用同样的方法求出 存储数据的键的哈希值,并映射到圆上。然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。如果超过2^32仍然找不到服务器,就 会保存到第一台服务器上。下面有一张比较经典的图,直接用过来,不修改了。

 

    

        图一  Consistent Hashing原理示意图

 

     这里有四台服务器,我们假设增加一台服务器Node5,可以看到,它影响的数据只是在增加Node5逆时针方向的数据会受到影响。同样,删除其中一台服务器,例如删除服务器node4,那么影响的数据也只是node4上缓存的数据。

   

   图二 Consistent Hashing添加服务器

 

   Consistent Hashing最大限度地抑制了hash键的重新分布。另外要取得比较好的负载均衡的效果,往往在服务器数量比较少的时候需要增加虚拟节点来保证服务器能 均匀的分布在圆环上。因为使用一般的hash方法,服务器的映射地点的分布非常不均匀。使用虚拟节点的思想,为每个物理节点(服务器)在圆上分配 100~200个点。这样就能抑制分布不均匀,最大限度地减小服务器增减时的缓存重新分布。用户数据映射在虚拟节点上,就表示用户数据真正存储位置是在该 虚拟节点代表的实际物理服务器上。
下面有一个图描述了需要为每台物理服务器增加的虚拟节点。

 

  图三 虚拟节点倍数- 物理节点数关系图

 

      x轴表示的是需要为每台物理服务器扩展的虚拟节点倍数(scale),y轴是实际物理服务器数,可以看出,当物理服务器的数量很小时,需要更大的虚拟节 点,反之则需要更少的节点,从图上可以看出,在物理服务器有10台时,差不多需要为每台服务器增加100~200个虚拟节点才能达到真正的负载均衡。

 

 

下面是一个简单的java参考实现:

 

   
   
   
   
  1.  1. import java.util.Collection;   
  2.  2. import java.util.SortedMap;   
  3.  3. import java.util.TreeMap;   
  4.  4.    
  5.  5. public class ConsistentHash<T> {   
  6.  6.    
  7.  7.  private final HashFunction hashFunction;   
  8.  8.  private final int numberOfReplicas;   
  9.  9.  private final SortedMap<Integer, T> circle = new TreeMap<Integer, T>();   
  10. 10.    
  11. 11.  public ConsistentHash(   
  12. 12.       HashFunction hashFunction, //hash算法   
  13. 13.       int numberOfReplicas,//虚拟节点数   
  14. 14.       Collection<T> nodes//物理节点   
  15. 15.    ) {   
  16. 16.    this.hashFunction = hashFunction;   
  17. 17.    this.numberOfReplicas = numberOfReplicas;   
  18. 18.    
  19. 19.    for (T node : nodes) {   
  20. 20.      add(node);   
  21. 21.    }   
  22. 22.  }   
  23. 23.    
  24. 24.  public void add(T node) {   
  25. 25.    for (int i = 0; i < numberOfReplicas; i++) {   
  26. 26.      circle.put(hashFunction.hash(node.toString() + i), node);   
  27. 27.    }   
  28. 28.  }   
  29. 29.    
  30. 30.  public void remove(T node) {   
  31. 31.    for (int i = 0; i < numberOfReplicas; i++) {   
  32. 32.      circle.remove(hashFunction.hash(node.toString() + i));   
  33. 33.    }   
  34. 34.  }   
  35. 35.     
  36. 36. //关键算法   
  37. 37.  public T get(Object key) {   
  38. 38.    if (circle.isEmpty()) {   
  39. 39.      return null;   
  40. 40.    }   
  41. 41.    int hash = hashFunction.hash(key);   
  42. 42.    if (!circle.containsKey(hash)) {   
  43. 43.      SortedMap<Integer, T> tailMap = circle.tailMap(hash);   
  44. 44.      hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();   
  45. 45.    }   
  46. 46.    return circle.get(hash);   
  47. 47.  }   
  48. 48.    
  49. 49. }   

 附:

java.util
接口 SortedMap

public interface SortedMap
     
     
     
     
extends Map

保证按照键的升序排列的映射,可以按照键的自然顺序(参见 Comparable 接口)进行排序,或者通过创建有序映射时提供的比较器进行排序。对有序映射的集合视图(由 entrySetkeySetvalues 方法返回)进行迭代时,此顺序就会反映出来。要采用此排序,还需要提供一些其他操作(此接口是相似于 SortedSet 接口的映射)。

插入有序映射的所有键都必须实现 Comparable 接口(或者被指定的比较器所接受)。另外,所有这些键都必须是可互相比较的k1.compareTo(k2)(或 comparator.compare(k1, k2))对有序映射中的任意两个元素 k1k2 都不得抛出 ClassCastException。试图违反这些限制将导致违反方法或者构造方法的调用,从而抛出 ClassCastException

注意,如果有序映射正确实现了 Map 接口,则有序映射所保持的顺序(无论是否明确提供了比较器)都必须保持相等一致性。(相等一致性 的精确定义请参阅 Comparable 接口或 Comparator 接口)。这也是因为 Map 接口是按照 equals 操作定义的,但有序映射使用它的 compareTo(或 compare)方法对所有键进行比较,因此从有序映射的观点来看,此方法认为相等的两个键就是相等的。即使顺序没有保持相等一致性,树映射的行为仍然 定义良好的,只不过没有遵守 Map 接口的常规协定。

所有通用有序映射实现类都应该提供 4 个“标准”构造方法:1) void(不带参数)构造方法,创建空的有序映射,按照键的自然顺序 排序。2) 带有一个 Comparator 类型参数的构造方法,创建一个空的有序映射,根据指定的比较器排序。3) 带有一个 Map 类型参数的构造方法,创建一个键-值映射关系与参数相同的有序映射,按照键的自然顺序排序。4) 带有一个有序映射类型参数的构造方法,创建一个新的有序映射,键-值映射关系及排序方法与输入的有序映射相同。除了 JDK 实现(TreeMap 类)遵循此建议外,无法保证强制实施此建议(因为接口不能包含构造方法)。

此接口是 Java Collections Framework 的成员。

 

SortedMap tailMap(K fromKey)
返回有序映射的部分视图,其键大于或等于 fromKey。返回的有序映射由此有序映射支持,因此返回有序映射中的更改将反映在此有序映射中,反之亦然。返回的映射支持此有序映射支持的所有可选映射操作。

如果用户试图插入超出指定范围的键,则此方法返回的映射将抛出 IllegalArgumentException

注:此方法总是返回包括(低)终结点的视图。如果需要不含此终结点的视图,而且元素类型允许计算给定值的后继值,则只需要请求以 successor(lowEndpoint) 为界的 tailMap。例如,假设 m 是一个用字符串作为键的映射。下面的语句将得到一个包含 m 中键严格大于 low 的所有键-值映射关系的视图:

    Map tail = m.tailMap(low+"\0");

 

参数:
fromKey - tailMap 的低终结点(包括)。
返回:
此有序映射指定的最终范围的视图。
抛出:
ClassCastException - 如果 fromKey 与此映射的比较器不兼容(如果此映射没有比较器,并且 fromKey 没有实现 Comparable)。如果 fromKey 不能和映射中的当前键比较,则实现可能抛出此异常,但不要求。
IllegalArgumentException - 如果此映射本身是一个 subMap、headMap 或 tailMap,并且 fromKey 不在 subMap、headMap 或 tailMap 指定的范围之内。
NullPointerException - 如果 fromKeynull,并且此有序映射不能接受 null 键。

firstKey

K firstKey()
返回有序映射中当前第一个(最小的)键。

 

返回:
有序映射中当前第一个(最小的)键。
抛出:
NoSuchElementException - 如果是空映射。