集群通过不同的节点维护不同的槽。 槽的变更:
- 集群创建时
- 新增/删除节点
迁移
客户端交互
一、概述
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
动态重分配方式
当节点变化时,通过以下步骤进行槽位迁移:
-
标记槽位状态
- 源节点:标记槽位为
MIGRATING(迁移中) - 目标节点:标记槽位为
IMPORTING(导入中)
- 源节点:标记槽位为
-
数据迁移
- 逐个将槽位中的键值对从源节点移动到目标节点
- 使用
MIGRATE命令进行原子性迁移
-
更新映射
- 集群所有节点更新槽位映射表
- 客户端通过
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 的请求:
├─ 直接连接目标节点
├─ 无需重定向
└─ 性能最优 ✓