1. 一致性 hash 算法应用领域


  • 分布式数据存储场景:缓存、ES、Hadoop、分布式数据库

    2. 一致性 hash 算法引出


单节点服务器,以缓存为例

  • 使用缓存的目的:提升数据访问性能,缓解数据库压力!

image.png

  • 常用缓存中间件

    • Memcached
    • Redis

      高并发问题

      互联网公司的分布式高并发系统有什么特点?

  • 高并发

  • 海量数据
  • 高并发问题如何处理?(应用集群)

image.png

单机缓存能扛起高并发吗?

  • 不能

    Redis、Memcached 的并发能力有多强?

  • 很强:10w 并发

  • 如果并发量达 30w 怎么办?(主从集群)

image.png

海量数据对缓存有什么影响?(分片集群)

  • 缓存的数据量很大,会超出单机内存容量。

image.png

  • 数据如何均衡分布到缓存集群的节点上?

    3. 均衡分布方式


方式一:hash(key) % 集群节点数

image.png

  • 示例:3 个节点的集群,数据 zp:888
    • 假设:hash(zp) = 200
    • 则数据放到服务器 2 上 200 % 3 = 2
  • 搞促销活动,临时增加一台服务器来提高集群性能,方便扩展集群吗?
    • hash(zp) = 200200 % 4 = 0
  • 增加一个节点后,有多大比例的数据缓存命不中?
    • 3 -> 43/4
    • 99 -> 10099%
  • 对系统会有什么影响?(缓存雪崩)
    • 大量缓存命不中,就会访问数据库。
    • 瞬间失去了缓存的分压,数据库不堪重负,崩溃。
  • 对程序员的影响

    • 加班:凌晨3/4点,扩容,预热数据。

      方式二:一致性 hash 算法

      image.png
  • hash 值一个非负整数,把非负整数的值范围做成一个圆环。(0 - int.max)

  • 对集群的节点的某个属性求 hash 值(如节点名称),根据 hash 值把节点放到环上。
  • 对数据的 key 求 hash,一样的把数据也放到环上,按顺时针方向,找离它最近的节点,就存储到这个节点上。
  • 新增节点能均衡缓解原有节点的压力吗?
    • 不能。
    • 假如节点 3 靠近节点 1,影响数据少,但是会导致节点 3 上数据少。
    • 假如节点 3 靠近节点 2,影响数据多,并且导致节点 2 上数据很少。
  • 集群的节点一定会均衡分布在环上吗?

image.png

  • 不一定,hash 是散列值。
  • 节点 0、2、3 处于“打酱油”状态。

    方式三:一致性 Hash 算法 + 虚拟节点

  • 一致性 Hash 算法,只是解决缩容、扩容问题(程序员加班问题)。
  • 引入虚拟节点解决数据分布不均衡问题。

image.png

4. 一致性 Hash 算法需要考虑的问题


Hash 算法选择

  • hashCode(),不够散列,会有负值(取绝对值即可)
  • 其他 hash 算法:比如 CRC32_HASH、FNV1_32_HASH、KETAMA_HASH 等,其中 KETAMA_HASH 是默认的 memcached 推荐的一致性 Hash 算法。

    虚拟节点放到环上,具体是要做什么?

  • 根据 Hash 值排序存储。

  • 排序存储要被快速查找。
  • 这个排序存储还要能方便变更。
  • 考虑:
    • Array:排序可以做到,但是变更会有问题,并且快速查找也会有问题(根据值查找)。
    • 链表:变更和查找不方便。
    • 二叉树:变更和查找都很快。会有遍历层级过高,特殊情况会退化成链表的问题。
    • 红黑树:解决层级过高的问题。
    • TreeMap

      5. 代码实现一致性 Hash 算法


FNV1_32_HASH

  1. public class FNV1_32_HASH {
  2. /** 尽量让 hash 值散列开 */
  3. public static int getHash(String str) {
  4. final int p = 16777619;
  5. int hash = (int) 2166136261L;
  6. for (int i = 0; i < str.length(); i ++) {
  7. hash = (hash ^ str.charAt(i)) * p;
  8. }
  9. hash += hash << 13;
  10. hash ^= hash >> 7;
  11. hash += hash << 3;
  12. hash ^= hash >> 17;
  13. hash += hash << 5;
  14. // 如果算出来的值为负数则取其绝对值
  15. if (hash < 0) {
  16. hash = Math.abs(hash);
  17. }
  18. return hash;
  19. }
  20. }

