分布式存储 - 那些关于分布式缓存的一二事儿

文章目录

  • 概述
  • 缓存分片算法
    • 1. Hash算法
    • 2. 一致性Hash算法
    • 3. 应用场景
  • Redis集群方案
    • 1. Redis 集群方案原理
    • 2. Redis 集群方案的优势
    • 3. Java 代码示例:Redis 集群数据定位
  • Redis 集群中的节点通信机制:Gossip 协议
    • Redis 集群的节点通信:Gossip 协议
    • Redis 集群的节点通信流程
    • Redis 集群中的消息类型
    • Gossip 协议的消息结构
    • Redis 集群的高可用性与容错性
    • 小结
  • 请求分布式缓存的路由
    • 1. 槽(Slot)信息的存储与解析
    • 2. `clusterState` 结构与节点的槽映射
    • 3. Redis 客户端的数据路由过程
    • 4. 数据迁移期间的 ASK 重定向机制
    • Redis 集群路由机制的优势
  • 缓存节点的扩展和收缩
    • 1. 缓存节点的扩展
      • 1.1 新节点加入集群
      • 1.2 槽分配和数据迁移
    • 2. 缓存节点的收缩
      • 2.1 槽数据迁移
      • 2.2 节点下线通知
  • 缓存故障的发现和恢复
    • 故障检测机制:主观下线与客观下线
      • 1. 主观下线
      • 2. 客观下线
    • 故障恢复:主节点选举与自动故障转移
      • 1. 资格检查
      • 2. 触发选举
      • 3. 发起选举请求
      • 4. 投票选举
      • 5. 广播新主节点信息
    • 小结

概述

分布式缓存系统是为了解决高并发场景中数据访问的速度瓶颈,将频繁访问的数据存储在内存中,以便迅速响应应用程序的查询请求。

分布式存储 - 那些关于分布式缓存的一二事儿_第1张图片

  1. 缓存类型:分布式缓存分为进程内缓存和进程外缓存。

    • 进程内缓存:在应用程序的JVM内,缓存大小受限于单机的内存,不适合超大规模数据的存储。
    • 进程外缓存:独立于应用程序的JVM,以单独服务的形式存在,可在多台缓存服务器上进行水平扩展,适合分布式应用场景。
  2. 分布式缓存的特点

    • 独立性:分布式缓存系统作为一个独立服务,不依赖于应用实例,与应用相分离。
    • 共享性:可以被多个应用访问,实现缓存数据的共享。
  3. 缓存节点和缓存代理

    • 缓存节点:分布式缓存将数据分片并分布到多个缓存节点上,每个节点负责存储特定的数据片。为了容纳更多的数据或分担负载,可以水平扩展缓存节点。
    • 缓存代理:用于管理请求的路由,使其指向相应的缓存节点。代理如 twemproxyRedis Cluster 等负责缓存分片、节点路由以及负载均衡。
  4. 主从节点结构:为提高缓存的可用性和容错性,每个主缓存节点会有一个或多个从节点:

    • 主从同步:数据写入主节点时,会同步到从节点。
    • 故障切换:如果主节点故障,代理可将请求切换到从节点,保证缓存服务的持续可用性。
  5. 数据过期和快照

    • 缓存过期:为避免数据冗余和内存占用,每个缓存条目通常设置过期时间。
    • 数据持久化:缓存系统会定期将数据快照保存至文件,便于缓存崩溃后的数据恢复。

通过这种分布式缓存架构,系统可以在高并发环境下保持低延迟、高性能的数据访问能力,同时具备扩展性和容灾能力。


缓存分片算法

1. Hash算法

  • 原理:根据数据的关键值(如数据ID)进行Hash计算,将结果对缓存节点的总数取模。
import java.util.HashMap;
import java.util.Map;

public class HashShardingCache {
    // 模拟缓存节点,key为节点ID,value为存储的数据
    private Map<Integer, Map<Integer, String>> cacheNodes;
    private int nodeCount;

    public HashShardingCache(int nodeCount) {
        this.nodeCount = nodeCount;
        this.cacheNodes = new HashMap<>();
        for (int i = 0; i < nodeCount; i++) {
            cacheNodes.put(i, new HashMap<>());
        }
    }

    // 添加数据到缓存,数据根据ID % nodeCount的结果选择节点
    public void put(int id, String value) {
        int nodeId = getNode(id);
        cacheNodes.get(nodeId).put(id, value);
        System.out.println("数据ID " + id + " 存储到缓存节点 " + nodeId);
    }

