14.2 一致性Hash算法

面试重要程度:⭐⭐⭐⭐⭐

常见提问方式: "设计分布式缓存" "解决数据倾斜问题" "一致性Hash的优缺点"

预计阅读时间:35分钟

🎯 一致性Hash基本原理

传统Hash的问题

在分布式系统中,传统的Hash算法存在以下问题:

/**
 * 传统Hash分片的问题演示
 */
public class TraditionalHashProblems {
    
    /**
     * 传统Hash分片
     */
    public static int traditionalHash(String key, int serverCount) {
        return Math.abs(key.hashCode()) % serverCount;
    }
    
    /**
     * 演示节点变化对数据分布的影响
     */
    public static void demonstrateProblem() {
        String[] keys = {"user1", "user2", "user3", "user4", "user5"};
        
        System.out.println("=== 3台服务器时的分布 ===");
        for (String key : keys) {
            int server = traditionalHash(key, 3);
            System.out.println(key + " -> Server " + server);
        }
        
        System.out.println("\n=== 增加1台服务器后的分布 ===");
        for (String key : keys) {
            int server = traditionalHash(key, 4);
            System.out.println(key + " -> Server " + server);
        }
        
        // 结果:大部分数据需要重新分布
    }
}

问题分析:

  • 当服务器数量变化时,几乎所有数据都需要重新分布
  • 造成大量的数据迁移和缓存失效
  • 系统扩容或缩容成本极高

一致性Hash解决方案

一致性Hash通过构建Hash环来解决数据重分布问题。

🔄 一致性Hash实现

基础实现

/**
 * 一致性Hash算法实现
 */
public class ConsistentHash {
    
    /**
     * 虚拟节点数量
     */
    private static final int VIRTUAL_NODES = 150;
    
    /**
     * Hash环,使用TreeMap保证有序
     */
    private final TreeMap<Long, String> hashRing = new TreeMap<>();
    
    /**
     * 物理节点集合
     */
    private final Set<String> physicalNodes = new HashSet<>();
    
    /**
     * 添加节点
     */
    public void addNode(String node) {
        physicalNodes.add(node);
        
        // 为每个物理节点创建虚拟节点
        for (int i = 0; i < VIRTUAL_NODES; i++) {
            String virtualNode = node + "#" + i;
            long hash = hash(virtualNode);
            hashRing.put(hash, node);
        }
        
        System.out.println("Added node: " + node + " with " + VIRTUAL_NODES + " virtual nodes");
    }
    
    /**
     * 移除节点
     */
    public void removeNode(String node) {
        physicalNodes.remove(node);
        
        // 移除所有虚拟节点
        for (int i = 0; i < VIRTUAL_NODES; i++) {
            String virtualNode = node + "#" + i;
            long hash = hash(virtualNode);
            hashRing.remove(hash);
        }
        
        System.out.println("Removed node: " + node);
    }
    
    /**
     * 获取数据应该存储的节点
     */
    public String getNode(String key) {
        if (hashRing.isEmpty()) {
            return null;
        }
        
        long hash = hash(key);
        
        // 查找第一个大于等于该hash值的节点
        Map.Entry<Long, String> entry = hashRing.ceilingEntry(hash);
        
        // 如果没找到,说明应该存储在第一个节点(环形结构)
        if (entry == null) {
            entry = hashRing.firstEntry();
        }
        
        return entry.getValue();
    }
    
    /**
     * Hash函数(使用FNV1a算法)
     */
    private long hash(String key) {
        final long FNV_64_INIT = 0xcbf29ce484222325L;
        final long FNV_64_PRIME = 0x100000001b3L;
        
        long hash = FNV_64_INIT;
        for (byte b : key.getBytes()) {
            hash ^= b;
            hash *= FNV_64_PRIME;
        }
        
        return hash;
    }
    
    /**
     * 获取所有物理节点
     */
    public Set<String> getPhysicalNodes() {
        return new HashSet<>(physicalNodes);
    }
    
    /**
     * 获取Hash环状态
     */
    public void printRingStatus() {
        System.out.println("=== Hash Ring Status ===");
        System.out.println("Physical nodes: " + physicalNodes.size());
        System.out.println("Virtual nodes: " + hashRing.size());
        
        // 统计每个物理节点的虚拟节点分布
        Map<String, Integer> distribution = new HashMap<>();
        for (String node : hashRing.values()) {
            distribution.put(node, distribution.getOrDefault(node, 0) + 1);
        }
        
        System.out.println("Virtual nodes distribution:");
        for (Map.Entry<String, Integer> entry : distribution.entrySet()) {
            System.out.println("  " + entry.getKey() + ": " + entry.getValue());
        }
    }
}

