每日一博 - 一致性哈希:分布式系统的数据分配利器

文章目录

  • 概述
  • 1、一致性哈希算法的诞生背景
  • 2、一致性哈希的基本原理
  • 3、一致性哈希的优势和挑战
  • 4、虚拟节点的引入
  • 5、 Java代码实现

每日一博 - 一致性哈希:分布式系统的数据分配利器_第1张图片

概述

在现代分布式系统中,如何高效地将数据分布在多个服务器上,同时保证扩展性和容错性,是一个至关重要的问题。一致性哈希算法(Consistent Hashing)正是为了解决这些挑战而设计的。今天,我们来深入探讨这个经典的分布式算法,包括它的基本原理、优缺点,以及实际应用中的Java代码实现。

1、一致性哈希算法的诞生背景

在大规模分布式系统中,数据往往需要存储在多个节点(如服务器)上。传统的哈希算法可以将数据均匀地分配在固定数量的节点上。然而,一旦有新的节点加入或已有节点退出,这种方式就会导致数据的大规模迁移,对系统的扩展性造成巨大挑战。

一致性哈希正是为了解决这一问题而提出的。它可以在节点数量发生变化时,将数据迁移量控制在最小范围,提升系统的容错性和扩展性

特性 哈希算法 一致性哈希算法
主要用途 数据校验、密码加密、基本数据分区 动态节点数据分配、缓存集群、分布式存储
冲突处理 通过哈希函数避免或使用链表、开放地址法等方式 节点增减时,仅影响邻近数据,减少冲突
节点扩展性 节点增减影响大,需重新分配大部分数据 节点增减影响小,仅重新分配一小部分数据
平衡数据分布 依赖哈希函数本身 借助虚拟节点技术,确保数据在环上均匀分布
应用场景 数据完整性、加密、简单分布 分布式系统中的数据分布、缓存、负载均衡


2、一致性哈希的基本原理

一致性哈希算法(Consistent Hashing)是一种特殊的哈希算法,最早应用于分布式系统的数据分区。其核心思想是将数据和存储节点都映射到同一个哈希环(通常是一个虚拟的“0到2^32”整数环),并按顺时针方向为每个数据选择一个最近的节点来存储。

主要特点:

  • 平滑性:节点的增减对整体数据分布影响较小。例如,新增节点时仅影响原环上一部分数据,而不需要重新分配所有数据。
  • 均匀性:数据在环上尽量均匀分布,避免节点负载不均。
  • 负载平衡:通过虚拟节点技术,每个实际节点分配多个虚拟节点,可以更均匀地分散数据,防止数据倾斜。
  • 适用性:一致性哈希主要用于动态扩展场景,如缓存集群、分布式存储

一致性哈希的核心思想是将哈希空间组织成一个逻辑上的环,并将数据和节点映射到这个环上,采用顺时针分配策略,使数据能够高效地分布和存储。

具体来说,算法的基本步骤如下:

  1. 创建环形空间:首先将哈希空间映射为一个逻辑环。例如,对于32位的哈希空间,值域为0 - 2^32 - 1,所有节点和数据的哈希值分布在这个空间上。

  2. 映射节点位置:每个物理节点(如服务器)被哈希到环上的一个位置,称为“节点哈希位置”。

  3. 映射数据位置:同样,每个数据项(如缓存键)也会被哈希到环上的某个位置。该数据将存储到顺时针方向第一个遇到的节点上。

  4. 容错性与扩展性:当节点加入或退出时,只需调整与该节点相关的少部分数据,其他数据的位置保持不变,确保系统的平稳扩展。


典型应用:

  • 分布式缓存系统(如 Memcached、Redis Cluster),在服务器增减时能最小化影响。
  • 分布式数据库(如 Cassandra、DynamoDB)在数据分片与分区上广泛使用。
  • 分布式文件系统(如 Amazon S3、HDFS)

哈希算法:适合一般数据校验和静态数据分布场景。

一致性哈希算法:适合动态、扩展性要求高的分布式系统,尤其在节点频繁增减的场景中能有效平衡数据分布和负载


3、一致性哈希的优势和挑战

一致性哈希因其独特的特性,在分布式系统中有着显著的优势:

  • 高扩展性:可以轻松地增加或减少节点,不会影响系统的整体运行。
  • 负载均衡:通过引入虚拟节点,进一步均衡数据在节点之间的分布。
  • 容错性:避免了传统哈希算法在节点变动时带来的大规模数据迁移。

尽管如此,一致性哈希也有一些缺点。例如,直接使用节点哈希位置分配数据会导致数据分布不均匀,这就需要引入虚拟节点来平衡数据


4、虚拟节点的引入

在实际应用中,由于节点的哈希位置可能会导致数据分布不均匀,一致性哈希通常会引入“虚拟节点”的概念来解决该问题。虚拟节点的作用是通过将每个物理节点映射到多个位置,从而更均匀地分布数据,避免出现数据倾斜现象。

  • 如何添加虚拟节点:每个物理节点会被映射成多个“虚拟节点”并散布在哈希环上。
  • 数据分布方式:当数据项哈希到环上某个位置后,会顺时针寻找最近的虚拟节点,由此分配数据。

