redis集群槽的概念及迁移过程

62 阅读7分钟

集群通过不同的节点维护不同的槽。 槽的变更:

  1. 集群创建时
  2. 新增/删除节点

迁移

客户端交互

一、概述

Redis 集群是 Redis 提供的分布式解决方案,允许数据分散存储在多个 Redis 节点上,提供高可用性和可扩展性。

核心特性

1. 数据分片(Sharding)

  • 使用哈希槽(Hash Slot)机制,共 16384 个槽位
  • 每个键通过 CRC16 算法映射到对应槽位
  • 槽位分配给不同的节点,实现数据分布式存储

2. 高可用性

  • 支持主从复制(Master-Slave)
  • 自动故障转移:主节点宕机时,从节点自动晋升为主节点
  • 无需外部工具,集群自我管理

3. 可扩展性

  • 支持动态添加/删除节点
  • 集群自动重新分配槽位和数据
  • 水平扩展存储容量和处理能力

架构组成

┌─────────────────────────────────────┐
│      Redis 集群(6个节点示例)       │
├─────────────────────────────────────┤
│ Master1(0-5461)  ↔  Slave1         │
│ Master2(5462-10922) ↔  Slave2      │
│ Master3(10923-16383) ↔  Slave3     │
└─────────────────────────────────────┘

部署最低要求

  • 最少节点数:3个主节点(建议6个:3主3从)
  • 通信端口:每个节点需要2个端口(普通+集群总线端口)

主要优势

优势说明
高性能数据分片提高吞吐量
高可用自动故障转移,无单点故障
易扩展动态增删节点
自治性无需外部依赖

主要限制

⚠️ 需要注意的问题

  • 不支持多键事务(跨槽位操作)
  • 不支持 Lua 脚本跨多个键
  • 客户端需要支持集群协议
  • 管理复杂度相对较高

二、Redis 集群槽位分配的时间和方式

槽位分配的确定时间

1. 集群创建时

在初始化 Redis 集群时,槽位分配就已经确定。使用 redis-cli --cluster create 命令创建集群时,系统会自动将 16384 个槽位均匀分配给各个主节点。

2. 节点加入/离开时

当集群运行中有节点加入或移除时,槽位分配会动态调整。指出"当节点加入或离开集群时,槽位分配动态调整以确保数据分布和可用性",以及"Redis 集群支持在集群运行时添加或删除节点"。

槽位分配的方式

初始分配方式

对于 N 个主节点的集群,槽位分配采用均匀分配策略:

每个主节点分配的槽位数 = 16384 ÷ N

示例(3个主节点):
- Master1:0 - 5461
- Master2:5462 - 10922  
- Master3:10923 - 16383

动态重分配方式

当节点变化时,通过以下步骤进行槽位迁移:

  1. 标记槽位状态

    • 源节点:标记槽位为 MIGRATING(迁移中)
    • 目标节点:标记槽位为 IMPORTING(导入中)
  2. 数据迁移

    • 逐个将槽位中的键值对从源节点移动到目标节点
    • 使用 MIGRATE 命令进行原子性迁移
  3. 更新映射

    • 集群所有节点更新槽位映射表
    • 客户端通过 CLUSTER SLOTS 命令获取最新映射 [1]

三、槽位迁移过程中的客户端读写机制详解

一、客户端的基本架构

Redis 集群客户端的三层结构

┌─────────────────────────────────────┐
│      应用层(业务代码)              │
└─────────────────────────────────────┘
                ↓
┌─────────────────────────────────────┐
│   客户端库(redis-py、jedis 等)    │
├─────────────────────────────────────┤
│  ├─ 槽位映射缓存                     │
│  ├─ 连接池管理                       │
│  ├─ 重定向处理                       │
│  └─ 重试机制                         │
└─────────────────────────────────────┘
                ↓