    // 从缓存获取数据
    public String get(int id) {
        int nodeId = getNode(id);
        return cacheNodes.get(nodeId).get(id);
    }

    // 计算数据应该放入的节点
    private int getNode(int id) {
        return id % nodeCount;
    }

    // 模拟新增节点导致的数据迁移
    public void addNode() {
        System.out.println("\n== 新增缓存节点 ==");
        nodeCount++;
        cacheNodes.put(nodeCount - 1, new HashMap<>());

        // 迁移数据:重新计算每条数据的节点位置
        Map<Integer, Map<Integer, String>> newCacheNodes = new HashMap<>();
        for (int i = 0; i < nodeCount; i++) {
            newCacheNodes.put(i, new HashMap<>());
        }

        for (Map<Integer, String> nodeData : cacheNodes.values()) {
            for (Map.Entry<Integer, String> entry : nodeData.entrySet()) {
                int newNodeId = getNode(entry.getKey());
                newCacheNodes.get(newNodeId).put(entry.getKey(), entry.getValue());
            }
        }

        cacheNodes = newCacheNodes;
    }

    // 打印当前缓存节点的数据分布
    public void printCacheNodes() {
        System.out.println("\n当前缓存节点数据分布:");
        for (Map.Entry<Integer, Map<Integer, String>> entry : cacheNodes.entrySet()) {
            System.out.println("节点 " + entry.getKey() + " 包含数据:" + entry.getValue().keySet());
        }
    }

    public static void main(String[] args) {
        HashShardingCache cache = new HashShardingCache(3);

        // 添加数据
        for (int i = 1; i <= 10; i++) {
            cache.put(i, "Value" + i);
        }

        cache.printCacheNodes();

        // 新增节点,观察数据迁移情况
        cache.addNode();
        cache.printCacheNodes();
    }
}

假设有10条数据(ID从1到10),并有3个缓存节点。通过对数据ID取模运算,分配数据到相应的节点上。例如,ID % 3运算后:
- 结果为0的数据分配给缓存节点0(如ID 3、6、9)。
- 结果为1的数据分配给缓存节点1(如ID 1、4、7、10)。
- 结果为2的数据分配给缓存节点2(如ID 2、5、8)。

  • 问题:当缓存节点数量发生变化(如新增节点)时,数据分布将发生大规模迁移(如从ID % 3ID % 4),导致大量缓存数据失效,增加数据库的访问压力。

2. 一致性Hash算法

  • 原理:将数据和缓存节点分别映射到一个Hash环上。通过将Hash值映射到固定的范围(如2^32)上,实现对缓存节点和数据的均匀分布。
  • 流程
    1. 将缓存节点的IP地址进行Hash计算,并将结果映射到环上。
    2. 将需要缓存的数据的ID同样进行Hash计算,得到数据在环中的位置。
    3. 按顺时针方向找到数据最近的缓存节点,将数据存入该节点。
开始
将缓存节点的IP地址进行Hash计算
将结果映射到环上
将需要缓存的数据的ID进行Hash计算
得到数据在环中的位置
按顺时针方向找到数据最近的缓存节点
将数据存入该节点
结束

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

  • 示例:如图所示,假设有3个节点(0、1、2)映射到Hash环中,并有4条数据(ID为1、2、3、4)。数据ID根据Hash值分布在环上:
    • 数据3存入节点0。
    • 数据1存入节点1。
    • 数据2、4存入节点2。
  • 优势:当新增或减少节点时,只需重新分配环上某些数据,避免大规模迁移问题。这种方式减少了数据失效率,确保系统的稳定性和高可用性。

3. 应用场景

一致性Hash算法适合于对稳定性要求较高、可能会频繁增加或减少节点的场景,例如电商秒杀系统等高并发应用中,减少缓存失效和数据库压力。

  • Hash算法:适合节点较固定的场景,简单但迁移开销大。
  • 一致性Hash算法:适合动态扩展的分布式场景,数据迁移量小,稳定性更好。 这种结构可以用于缓存系统,也可以适用于关系型数据库的分布式数据分片。

Redis集群方案