5、 Java代码实现

package com.artisan.stream;

/**
 * @author 小工匠
 * @version 1.0
 * @date 2024/11/21 23:16
 * @mark: show me the code , change the world
 */
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;

public class ConsistentHashing {

    // 节点类
    private static class Node {
        private final String identifier;

        Node(String identifier) {
            this.identifier = identifier;
        }

        public String getIdentifier() {
            return identifier;
        }

        @Override
        public String toString() {
            return identifier;
        }
    }

    // 一致性哈希类
    public static class ConsistentHash {
        private final TreeMap<Integer, Node> ring = new TreeMap<>();
        private final int virtualNodeCount;

        public ConsistentHash(int virtualNodeCount) {
            this.virtualNodeCount = virtualNodeCount;
        }

        // 哈希计算
        /**
         * 使用MD5算法生成字符串的哈希值
         * 此方法用于将给定的字符串键转换为一个整数哈希值,以便在一致性哈希算法中使用
         * 选择MD5算法是因为它提供了良好的哈希值分布,有助于在一致性哈希环上均匀分布键
         *
         * @param key 要生成哈希值的字符串键
         * @return 返回使用MD5算法计算得到的整数哈希值
         * @throws RuntimeException 如果Java环境中没有找到MD5算法,抛出运行时异常
         */
        private int hash(String key) {
            try {
                // 创建MD5消息摘要实例
                MessageDigest md5 = MessageDigest.getInstance("MD5");
                // 使用UTF-8编码将键转换为字节数组,并计算MD5哈希值
                byte[] digest = md5.digest(key.getBytes(StandardCharsets.UTF_8));
                // 从MD5哈希值的前4个字节构建一个整数作为最终的哈希值
                // 这里使用位操作符将字节转换为整数,并确保结果为正数
                return ((digest[0] & 0xff) << 24) | ((digest[1] & 0xff) << 16)
                        | ((digest[2] & 0xff) << 8) | (digest[3] & 0xff);
            } catch (NoSuchAlgorithmException e) {
                // 如果环境中没有MD5算法,抛出运行时异常
                throw new RuntimeException("MD5 algorithm not found", e);
            }
        }

        // 添加节点,包含虚拟节点
        /**
         * 在哈希环中添加节点
         * 通过创建多个虚拟节点来实现,以提高哈希环的均衡性
         *
         * @param node 要添加到哈希环中的节点
         */
        public void addNode(Node node) {
            // 遍历虚拟节点的数目,为每个虚拟节点计算哈希值并添加到环中
            for (int i = 0; i < virtualNodeCount; i++) {
                // 计算节点标识符和虚拟节点序号的组合哈希值
                int hash = hash(node.getIdentifier() + "#" + i);
                // 将计算出的哈希值和节点的映射关系添加到哈希环中
                ring.put(hash, node);
                // 打印添加虚拟节点的信息,包括节点、虚拟节点序号和对应的哈希值
                System.out.println("添加虚拟节点 [" + node + "#" + i + "] 到哈希环位置:" + hash);
            }
        }

        // 移除节点及其虚拟节点
        /**
         * 从哈希环中移除指定节点及其所有虚拟节点
         *
         * @param node 要移除的物理节点
         */
        public void removeNode(Node node) {
            // 遍历所有虚拟节点
            for (int i = 0; i < virtualNodeCount; i++) {
                // 计算虚拟节点的哈希值
                int hash = hash(node.getIdentifier() + "#" + i);
                // 从哈希环中移除虚拟节点
                ring.remove(hash);
                // 打印移除虚拟节点的日志信息
                System.out.println("从哈希环移除虚拟节点 [" + node + "#" + i + "] 位置:" + hash);
            }
        }

        // 获取数据存储节点
        /**
         * 根据给定的键获取对应的节点
         * 该方法首先对键进行哈希处理,然后在哈希环中找到第一个大于或等于哈希值的节点
         * 如果没有找到这样的节点,则返回哈希环中的第一个节点
         * 这个过程利用了一致性哈希算法来选择节点,以达到在分布式系统中平衡负载的目的
         *
         * @param key 用于获取节点的键
         * @return 返回找到的节点
         */
        public Node getNode(String key) {
            // 对键进行哈希处理,以确定其在哈希环中的位置
            int hash = hash(key);

            // 在哈希环中找到第一个大于或等于当前哈希值的节点
            Map.Entry<Integer, Node> entry = ring.ceilingEntry(hash);

            // 如果没有找到这样的节点,则返回哈希环中的第一个节点
            if (entry == null) {
                entry = ring.firstEntry();
            }

            // 返回找到的节点
            return entry.getValue();
        }
    }