高级实现(支持权重)

/**
 * 支持权重的一致性Hash实现
 */
public class WeightedConsistentHash {
    
    private final TreeMap<Long, String> hashRing = new TreeMap<>();
    private final Map<String, Integer> nodeWeights = new HashMap<>();
    private final Set<String> physicalNodes = new HashSet<>();
    
    /**
     * 添加带权重的节点
     */
    public void addNode(String node, int weight) {
        physicalNodes.add(node);
        nodeWeights.put(node, weight);
        
        // 根据权重计算虚拟节点数量
        int virtualNodes = weight * 40; // 基础虚拟节点数 * 权重
        
        for (int i = 0; i < virtualNodes; i++) {
            String virtualNode = node + "#" + i;
            long hash = hash(virtualNode);
            hashRing.put(hash, node);
        }
        
        System.out.println("Added weighted node: " + node + 
                         " (weight=" + weight + ", virtual_nodes=" + virtualNodes + ")");
    }
    
    /**
     * 移除节点
     */
    public void removeNode(String node) {
        Integer weight = nodeWeights.remove(node);
        if (weight == null) {
            return;
        }
        
        physicalNodes.remove(node);
        
        // 移除所有虚拟节点
        int virtualNodes = weight * 40;
        for (int i = 0; i < virtualNodes; i++) {
            String virtualNode = node + "#" + i;
            long hash = hash(virtualNode);
            hashRing.remove(hash);
        }
        
        System.out.println("Removed weighted node: " + node);
    }
    
    /**
     * 获取节点
     */
    public String getNode(String key) {
        if (hashRing.isEmpty()) {
            return null;
        }
        
        long hash = hash(key);
        Map.Entry<Long, String> entry = hashRing.ceilingEntry(hash);
        
        if (entry == null) {
            entry = hashRing.firstEntry();
        }
        
        return entry.getValue();
    }
    
    /**
     * 分析数据分布
     */
    public void analyzeDistribution(String[] testKeys) {
        Map<String, Integer> distribution = new HashMap<>();
        
        for (String key : testKeys) {
            String node = getNode(key);
            distribution.put(node, distribution.getOrDefault(node, 0) + 1);
        }
        
        System.out.println("=== Data Distribution Analysis ===");
        System.out.println("Total keys: " + testKeys.length);
        
        for (Map.Entry<String, Integer> entry : distribution.entrySet()) {
            String node = entry.getKey();
            int count = entry.getValue();
            double percentage = (count * 100.0) / testKeys.length;
            int weight = nodeWeights.getOrDefault(node, 1);
            
            System.out.printf("Node %s (weight=%d): %d keys (%.2f%%)\n", 
                            node, weight, count, percentage);
        }
    }
    
    private long hash(String key) {
        final long FNV_64_INIT = 0xcbf29ce484222325L;
        final long FNV_64_PRIME = 0x100000001b3L;
        
        long hash = FNV_64_INIT;
        for (byte b : key.getBytes()) {
            hash ^= b;
            hash *= FNV_64_PRIME;
        }
        
        return hash;
    }
}

🔧 实际应用场景

分布式缓存实现

/**
 * 基于一致性Hash的分布式缓存
 */
public class DistributedCache {
    
    private final ConsistentHash consistentHash;
    private final Map<String, CacheNode> cacheNodes;
    
    public DistributedCache() {
        this.consistentHash = new ConsistentHash();
        this.cacheNodes = new HashMap<>();
    }
    
    /**
     * 缓存节点
     */
    static class CacheNode {
        private final String nodeId;
        private final Map<String, Object> cache;
        
        public CacheNode(String nodeId) {
            this.nodeId = nodeId;
            this.cache = new ConcurrentHashMap<>();
        }
        
        public void put(String key, Object value) {
            cache.put(key, value);
            System.out.println("Node " + nodeId + " stored: " + key);
        }
        
        public Object get(String key) {
            Object value = cache.get(key);
            System.out.println("Node " + nodeId + " retrieved: " + key + 
                             " -> " + (value != null ? "HIT" : "MISS"));
            return value;
        }
        
        public void remove(String key) {
            cache.remove(key);
            System.out.println("Node " + nodeId + " removed: " + key);
        }
     

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

Java面试圣经 文章被收录于专栏

Java面试圣经,带你练透java圣经

全部评论
忍耐王
点赞 回复 分享
发布于 09-06 08:18 浙江

相关推荐

评论
1
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务