Redis 集群的分布式缓存方案通过将数据分片并分配到不同的缓存节点来实现扩展性和高可用性。Redis 集群主要基于槽(Slot)概念,将存储空间分为 16,384 个槽(即 ID 0 到 16,383),然后通过 Hash 算法将数据键映射到这些槽中。槽作为分片的最小单位,每个槽可以分配给不同的节点,从而实现数据的均匀分布和负载均衡。

1. Redis 集群方案原理

  1. 分片机制:Redis 集群利用 CRC16 校验算法将键值对映射到槽上。具体操作是将键值对的键进行 CRC16 校验,然后对 16,384 取模,以得到对应的槽号。这个槽号决定了该键值对的存储位置。

  2. 槽到节点的映射:Redis 集群将这 16,384 个槽分配给不同的节点。例如,在一个拥有 3 个节点的集群中,可以将槽按以下规则分配:

    • 节点 1:槽 0 ~ 5000
    • 节点 2:槽 5001 ~ 10,000
    • 节点 3:槽 10,001 ~ 16,383
      这样,集群中每个节点负责一部分槽,确保数据均匀分布在各个节点中。
  3. 数据定位:当 Redis 客户端请求一个键的值时,Redis 集群首先根据 CRC16(key) % 16384 计算出槽号,再根据槽到节点的映射找到对应的节点,然后访问该节点中的键值对并返回数据。例如,如果通过公式计算出槽号是 5002,则数据应该存放在节点 2,因为槽 5002 落在节点 2 负责的槽范围(5001 ~ 10,000)内。

  4. 重新分片:当节点增加或减少时,Redis 集群可以通过移动槽到不同节点来重新分配数据。这样可以动态调整每个节点的负载,减少单个节点的压力,提高集群的扩展性。

2. Redis 集群方案的优势

  • 高可用性:Redis 集群在每个节点上都可以有备份节点(Replica),当主节点发生故障时,备份节点可以接管其槽,确保集群的高可用性。
  • 扩展性:通过对槽的灵活分配,Redis 集群支持水平扩展,可以根据业务需求增加或减少节点,从而适应数据和访问量的增长。
  • 负载均衡:由于槽是分布在多个节点上,每个节点承担一定的负载,避免了集中在单节点上的性能瓶颈。
  • 容错性:在集群模式下,每个节点存储特定范围的槽数据,即使部分节点失效,集群仍然可以工作,只要大部分槽可用即可。

3. Java 代码示例:Redis 集群数据定位

假设我们有一个简化的集群,使用 TreeMap 来模拟槽到节点的映射。


import java.util.NavigableMap;
import java.util.TreeMap;

public class Demo {
    private final int SLOT_COUNT = 16384;
    private final TreeMap<Integer, String> slotToNodeMap = new TreeMap<>();

    // 初始化 Redis 集群,分配槽到不同节点
    public Demo () {
        // 初始化节点
        addNode("节点1", 0, 5000);
        addNode("节点2", 5001, 10000);
        addNode("节点3", 10001, SLOT_COUNT - 1);
    }

    // 添加节点并分配槽区间
    public void addNode(String node, int startSlot, int endSlot) {
        for (int slot = startSlot; slot <= endSlot; slot++) {
            slotToNodeMap.put(slot, node);
        }
    }

    // 根据键获取对应的 Redis 节点
    public String getNodeForKey(String key) {
        int slot = getSlot(key);
        System.out.println("键 " + key + " slot " + slot);
        // 获取最近的槽(TreeMap 提供的 ceilingEntry 方法)
        NavigableMap<Integer, String> tailMap = slotToNodeMap.tailMap(slot, true);
        int targetSlot = tailMap.isEmpty() ? slotToNodeMap.firstKey() : tailMap.firstKey();
        return slotToNodeMap.get(targetSlot);
    }

    // 根据 CRC16 校验值取模计算槽号
    private int getSlot(String key) {
        int crc16 = crc16(key);
        return crc16 % SLOT_COUNT;
    }

    // CRC16 校验计算(简化实现)
    private int crc16(String key) {
        int crc = 0xFFFF;
        for (char c : key.toCharArray()) {
            crc ^= c;
            for (int i = 0; i < 8; i++) {
                if ((crc & 1) != 0) {
                    crc = (crc >> 1) ^ 0xA001;
                } else {
                    crc >>= 1;
                }
            }
        }
        return crc & 0xFFFF;
    }

    public static void main(String[] args) {
        Demo cluster = new Demo();

        // 测试键到节点的分布
        String[] keys = {"小工匠", "KeyXX", "Redis", "Cluster"};
        for (String key : keys) {
            System.out.println("键 " + key + " 存储在 " + cluster.getNodeForKey(key));
        }
    }
}