    // 示例测试
    public static void main(String[] args) {
        ConsistentHash consistentHash = new ConsistentHash(3);

        // 添加节点
        Node nodeA = new Node("Node-A");
        Node nodeB = new Node("Node-B");
        Node nodeC = new Node("Node-C");

        consistentHash.addNode(nodeA);
        consistentHash.addNode(nodeB);
        consistentHash.addNode(nodeC);

        // 查找存储节点
        String key = "my-key";
        System.out.println("键 " + key + " 被分配到节点:" + consistentHash.getNode(key));

        // 移除节点并查看影响
        consistentHash.removeNode(nodeB);
        System.out.println("键 " + key + " 重新分配到节点:" + consistentHash.getNode(key));
    }
}


ConsistentHashing 类实现了一致性哈希算法,用于在分布式系统中平衡负载。该类包含两个内部类:NodeConsistentHashNode 类表示一个节点,ConsistentHash 类实现了具体的一致性哈希逻辑。


内部类:Node

private static class Node {
     .....
}
  • 字段
    • identifier:节点的唯一标识符。
  • 构造方法
    • Node(String identifier):初始化节点的标识符。
  • 方法
    • getIdentifier():返回节点的标识符。
    • toString():返回节点的标识符,用于打印节点信息。

内部类:ConsistentHash

public static class ConsistentHash {
    private final TreeMap<Integer, Node> ring = new TreeMap<>();
    private final int virtualNodeCount;

    public ConsistentHash(int virtualNodeCount) {
        this.virtualNodeCount = virtualNodeCount;
    }
  • 字段
    • ring:一个 TreeMap,用于存储哈希值和节点的映射关系,实现哈希环。
    • virtualNodeCount:每个物理节点的虚拟节点数量,用于提高哈希环的均衡性。
  • 构造方法
    • ConsistentHash(int virtualNodeCount):初始化一致性哈希对象,设置虚拟节点的数量。

方法:hash

private int hash(String key) {
    try {
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        byte[] digest = md5.digest(key.getBytes(StandardCharsets.UTF_8));
        return ((digest[0] & 0xff) << 24) | ((digest[1] & 0xff) << 16)
                | ((digest[2] & 0xff) << 8) | (digest[3] & 0xff);
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException("MD5 algorithm not found", e);
    }
}
  • 功能
    • 使用 MD5 算法生成字符串的哈希值。
  • 参数
    • key:要生成哈希值的字符串键。
  • 返回值
    • 返回使用 MD5 算法计算得到的整数哈希值。
  • 异常处理
    • 如果环境中没有 MD5 算法,抛出 RuntimeException

方法:addNode

public void addNode(Node node) {
    for (int i = 0; i < virtualNodeCount; i++) {
        int hash = hash(node.getIdentifier() + "#" + i);
        ring.put(hash, node);
        System.out.println("添加虚拟节点 [" + node + "#" + i + "] 到哈希环位置:" + hash);
    }
}
  • 功能
    • 在哈希环中添加节点,通过创建多个虚拟节点来提高哈希环的均衡性。
  • 参数
    • node:要添加到哈希环中的节点。

方法:removeNode

public void removeNode(Node node) {
    for (int i = 0; i < virtualNodeCount; i++) {
        int hash = hash(node.getIdentifier() + "#" + i);
        ring.remove(hash);
        System.out.println("从哈希环移除虚拟节点 [" + node + "#" + i + "] 位置:" + hash);
    }
}
  • 功能
    • 从哈希环中移除指定节点及其所有虚拟节点。
  • 参数
    • node:要移除的物理节点。

方法:getNode

public Node getNode(String key) {
    int hash = hash(key);
    Map.Entry<Integer, Node> entry = ring.ceilingEntry(hash);
    if (entry == null) {
        entry = ring.firstEntry();
    }
    return entry.getValue();
}
  • 功能
    • 根据给定的键获取对应的节点。
    • 该方法首先对键进行哈希处理,然后在哈希环中找到第一个大于或等于哈希值的节点。如果没有找到这样的节点,则返回哈希环中的第一个节点。
  • 参数
    • key:用于获取节点的键。
  • 返回值
    • 返回找到的节点。

主方法:main

public static void main(String[] args) {
    ConsistentHash consistentHash = new ConsistentHash(3);

    Node nodeA = new Node("Node-A");
    Node nodeB = new Node("Node-B");
    Node nodeC = new Node("Node-C");

    consistentHash.addNode(nodeA);
    consistentHash.addNode(nodeB);
    consistentHash.addNode(nodeC);

    String key = "my-key";
    System.out.println("键 " + key + " 被分配到节点:" + consistentHash.getNode(key));

    consistentHash.removeNode(nodeB);
    System.out.println("键 " + key + " 重新分配到节点:" + consistentHash.getNode(key));
}
  • 功能
    • 创建一个 ConsistentHash 对象,并添加几个节点。
    • 测试键的分配情况,并查看移除节点后的影响。

小结

ConsistentHashing 类通过一致性哈希算法实现了在分布式系统中平衡负载的功能。主要步骤包括:

  1. 定义节点类 Node
  2. 实现一致性哈希类 ConsistentHash,包含哈希计算、添加节点、移除节点和获取节点的方法。

每日一博 - 一致性哈希:分布式系统的数据分配利器_第2张图片

你可能感兴趣的:(【每日一博】,哈希算法,一致性哈希)