深入理解分布式系统的核心原理与实现
一致性哈希是一种特殊的哈希技术,用于在分布式系统中, 将数据动态地分配到多个节点上,同时最大限度地减少节点增减时数据的迁移量。
假设我们有 N 台服务器,使用普通哈希函数:
// 传统方法:对服务器数量取模 function getServer(key, N) { return hash(key) % N; }
问题:当服务器数量 N 变化时(扩容/缩容),几乎所有数据的映射都会改变!
例如:N=10 → N=11,只有 1/10 的数据能命中正确位置,其余 9/10 都需要重新分配!
这会导致缓存雪崩、数据库压力骤增等严重问题。
一致性哈希通过环形哈希空间的设计,将节点和数据都映射到同一个哈希环上:
点击按钮观察节点增减时,数据的重新分布情况
基本实现中,一个物理节点只对应环上一个点,会导致:
解决方案:虚拟节点
每个物理节点对应多个虚拟节点(通常100-200个),分散在环上:
// 虚拟节点实现示例 class ConsistentHash { constructor(virtualNodesPerNode = 150) { this.ring = new SortedMap(); this.virtualNodes = virtualNodesPerNode; } addNode(nodeId) { for (let i = 0; i < this.virtualNodes; i++) { const vnodeKey = `${nodeId}#VN${i}`; const hash = this.hash(vnodeKey); this.ring.set(hash, nodeId); } } }
Redis Cluster 是 Redis 官方提供的分布式解决方案, 实现了数据在多个 Redis 节点之间的自动分片、高可用和故障转移。
Redis Cluster 使用哈希槽而非一致性哈希:
// Redis Cluster 使用 16384 个哈希槽 // 计算每个 key 属于哪个槽 function CRC16(key) { // CRC16算法 } function getSlot(key) { return CRC16(key) % 16384; } // 示例: getSlot("user:1001") // → 返回 0~16383 之间的槽号 getSlot("order:2023") // → 返回另一个槽号
假设有3个主节点:
扩容时:从现有节点"偷"一些槽给新节点(槽迁移)。
缩容时:将该节点的槽迁移给其他节点,然后下线。
| 对比维度 | 一致性哈希 | Redis Cluster 哈希槽 |
|---|---|---|
| 设计思想 | 环形空间,顺时针查找 | 固定16384个槽位,预分配 |
| 数据迁移量 | 理论上 K/N(K=数据量,N=节点数) | 只有被迁移槽的数据 |
| 负载均衡 | 需要虚拟节点来保证 | 槽可以精细控制分配 |
| 实现复杂度 | 中等(需要维护有序结构) | 较低(槽分配是离散的) |
| 适用场景 | Memcached、分布式缓存 | Redis官方集群方案 |
| 节点通信 | 取决于具体实现 | Gossip协议 |
Redis作者给出的理由:
/** * 一致性哈希实现(含虚拟节点) */ class ConsistentHash { constructor(virtualNodes = 150) { this.nodes = new Map(); // 物理节点 → 虚拟节点集合 this.ring = new SortedMap(); // 哈希环:hash → nodeId this.vnodeCount = virtualNodes; } // MurmurHash / MD5 / CRC32 等均可 hash(key) { let hash = 0; for (let i = 0; i < key.length; i++) { const ch = key.charCodeAt(i); hash = ((hash << 5) - hash) + ch; hash = hash & hash; } return hash & 0xFFFFFFFF; // 无符号32位 } // 添加物理节点 addNode(nodeId) { const vnodes = []; for (let i = 0; i < this.vnodeCount; i++) { const vkey = `${nodeId}##VN${i}`; const hash = this.hash(vkey); this.ring.set(hash, nodeId); vnodes.push(hash); } this.nodes.set(nodeId, vnodes); } // 删除物理节点 removeNode(nodeId) { const vnodes = this.nodes.get(nodeId); if (!vnodes) return; vnodes.forEach(hash => this.ring.delete(hash)); this.nodes.delete(nodeId); } // 查找key对应的节点 getNode(key) { if (this.ring.size === 0) return null; const hash = this.hash(key); // 找到第一个 hash >= keyHash 的节点(顺时针) const keys = [...this.ring.keys()].sort((a,b) => a-b); let target = keys.find(k => k >= hash); if (!target) target = keys[0]; // 绕回环开头 return this.ring.get(target); } }
# redis-cluster.conf - 每个节点都需要此配置 # 启用集群模式 cluster-enabled yes # 集群配置文件(自动生成) cluster-config-file nodes.conf # 节点超时时间(毫秒) cluster-node-timeout 15000 # 是否将所有slot都分配给本节点(默认no) cluster-require-full-coverage no # 启动6个节点(3主3从) # 端口:7000, 7001, 7002, 7003, 7004, 7005 # 使用 redis-cli 创建集群 redis-cli --cluster create \ 127.0.0.1:7000 \ 127.0.0.1:7001 \ 127.0.0.1:7002 \ 127.0.0.1:7003 \ 127.0.0.1:7004 \ 127.0.0.1:7005 \ --cluster-replicas 1 # 查看集群状态 redis-cli -p 7000 cluster nodes redis-cli -p 7000 cluster slots
# 将槽 0~1000 从节点A迁移到节点B # 1. 在目标节点B上标记准备导入 redis-cli -p 7001 CLUSTER SETSLOT 5461 IMPORTING src-node-id # 2. 在源节点A上标记准备导出 redis-cli -p 7000 CLUSTER SETSLOT 5461 MIGRATING dst-node-id # 3. 逐个迁移key(或使用 redis-cli --cluster reshard) redis-cli --cluster reshard 127.0.0.1:7000 \ --cluster-from src-node-id \ --cluster-to dst-node-id \ --cluster-slots 1000
/** * Redis Cluster 客户端路由示例(Java + Jedis) */ public class RedisClusterClient { // 计算key属于哪个槽 public static int getSlot(String key) { // 处理 hash tag:{user:1001}.name → 只对 {user:1001} 部分hash int start = key.indexOf('{'); int end = key.indexOf('}'); if (start != -1 && end != -1 && start < end) { key = key.substring(start + 1, end); } return CRC16(key) % 16384; } // 客户端缓存槽映射表 private Map<Integer, HostAndPort> slotMap; // 发送命令:先本地计算槽,再路由到正确节点 public sendCommand(String key, Command cmd) { int slot = getSlot(key); HostAndPort node = slotMap.get(slot); try { return sendToNode(node, cmd); } catch (MovedException e) { // 槽已迁移:更新本地映射,重试 this.slotMap = fetchSlotMap(e.getNewNode()); return sendCommand(key, cmd); // 重试 } } }
解决分布式缓存中节点动态变化导致大量数据迁移的问题。通过哈希环+虚拟节点,实现节点变化时只影响 1/N 的数据。广泛应用于 Memcached、Couchbase 等系统。
Redis官方分布式方案,使用16384个哈希槽实现数据分片。支持自动故障转移、在线扩缩容。通过Gossip协议进行节点通信,主从复制保证高可用。
一致性哈希是一种通用的分布式哈希算法思想;Redis Cluster 是一种具体的分布式系统实现,它使用了类似但不同的哈希槽方案。理解一致性哈希有助于理解Redis Cluster的设计哲学。
📌 使用Redis → 直接用Redis Cluster(官方方案,成熟稳定)
📌 自研分布式缓存 → 参考一致性哈希设计
📌 需要多Key事务 → 考虑Hash Tag或Proxy型方案(如Codis)