分布式存储 - 那些关于分布式缓存的一二事儿_第2张图片

  • addNode 方法:用来将特定槽区间分配给指定节点。
  • getNodeForKey 方法:计算键的槽,并使用 TreeMap 查找最接近的槽映射的节点。
  • CRC16 校验:提供了一个简单的 CRC16 计算方法,实际应用中可以直接使用 Java 库或 Redis 官方提供的 CRC16 算法。

Redis 集群方案通过使用槽来管理和分片数据,有效解决了分布式缓存的一致性和负载均衡问题。每次访问键时,通过 CRC16 和槽号的映射表定位到具体的节点。这种方法不仅减少了数据迁移(因为槽是较小的迁移单元),而且当集群规模变化时也能高效地分配数据。


Redis 集群中的节点通信机制:Gossip 协议

在分布式缓存方案中,数据的拆分和分布式存储往往通过分片算法实现,比如 Redis 集群使用的虚拟槽算法,可以高效地将数据均匀分布到多个节点上。然而,这只是分布式缓存的一部分挑战。

在一个多节点的分布式缓存系统中,各节点之间的通信同样至关重要。它不仅关系到数据的同步,还直接影响到系统的高可用性和稳定性。

那么,Redis 集群中的各个缓存节点是如何进行通信的呢?

Redis 集群的节点通信:Gossip 协议

在 Redis 集群中,节点之间使用一种名为 Gossip 协议 的方式进行通信。Gossip(八卦)协议是一种轻量级的协议,广泛应用于分布式系统中,用于节点之间交换和共享信息。Redis 集群的各个节点通过 Gossip 协议,彼此分享状态数据,实时保持对整个集群的健康状况和拓扑结构的了解。

Redis 集群的节点通信流程

在 Redis 集群中,节点之间的通信主要用于维护集群的元数据信息(即每个节点包含哪些数据、节点是否出现故障等)。节点之间定期交换这些信息,确保集群内所有节点都对集群状态有一致的认知。这个过程可以理解为集群中的节点在“八卦”彼此的状态,最终每个节点都“听说”了其他节点的状态。

缓存节点1 缓存节点2 发送Meet消息 回复Pong消息 发送Ping消息 回复Pong消息 loop [定期通信] 缓存节点1 缓存节点2

其通信过程可以分为以下几个步骤:

  1. 独立的 TCP 通道:Redis 集群中的每个节点都开通了一个独立的 TCP 通道,专门用于与其他节点的通信。

  2. 定时发送 Ping 消息:每个节点都有一个定时任务,每隔一段时间(比如每秒 5 次)会从系统中随机选出一个其他节点,向其发送 Ping 消息。

  3. 接收并回复 Pong 消息:被 Ping 的节点在接收到 Ping 消息后,会礼貌性地回复一个 Pong 消息。这样,Ping 和 Pong 消息的往返可以实现节点之间的状态同步。

  4. 循环执行,保持通信:上述 Ping-Pong 消息的传递是一个持续的过程,使得集群内的所有节点都能不断获取其他节点的最新状态。

节点A 节点B 节点C 节点D 选择发送节点 发送Ping消息 回复Pong消息 发送Ping消息 回复Pong消息 发送Ping消息 回复Pong消息 loop [定时任务] 节点A 节点B 节点C 节点D

这种定期的通信机制保证了节点之间的同步和实时性,及时获知集群中节点的健康状况和数据分布。


Redis 集群中的消息类型

在 Redis 集群的 Gossip 协议中,不同类型的消息传递不同的信息,下面是四种常用的消息类型:

  1. Meet 消息:当集群中新增节点时,老节点会收到新节点发送的 Meet 消息。Meet 消息通知了已有节点:集群中来了一个新伙伴。老节点接收到 Meet 消息后,礼貌性地回复一个 Pong 消息,并开始与新节点进行后续的 Ping-Pong 通信。

  2. Ping 消息:这是最常见的消息类型。每个节点会定期向其他节点发送 Ping 消息,Ping 消息包含了节点的状态数据、负责的槽信息等。当接收节点收到 Ping 消息后,便能更新对该节点的状态认知。

  3. Pong 消息:Pong 消息通常是对 Ping 消息的回应,包含节点的状态信息。一个节点在接收到 Meet 或 Ping 消息时,会回复 Pong 消息,通知对方自己的最新状态。

  4. Fail 消息:当一个节点检测到其他节点出现故障时,会向集群内的其他节点广播 Fail 消息。这种消息在节点失效检测和故障恢复方面起着关键作用。通过 Fail 消息,整个集群可以在短时间内同步故障节点的信息,以便于及时处理故障。


