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圣经