┌─────────────────────────────────────┐
│      Redis 集群节点                  │
├─────────────────────────────────────┤
│  ├─ 主节点(MIGRATING/IMPORTING)   │
│  └─ 从节点                           │
└─────────────────────────────────────┘

二、槽位映射缓存机制

1. 初始化:获取集群拓扑

客户端启动时,首先获取完整的槽位映射:

CLUSTER SLOTS

返回格式

1) 1) (integer) 0           # 槽位起始
   2) (integer) 5460        # 槽位结束
   3) 1) "127.0.0.1"        # 主节点 IP
      2) (integer) 7000     # 主节点端口
   4) 1) "127.0.0.1"        # 从节点 IP
      2) (integer) 7001     # 从节点端口

2) 1) (integer) 5461
   2) (integer) 10921
   3) 1) "127.0.0.1"
      2) (integer) 7002
   ...

2. 客户端缓存结构

# 客户端内部维护的槽位映射表
slot_map = {
    0-5460: {
        'master': ('127.0.0.1', 7000),
        'slaves': [('127.0.0.1', 7001)]
    },
    5461-10920: {
        'master': ('127.0.0.1', 7002),
        'slaves': [('127.0.0.1', 7003)]
    },
    10921-16383: {
        'master': ('127.0.0.1', 7004),
        'slaves': [('127.0.0.1', 7005)]
    }
}

# 连接池
connection_pool = {
    ('127.0.0.1', 7000): Connection(),
    ('127.0.0.1', 7001): Connection(),
    ...
}

3. 缓存的更新时机

客户端启动
    ↓
执行 CLUSTER SLOTS
    ↓
构建本地槽位映射缓存
    ↓
业务查询/更新
    ↓
遇到 MOVED/ASK 重定向
    ↓
更新缓存(仅 MOVED)
    ↓
重试请求

三、读操作(GET)的完整流程

正常情况:槽位未迁移

客户端执行 GET mykey
    ↓
计算 key 的槽位:slot = CRC16(mykey) % 16384
    ↓
查询本地缓存:slot_map[slot] → ('127.0.0.1', 7000)
    ↓
从连接池获取连接
    ↓
发送 GET mykey 到 ('127.0.0.1', 7000)
    ↓
节点返回 value
    ↓
客户端返回结果 ✓

迁移情况1:ASK 重定向(槽位迁移中)

客户端执行 GET mykey
    ↓
计算 slot,查询缓存 → ('127.0.0.1', 7000)
    ↓
发送 GET mykey 到 7000
    ↓
源节点(MIGRATING 状态)检查:
    ├─ key 存在 → 返回 value ✓
    └─ key 不存在 → 返回 "-ASK 5461 127.0.0.1:7002"
                   (槽位已迁移到 7002)
    ↓
客户端收到 ASK 重定向
    ↓
客户端执行:
    1. 连接 127.0.0.1:7002
    2. 发送 ASKING 命令
    3. 发送 GET mykey
    4. 获取 value ✓
    ↓
【关键】不更新本地缓存
(因为迁移可能还在进行中)
    ↓
返回结果 ✓

ASK 重定向的代码流程 [2]:

def get(key):
    slot = calculate_slot(key)
    node = slot_map[slot]
    
    try:
        return send_command(node, "GET", key)
    except AskRedirect as e:
        # e.target_node = 目标节点
        # 一次性重定向,不更新映射
        connection = get_connection(e.target_node)
        connection.send("ASKING")
        return connection.send("GET", key)

迁移情况2:MOVED 重定向(槽位迁移完成)

客户端执行 GET mykey
    ↓
计算 slot,查询缓存 → ('127.0.0.1', 7000)
    ↓
发送 GET mykey 到 7000
    ↓
源节点检查:key 不存在
    ↓
返回 "-MOVED 5461 127.0.0.1:7002"
     (槽位所有权已转移)
    ↓
客户端收到 MOVED 重定向
    ↓
【关键】更新本地缓存:
    slot_map[5461] = ('127.0.0.1', 7002)
    ↓