Gossip 协议的消息结构

在 Redis 集群的 Gossip 协议中,消息传递的格式相对简单但富有信息量。以下是消息结构体 clusterMsg 的简化定义:

typedef struct
{
    char sig[4];        /* 信号标识 */
    uint32_t totlen;    /* 消息总长度 */
    uint16_t ver;       /* 协议版本 */
    uint16_t port;      /* TCP 端口号 */
    uint16_t type;      /* 消息类型(如 Meet、Ping、Pong、Fail) */
    uint16_t count;     /* 消息体包含的节点数 */
    uint64_t currentEpoch;  /* 当前发送节点的配置纪元 */
    uint64_t configEpoch;   /* 主从节点的配置纪元 */
    uint64_t offset;    /* 复制偏移量 */
    char sender[CLUSTER_NAMELEN]; /* 发送节点的节点名称 */
    unsigned char myslots[CLUSTER_SLOTS/8]; /* 发送节点的槽信息 */
    char slaveof[CLUSTER_NAMELEN];
    char myip[NET_IP_STR_LEN];    /* 发送节点的 IP 地址 */
    uint16_t flags;      /* 发送节点的标识,区分主从角色 */
    unsigned char state; /* 发送节点的集群状态 */
    unsigned char mflags[3]; /* 消息标识 */
    union clusterMsgData data; /* 消息正文 */
} clusterMsg;

结构体中包含了多种字段,其中:

  • type 表示消息的类型(如 Meet、Ping、Pong、Fail 等),
  • myslots 数组记录了节点负责的槽信息,每次发送时会将该信息发送给其他节点。

这些字段和数据类型的设计确保了消息能够携带充足的信息,使得节点间的通信不仅能传递基本状态,还能包含数据分布、节点角色等关键信息。


Redis 集群的高可用性与容错性

Redis 集群采用 Gossip 协议来维持节点状态,确保各节点能够对集群状态保持一致的认知。通过定期的 Ping-Pong 通信和故障广播(Fail 消息),即使某些节点发生故障,集群中的其他节点也能够快速感知,并对数据分布或角色进行调整,以保障服务的高可用性。


小结

Redis 集群中的 Gossip 协议使得每个节点能够实时了解集群的整体状况,实现了高效的分布式管理和数据同步。通过 Meet、Ping、Pong、Fail 等消息类型,各节点之间不断地“八卦”彼此的状态。这种机制不仅提高了 Redis 集群的容错能力,还为集群的扩展和动态调整提供了基础。

Redis 集群的 Gossip 通信机制具有以下优势:

  • 分布式和去中心化:每个节点都负责管理自己的状态,并通过 Gossip 协议广播给其他节点,避免了中心节点的单点故障。
  • 高效的故障检测:节点之间的定期通信让系统可以快速发现并处理故障,提高了集群的稳定性。
  • 动态扩展支持:当集群中新增节点时,Meet 消息可以自动通知老节点,从而实现集群的自动扩展。

通过 Gossip 协议,Redis 集群实现了高效的数据同步和节点管理,确保了系统在分布式场景下的高可用性和稳定性。


请求分布式缓存的路由

在 Redis 集群中,每个节点通过 Gossip 协议相互通信,实时共享集群状态,确保所有节点对集群的结构和状态保持一致。而对于客户端来说,要查询 Redis 集群中的数据,首要问题就是要知道数据存放在哪个节点上。

接下来我们看看Redis 集群如何通过槽信息与路由规则,实现客户端在多节点环境中准确访问数据,即使在数据迁移过程中,也能高效地完成重定向。


1. 槽(Slot)信息的存储与解析