测试 String.hashCode() 和 以上算法的均衡度

  1. public class HashTest {
  2. public static void main(String[] args) {
  3. System.out.println("hashCode(): ");
  4. System.out.println("192.168.0.0:1111 的哈希值:" + "192.168.0.0:1111".hashCode());
  5. System.out.println("192.168.0.1:1111 的哈希值:" + "192.168.0.1:1111".hashCode());
  6. System.out.println("192.168.0.2:1111 的哈希值:" + "192.168.0.2:1111".hashCode());
  7. System.out.println("192.168.0.3:1111 的哈希值:" + "192.168.0.3:1111".hashCode());
  8. System.out.println("192.168.0.4:1111 的哈希值:" + "192.168.0.4:1111".hashCode());
  9. System.out.println("192.168.1.0:1111 的哈希值:" + "192.168.1.0:1111".hashCode());
  10. System.out.println();
  11. System.out.println("FNV1_32_HASH: ");
  12. System.out.println("192.168.0.0:1111 的哈希值:" + FNV1_32_HASH.getHash("192.168.0.0:1111"));
  13. System.out.println("192.168.0.1:1111 的哈希值:" + FNV1_32_HASH.getHash("192.168.0.1:1111"));
  14. System.out.println("192.168.0.2:1111 的哈希值:" + FNV1_32_HASH.getHash("192.168.0.2:1111"));
  15. System.out.println("192.168.0.3:1111 的哈希值:" + FNV1_32_HASH.getHash("192.168.0.3:1111"));
  16. System.out.println("192.168.0.4:1111 的哈希值:" + FNV1_32_HASH.getHash("192.168.0.4:1111"));
  17. System.out.println("192.168.1.0:1111 的哈希值:" + FNV1_32_HASH.getHash("192.168.1.0:1111"));
  18. }
  19. }

简单实现一个一致性算法

  1. public class ConsistenceHash {
  2. /** 物理节点集合 */
  3. private List<String> realNodes = new ArrayList<>();
  4. /** 虚拟节点数,用户进行指定 */
  5. private int virtualNums = 100;
  6. /**
  7. * 物理节点和虚拟节点的对应关系
  8. * key:物理节点
  9. * value:虚拟 hash 值
  10. */
  11. private Map<String, List<Integer>> real2VirtualMap = new HashMap<>();
  12. /**
  13. * 排序存储结构,红黑树
  14. * key:虚拟节点 hash 值
  15. * value:物理节点
  16. */
  17. private SortedMap<Integer, String> sortedMap = new TreeMap<>();
  18. public ConsistenceHash() { }
  19. public ConsistenceHash(int virtualNums) {
  20. this.virtualNums = virtualNums;
  21. }
  22. /**
  23. * 添加服务节点
  24. */
  25. public void addServer(String node) {
  26. realNodes.add(node);
  27. String vnode = null;
  28. // i:v-001,count:产生了虚拟节点的次数
  29. int i = 0, count = 0;
  30. List<Integer> virtualNodes = new ArrayList<>();
  31. real2VirtualMap.put(node, virtualNodes);
  32. // 创建虚拟节点,并且放到圆环上去(排序存储)
  33. while (count < this.virtualNums) {
  34. i++;
  35. vnode = node + "V-" + i;
  36. int hashValue = FNV1_32_HASH.getHash(vnode);
  37. // 解决 hash 碰撞
  38. if (! sortedMap.containsKey(hashValue)) {
  39. virtualNodes.add(hashValue);
  40. sortedMap.put(hashValue, node);
  41. count ++;
  42. }
  43. }
  44. }
  45. /**
  46. * 移除服务器
  47. */
  48. public void removeServer(String node) {
  49. List<Integer> virtualNodes = real2VirtualMap.get(node);
  50. if (virtualNodes != null && virtualNodes.size() > 0) {
  51. for (Integer hashVal : virtualNodes) {
  52. sortedMap.remove(hashVal);
  53. }
  54. }
  55. realNodes.remove(node);
  56. real2VirtualMap.remove(node);
  57. }
  58. /**
  59. * 根据 key 查找物理节点
  60. */
  61. public String getServer(String key) {
  62. // 根据 key 产生 hash 值
  63. int hashValue = FNV1_32_HASH.getHash(key);
  64. SortedMap<Integer, String> subMap = sortedMap.tailMap(hashValue);
  65. if (subMap == null || subMap.isEmpty()) {
  66. return sortedMap.get(sortedMap.firstKey());
  67. }
  68. return sortedMap.get(subMap.firstKey());
  69. }
  70. public static void main(String[] args) {
  71. ConsistenceHash ch = new ConsistenceHash();
  72. ch.addServer("192.168.1.10");
  73. ch.addServer("192.168.1.11");
  74. ch.addServer("192.168.1.12");
  75. for (int i = 0; i < 10; i ++) {
  76. System.out.println("a" + i + " 对应的服务器是:" + ch.getServer("a" + i));
  77. }
  78. }
  79. }