🔴 Redis集群 & 一致性哈希

深入理解分布式系统的核心原理与实现

🔗一、一致性哈希(Consistent Hashing)是什么?

一致性哈希是一种特殊的哈希技术,用于在分布式系统中, 将数据动态地分配到多个节点上,同时最大限度地减少节点增减时数据的迁移量。

❌ 传统哈希的问题

假设我们有 N 台服务器,使用普通哈希函数:

// 传统方法:对服务器数量取模
function getServer(key, N) {
    return hash(key) % N;
}

问题:当服务器数量 N 变化时(扩容/缩容),几乎所有数据的映射都会改变!

例如:N=10 → N=11,只有 1/10 的数据能命中正确位置,其余 9/10 都需要重新分配!

这会导致缓存雪崩、数据库压力骤增等严重问题。

✅ 一致性哈希的解决方案

一致性哈希通过环形哈希空间的设计,将节点和数据都映射到同一个哈希环上:

  • 🎯 哈希环:将哈希值空间组织成一个虚拟的环(通常 0 ~ 2^32-1)
  • 🖥️ 节点映射:将每个服务器节点通过哈希函数映射到环上
  • 📦 数据映射:将数据key也映射到环上,然后顺时针找到第一个节点
  • 扩容时:只影响新节点与上一个节点之间的数据(约 1/N)
  • 缩容时:只影响被删除节点上的数据(约 1/N)

🎨 一致性哈希环可视化

点击按钮观察节点增减时,数据的重新分布情况

💡 虚拟节点(Virtual Nodes)

基本实现中,一个物理节点只对应环上一个点,会导致:

解决方案:虚拟节点

每个物理节点对应多个虚拟节点(通常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集群(Redis Cluster)是什么?

Redis Cluster 是 Redis 官方提供的分布式解决方案, 实现了数据在多个 Redis 节点之间的自动分片高可用故障转移

🎯 核心目标

  • 数据分片(Sharding)
  • 高可用性(HA)
  • 水平扩展(Scale Out)
  • 故障自动转移

🏗️ 架构特点

  • 去中心化架构
  • 16384个哈希槽
  • 主从复制模型
  • Gossip协议通信

⚡ 关键能力

  • 自动故障检测
  • Slave自动提升
  • 在线扩容/缩容
  • 多Key操作限制

🔴 Redis Cluster 架构可视化

🎰 哈希槽(Hash Slot)机制

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") // → 返回另一个槽号

📊 16384个槽的分布

假设有3个主节点:

  • Master-A 负责槽 [0 ~ 5460] (约5461个槽)
  • Master-B 负责槽 [5461 ~ 10922] (约5462个槽)
  • Master-C 负责槽 [10923 ~ 16383] (约5461个槽)

扩容时:从现有节点"偷"一些槽给新节点(槽迁移)。

缩容时:将该节点的槽迁移给其他节点,然后下线。

⚖️三、一致性哈希 vs Redis Cluster 哈希槽

对比维度 一致性哈希 Redis Cluster 哈希槽
设计思想 环形空间,顺时针查找 固定16384个槽位,预分配
数据迁移量 理论上 K/N(K=数据量,N=节点数) 只有被迁移槽的数据
负载均衡 需要虚拟节点来保证 槽可以精细控制分配
实现复杂度 中等(需要维护有序结构) 较低(槽分配是离散的)
适用场景 Memcached、分布式缓存 Redis官方集群方案
节点通信 取决于具体实现 Gossip协议

🤔 为什么Redis不用一致性哈希?

Redis作者给出的理由:

  1. 固定的槽数量(16384):迁移时只需搬运槽,不需要重新计算所有key的哈希值
  2. 更容易理解和调试:槽的分配是显式的,人可以直观地看到哪些槽在哪些节点上
  3. 更好的可控性:可以手动调整槽的分布,优化负载
  4. 简化客户端实现:客户端只需要知道槽映射,不需要实现复杂的环查找

⚙️四、实现原理与代码示例

🔗 一致性哈希 - JavaScript 完整实现

/**
 * 一致性哈希实现(含虚拟节点)
 */
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 配置与启动

# 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); // 重试
        }
    }
}

⚠️ Redis Cluster 使用限制

  • 多Key操作:所有key必须在同一个槽中(使用Hash Tag强制绑定)
  • 事务:MULTI/EXEC 中的key必须在同一节点
  • 管道:批量命令的key必须路由到同一节点
  • 数据库:只能使用 db0(SELECT命令不可用)

📝五、总结

一致性哈希

解决分布式缓存中节点动态变化导致大量数据迁移的问题。通过哈希环+虚拟节点,实现节点变化时只影响 1/N 的数据。广泛应用于 Memcached、Couchbase 等系统。

Redis Cluster

Redis官方分布式方案,使用16384个哈希槽实现数据分片。支持自动故障转移、在线扩缩容。通过Gossip协议进行节点通信,主从复制保证高可用。

两者关系

一致性哈希是一种通用的分布式哈希算法思想;Redis Cluster 是一种具体的分布式系统实现,它使用了类似但不同的哈希槽方案。理解一致性哈希有助于理解Redis Cluster的设计哲学。

选型建议

📌 使用Redis → 直接用Redis Cluster(官方方案,成熟稳定)

📌 自研分布式缓存 → 参考一致性哈希设计

📌 需要多Key事务 → 考虑Hash Tag或Proxy型方案(如Codis)

🎓 进一步学习资源

  • 📖 Redis官方文档:Cluster Spec
  • 📖 一致性哈希论文:MIT CSAIL 1997
  • 📖 《Designing Data-Intensive Applications》第5章
  • 📖 Redis设计与实现(黄健宏)- 集群章节