Redis 集群将整个键空间划分为 16,384 个槽。通过对键的哈希值计算,每个键都会被映射到特定的槽中,这样可以让不同的槽分布在不同的节点上,形成分布式存储。槽分配的细节如下:

  • myslots 数组:每个 Redis 节点会使用一个 myslots 二进制位数组(bit array)来管理槽信息。总长度为 2,048 字节(即 16,384 位),每位表示一个槽。例如,若某节点负责 1, 2, 3 号槽,则其数组中的相应位会被置为 1。

    CLUSTER_SLOTS的取值是 16384,表示这个数组的长度是 16384/8=2048B,由于 1B=8bit,所以数组共包含 16 384 个 bit 位(二进制位)。每个节点分别用 1 个 bit 位来标记自己是否拥有某个槽的数据。

  • 快速判断槽的归属:借助位数组的结构,Redis 可以在 O(1) 的时间复杂度下判断某个节点是否存放了特定槽的数据。比如,只需检查二进制数组的第 2 位是否为 1,就能判断该节点是否负责槽编号为 2 的数据。


2. clusterState 结构与节点的槽映射

每个 Redis 节点会通过 Gossip 协议定期接收其他节点的槽信息,并将这些信息存储在本地的 clusterState 结构中。这个结构记录了整个集群的槽分布:

typedef struct clusterState {
    clusterNode *myself;  // 指向当前节点的指针
    clusterNode *slots[CLUSTER_SLOTS];  // 槽映射表
} clusterState;
  • 槽映射表(slots 数组)slots 数组包含 16,384 个元素,每个元素代表一个槽,槽中存放的是该槽对应的 clusterNode(即缓存节点)的信息。
  • 节点数据共享:每个 Redis 节点中都拥有一份完整的 clusterState,存放集群中所有槽和节点的对应关系。这样,集群中的每个节点都可以为客户端提供准确的路由信息。

3. Redis 客户端的数据路由过程

当 Redis 客户端请求数据时,路由过程如下:

客户端 节点1 节点2 计算槽号 CRC16(key) % 16384 发送请求 返回 MOVED 重定向 根据 MOVED 重定向发送请求 返回数据 客户端 节点1 节点2
  1. 计算槽号:客户端通过 CRC16(key) % 16384 计算出键对应的槽号。

  2. 定位节点:客户端将请求发送至目标节点(假设为节点 1),并尝试从该节点中获取数据。

  3. MOVED 重定向:若该槽数据已迁移至其他节点(例如节点 2),节点 1 会向客户端返回 MOVED 重定向请求,告知客户端目标节点的地址。

  4. 重新请求:客户端根据 MOVED 请求提供的节点地址,直接访问节点 2,从而成功获取数据。


4. 数据迁移期间的 ASK 重定向机制

Redis 支持在不下线服务的情况下进行槽数据迁移,即将槽从一个节点迁移至另一个节点。

此时,Redis 集群会通过 ASK 重定向机制来确保数据访问不中断:

客户端 节点1 节点2 发送请求 返回 ASK 重定向 发送 Asking 命令 确认数据状态并返回数据 客户端 节点1 节点2
  1. ASK 重定向:在迁移数据的过程中,如果客户端请求的槽数据暂未迁移至目标节点,原节点会返回 ASK 重定向,告知客户端目标节点地址。

  2. Asking 命令:客户端收到 ASK 请求后,向目标节点发送 Asking 命令,确认目标节点是否已存储该槽数据。

  3. 数据返回:目标节点接收 Asking 命令后,确认数据状态并返回给客户端。

这种机制确保了即便在数据迁移过程中,客户端也能顺利找到所需数据。


Redis 集群路由机制的优势

Redis 集群的路由和重定向机制在分布式环境中具备以下优势:

  • 高效路由:通过 CRC16 哈希算法快速定位槽,结合 MOVEDASK 重定向机制,Redis 集群能高效地支持客户端路由请求。
  • 全局状态同步:每个节点都拥有完整的 clusterState 结构,记录着集群的槽分布关系,保证所有节点的状态一致。
  • 迁移兼容性:在数据迁移期间,通过 ASK 重定向确保客户端访问不中断,实现了数据访问的平滑过渡。

这些设计让 Redis 集群在复杂的分布式环境中也能实现高效的路由与重定向,确保数据的稳定可用。


缓存节点的扩展和收缩

在分布式 Redis 集群中,缓存节点扩展和收缩的过程是通过数据槽的重新分配和迁移来实现的。

1. 缓存节点的扩展

在 Redis 集群中扩展缓存节点的常见原因包括业务增长、并发请求量增加或现有节点容量不足。当新节点加入集群时,集群需要重新分配槽,以使新的节点能承担一部分存储任务。这一过程主要涉及以下步骤:

1.1 新节点加入集群