客户端执行:
    1. 连接 127.0.0.1:7002
    2. 发送 GET mykey
    3. 获取 value ✓
    ↓
后续相同 slot 的请求直接使用新映射 ✓

MOVED 重定向的代码流程

def get(key):
    slot = calculate_slot(key)
    node = slot_map[slot]
    
    try:
        return send_command(node, "GET", key)
    except MovedRedirect as e:
        # e.target_node = 新节点
        # 永久重定向,更新映射
        slot_map[slot] = e.target_node  # 更新缓存!
        connection = get_connection(e.target_node)
        return connection.send("GET", key)

四、写操作(SET)的完整流程

正常情况:槽位未迁移

客户端执行 SET mykey myvalue
    ↓
计算 slot,查询缓存 → ('127.0.0.1', 7000)
    ↓
发送 SET mykey myvalue 到 7000
    ↓
节点写入数据
    ↓
返回 OK ✓

迁移情况1:源节点拒绝写入(MIGRATING 状态)

客户端执行 SET mykey myvalue
    ↓
计算 slot,查询缓存 → ('127.0.0.1', 7000)
    ↓
发送 SET mykey myvalue 到 7000
    ↓
源节点状态:MIGRATING
    ├─ key 已迁移 → 拒绝写入
    └─ 返回 "-MOVED 5461 127.0.0.1:7002"
    ↓
客户端收到 MOVED 重定向
    ↓
更新本地缓存:slot_map[5461] = ('127.0.0.1', 7002)
    ↓
重试:发送 SET mykey myvalue 到 7002
    ↓
目标节点(IMPORTING 状态)接受写入
    ↓
返回 OK ✓

关键点 [2]:源节点在 MIGRATING 状态下,拒绝对已迁移键的写入

迁移情况2:目标节点接受写入(IMPORTING 状态)

客户端执行 SET newkey newvalue
    ↓
计算 slot,查询缓存 → ('127.0.0.1', 7000)
    ↓
发送 SET newkey newvalue 到 7000
    ↓
源节点状态:MIGRATING
    ├─ key 不存在(新键)
    └─ 拒绝写入新键
    └─ 返回 "-MOVED 5461 127.0.0.1:7002"
    ↓
客户端更新缓存并重试
    ↓
发送 SET newkey newvalue 到 7002
    ↓
目标节点状态:IMPORTING
    ├─ 接受新键写入
    └─ 返回 OK ✓

五、ASK vs MOVED 的详细对比

ASK 重定向(迁移中) [2]

源节点状态:MIGRATING
目标节点状态:IMPORTING
槽位状态:正在转移中

客户端行为:
1. 连接源节点 → 收到 ASK
2. 连接目标节点 → 发送 ASKING 命令
3. 执行实际命令
4. 【不更新】本地槽位映射

下次相同 slot 的请求:
├─ 仍然先连接源节点
├─ 如果 key 在源节点 → 直接返回
└─ 如果 key 在目标节点 → 再次 ASK 重定向

ASKING 命令的作用 [2]:

目标节点处于 IMPORTING 状态时,会拒绝不带 ASKING 的请求:

客户端不发送 ASKING:
    SET key value
    ↓
目标节点:这个槽位不属于我,拒绝!
    返回 "-CLUSTERDOWN" 或错误

客户端发送 ASKING:
    ASKING
    SET key value
    ↓
目标节点:收到 ASKING,临时授权
    接受 SET 命令 ✓

MOVED 重定向(迁移完成) [3]

源节点状态:正常(槽位已转移)
目标节点状态:正常(拥有槽位)
槽位状态:已完全转移

客户端行为:
1. 连接源节点 → 收到 MOVED
2. 【更新】本地槽位映射
3. 连接目标节点 → 执行命令

下次相同 slot 的请求:
├─ 直接连接目标节点
├─ 无需重定向
└─ 性能最优 ✓