新节点加入时无法直接与集群通信。此时,需要现有节点通过 cluster meet 命令来让新节点加入集群。例如,如果新节点的 IP 是 192.168.1.1,端口是 5002,可以在老节点上执行以下命令:

192.168.1.1:5003> cluster meet 192.168.1.1 5002

这样,新节点被集群“接纳”后,可以正式开始与其他节点通信。

1.2 槽分配和数据迁移

新节点上线后需要分配槽和数据。Redis 集群通过客户端指令让老节点将一部分槽的数据迁移到新节点上。

客户端 源节点 目标节点 AllMasters cluster setslot {slot} importing {sourceNodeId} cluster setslot {slot} migrating {targetNodeId} cluster getkeysinslot {slot} {count} migrate {targetIP} {targetPort} "" 0 {timeout} keys{keys} loop [数据迁移] cluster setslot {slot} node {targetNodeId} 广播槽分配信息 客户端 源节点 目标节点 AllMasters

具体的迁移过程包括:

  1. 设置槽导入状态:客户端向目标节点(新节点)发送指令,让它准备好接收槽数据:
    cluster setslot {slot} importing {sourceNodeId}
    
  2. 设置槽导出状态:客户端向源节点(老节点)发送指令,让它准备将对应槽的数据迁出:
    cluster setslot {slot} migrating {targetNodeId}
    
  3. 获取槽数据:源节点根据槽号获取需要迁移的数据键。此时使用的命令为:
    cluster getkeysinslot {slot} {count}
    
    这里的 count 表示将要迁移的数据条数。
  4. 批量数据迁移:源节点通过 migrate 命令将数据迁移到目标节点。例如:
    migrate {targetIP} {targetPort} "" 0 {timeout} keys {keys}
    
    这个命令可以通过流水线(pipeline)批量迁移数据,提高数据迁移效率。
  5. 槽迁移完成:数据迁移完成后,目标节点更新自己的槽信息,广播到整个集群,通知所有节点完成槽分配:
    cluster setslot {slot} node {targetNodeId}
    

2. 缓存节点的收缩

缓存节点的收缩通常发生在节点故障或集群需要缩减规模时。下线节点会将其负责的槽分配给其他节点,并通知其他节点停止与该节点通信。

2.1 槽数据迁移

与扩容类似,移除节点的过程也涉及数据迁移,不过方向相反:从待下线节点向其他主节点迁移数据。

迁移步骤基本相同,只是在数据迁移完成后,待下线节点还需要通知其他节点将自己标记为下线节点。


2.2 节点下线通知

在下线操作中,其他节点需要“遗忘”这个即将下线的节点。可以使用 cluster forget 命令通知全网的其他节点。例如:

cluster forget {downNodeId}

这个命令会在集群内广播,使所有节点不再将下线节点列入通信名单中。


缓存故障的发现和恢复

在 Redis 集群的分布式缓存系统中,节点故障检测与恢复是确保系统高可用性的关键手段。当集群中的某个节点因硬件或网络故障导致无法访问时,Redis 会通过检测机制自动标记下线节点,并进行故障恢复,以此维持系统的稳定性。


故障检测机制:主观下线与客观下线

Redis 集群通过两种方式确定节点是否故障:主观下线客观下线

1. 主观下线

当一个节点未能在设定时间内响应 Ping 请求,则发送 Ping 的节点会将该节点标记为主观下线(Subjective Down)。

节点A 节点B 发送 Ping 请求 返回 Pong 响应 更新最后一次通信时间 更新最后一次通信时间 标记节点 B 为主观下线 alt [节点B正常工作] [超过 cluster-node-timeout] 节点A 节点B

具体来说:

  • 节点 A 向节点 B 发送 Ping 请求。
  • 若节点 B 正常工作,则返回 Pong 响应,同时双方更新最后一次通信时间。
  • 如果节点 B 未能及时响应 Pong(超过配置项 cluster-node-timeout),节点 A 将节点 B 视为主观下线。

需要注意的是,主观下线是个体判断结果,仅代表节点 A 的“个人意见”。实际上,节点 B 可能只是因为与节点 A 的网络中断,而与其他节点保持正常通信。因此,主观下线不代表节点 B 真的下线。


2. 客观下线

Redis 采用投票机制来确定节点的客观下线。当多个节点都认为某节点主观下线时,就会触发客观下线(Objective Down)流程:

节点A 节点B 节点C 节点D 节点E 标记节点X为主观下线 标记节点X为主观下线 标记节点X为主观下线 发送主观下线信息 确认 发送主观下线信息 确认 统计主观下线投票 超过半数持有槽的主节点同意 标记节点X为客观下线 广播 Fail 消息 广播 Fail 消息 广播 Fail 消息 广播 Fail 消息 标记节点X为客观下线 标记节点X为客观下线 标记节点X为客观下线 标记节点X为客观下线 节点A 节点B 节点C 节点D 节点E
  • 当集群内超过半数持有槽的主节点都将某节点标记为主观下线,集群便将该节点标记为客观下线。
  • 客观下线消息会通过 Fail 消息在集群内广播,所有节点都会收到该消息,并将该节点标记为客观下线。

客观下线仅对主节点有效,不影响从节点。通过这种方式,集群能有效排除网络分区等非故障因素的干扰,确保故障检测的准确性。


故障恢复:主节点选举与自动故障转移

在标记主节点客观下线后,Redis 集群会从其从节点中选出一个节点替代它,完成故障恢复流程。以下是具体的恢复步骤。

原主节点 从节点1 从节点2 从节点3 主节点1 主节点2 主节点3 资格检查 检查断开时长 资格检查 检查断开时长 资格检查 检查断开时长 失去选举资格 保留选举资格 保留选举资格 alt [断开时长超过 cluster-node-timeout * cluster-slave-validity-factor] [断开时长在允许范围内] 触发选举 检查复制偏移量 触发选举 检查复制偏移量 触发选举 等待下一轮选举 alt [偏移量更大] [偏移量较小] 发起选举请求 更新 configEpoch 广播 FAILOVER_AUTH_REQUEST 广播 FAILOVER_AUTH_REQUEST 广播 FAILOVER_AUTH_REQUEST 投票选举 投票支持 投票支持 投票支持 成为新主节点 选举作废 alt [获得超过半- 数投票] [未获得足够- 票数] 广播新主节点信息 广播角色变更 广播角色变更 广播角色变更 广播角色变更 广播角色变更 广播角色变更 通知数据同步 通知数据同步 原主节点 从节点1 从节点2 从节点3 主节点1 主节点2 主节点3

1. 资格检查

在发生故障后,原主节点的所有从节点都需要通过资格检查。具体标准如下:

  • 每个从节点都会检测与主节点的断开时长。
  • 若断开时长超过 cluster-node-timeout * cluster-slave-validity-factor(默认值为 10),则该从节点失去选举资格。

资格检查的目的是防止因断开时长过长、数据不同步的从节点成为新的主节点,从而确保集群数据的正确性。

2. 触发选举

通过资格检查的从节点根据复制偏移量触发选举。复制偏移量表示从节点的数据同步进度,偏移量越大说明数据越新。在 Redis 中,复制偏移量更大的从节点会优先触发选举。

3. 发起选举请求

满足条件的从节点会发起选举请求并更新自己的配置纪元(configEpoch)。配置纪元是 Redis 集群的一个版本标识,随着集群状态的变化而递增。发起选举请求的从节点会将 FAILOVER_AUTH_REQUEST 消息广播到集群内,表示它准备竞选新的主节点。

4. 投票选举

只有主节点有投票权。当从节点发起选举请求后,集群中超过半数的主节点投票支持该从节点,该从节点便当选为新的主节点。如果在 cluster-node-timeout * 2 时间内未能获得足够的票数,选举作废,等待下一轮选举。

在投票过程中,偏移量较大的从节点通常会更早触发选举,因此更有可能赢得多数票。

5. 广播新主节点信息

新的主节点选出后,会将自己的角色变更信息广播给集群内的所有节点。新主节点负责故障节点的槽数据,并通知其他从节点完成数据同步。至此,故障主节点的恢复流程完成,新的主节点接替原主节点提供服务。


小结

Redis 集群的故障检测与恢复机制依赖 Ping/Pong 通信、主观下线、客观下线及投票选举机制。通过主观与客观下线的分离判断,Redis 集群能有效避免误判,并在主节点故障时快速选出新的主节点,实现自动化的故障转移与服务恢复。

这种机制不仅提升了系统的高可用性,也减少了手动干预的复杂性,使 Redis 集群能够在复杂的分布式环境中保持稳定运行。

在这里插入图片描述

你可能感兴趣的:(【分布式架构】,分布式,缓存)