Redis Cluster 集群深度解析

候选人小张在字节 P7 架构面中,面试官问:

"Redis Cluster 是怎么分片的?"

小张说:"用槽,16384 个槽。"

面试官追问:"槽是怎么分配的?主库挂了怎么选新主库?"

小张说:"哨兵可以选主...不对,Cluster 有自己的选主机制?"

小张卡住了。

【面试官心理】 这道题我用来测试候选人对 Redis Cluster 架构的理解深度。能说出槽概念的占 50%,能讲清选主机制的占 20%,能说清槽迁移的占 10%。

一、Redis Cluster 架构 🔴

1.1 数据分片

graph TD
    subgraph Cluster
        A[槽 0-5460<br/>主库1] --> B[从库1]
        C[槽 5461-10922<br/>主库2] --> D[从库2]
        E[槽 10923-16383<br/>主库3] --> F[从库3]
    end
    G[客户端] -->|"请求 key1|"| A
    G -->|"请求 key2|"| C
    G -->|"请求 key3|"| E

1.2 槽的分配

Redis Cluster 有 16384 个槽(slot)

槽的计算公式:slot = CRC16(key) % 16384

每个主库负责一部分槽:
- 主库1: 槽 0-5460
- 主库2: 槽 5461-10922
- 主库3: 槽 10923-16383

1.3 槽的计算过程

// CRC16 算法
unsigned int crc16(const char *key, int len) {
    // Redis 使用的 CRC16 实现
    // 用于将 key 映射到 0-16383 范围内
}

int slot = crc16(key, strlen(key)) % 16384;

二、故障检测与转移 🔴

2.1 故障检测

graph TD
    A[节点向集群广播 PING] --> B{节点收到 PONG?}
    B -->|"超时|"| C[标记为疑似下线 PFAIL]
    C --> D{多数主库认为疑似下线?}
    D -->|"是|"| E[标记为下线 FAIL]
    D -->|"否|"| F[恢复正常]
# 配置节点超时时间
cluster-node-timeout 15000  # 默认 15 秒

2.2 从库选举

graph TD
    A[主库下线] --> B[从库开始选举]
    B --> C[向集群广播消息]
    C --> D{收到多少主库投票?}
    D -->|"超过半数|"| E[成为新主库]
    D -->|"未超过|"| F[选举失败]
# 选举规则:
# 1. 从库 slave-replica-validity-factor > 0
# 2. 从库复制偏移量越接近主库,优先级越高
# 3. 收到超过半数主库的投票

2.3 集群状态

# 查看集群状态
redis-cli -c -h host -p 7001 cluster info

# 输出:
# cluster_state:ok
# cluster_slots_assigned:16384
# cluster_slots_ok:16384
# cluster_slots_pfail:0
# cluster_slots_fail:0
# cluster_known_nodes:6
# cluster_size:3
# cluster_current_epoch:6
# cluster_my_epoch:3
# cluster_stats_messages_ping_sent:1000
# cluster_stats_messages_pong_sent:1000

三、槽迁移 🟡

3.1 迁移过程

sequenceDiagram
    participant Client as 客户端
    participant OldMaster as 旧主库
    participant NewMaster as 新主库
    participant Cluster as 集群

    Client->>OldMaster: SET key value
    OldMaster->>Cluster: 标记槽迁移中
    OldMaster->>NewMaster: 迁移数据
    Client->>NewMaster: SET key value
    Note over NewMaster: ASK 转向:告诉客户端去新节点
    Client->>NewMaster: SET key value
    Note over NewMaster: 正常执行
    Cluster->>OldMaster: 标记槽已迁移

3.2 ASK 转向

# 当客户端请求的槽正在迁移时
# 旧主库返回 ASK 转向
ASK key value
# 或
ASKING

# 客户端收到 ASK 后,发送 ASKING 到新主库
# 然后再执行原来的命令

3.3 在线迁移槽

# 进入集群管理界面
redis-cli -c -h host -p 7001

# 重新分片
CLUSTER SETSLOT slot MIGRATING node-id
# 例如:
CLUSTER SETSLOT 100 MIGRATING 7c7d8e9f...

# 在目标节点导入
CLUSTER SETSLOT slot IMPORTING node-id

四、集群配置 🟡

4.1 创建集群

# 准备配置文件
# redis-7001.conf
port 7001
cluster-enabled yes
cluster-config-file nodes-7001.conf
cluster-node-timeout 15000
daemonize yes
pidfile /var/run/redis_7001.pid
logfile /var/log/redis/redis-7001.log
dbfilename dump-7001.rdb
appendfilename "appendonly-7001.aof"

# 启动节点
redis-server redis-7001.conf
redis-server redis-7002.conf
redis-server redis-7003.conf
redis-server redis-7004.conf
redis-server redis-7005.conf
redis-server redis-7006.conf

# 创建集群
redis-cli --cluster create \
    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 \
    127.0.0.1:7006 \
    --cluster-replicas 1
# --cluster-replicas 1 表示每个主库有 1 个从库

4.2 集群管理命令

# 查看集群节点
CLUSTER NODES

# 查看槽分配
CLUSTER SLOTS

# 添加主库
redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7001

# 添加从库
redis-cli --cluster add-node 127.0.0.1:7008 127.0.0.1:7001 --cluster-slave

# 重新分片
redis-cli --cluster reshard 127.0.0.1:7001

# 删除节点
redis-cli --cluster del-node 127.0.0.1:7007 node-id

五、客户端路由 🟡

5.1 Moved 重定向

# 客户端请求槽 100
redis-cli -c -h host -p 7001 GET key:100

# 如果槽 100 不在 7001 上
# 返回 MOVED 100 host:port
# 客户端自动转向正确节点
MOVED 100 127.0.0.1:7002

5.2 Jedis 客户端

// JedisCluster
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("host1", 7001));
nodes.add(new HostAndPort("host2", 7002));
nodes.add(new HostAndPort("host3", 7003));

JedisCluster cluster = new JedisCluster(
    nodes,
    3000,  // 连接超时
    3000,  // 读取超时
    3,     // 重试次数
    new JedisPoolConfig()
);

// 自动处理 MOVED 重定向
cluster.set("key", "value");
cluster.get("key");

六、集群限制 🟡

6.1 不支持的操作

# 以下操作在集群模式下不支持:
# - 多键操作(不在同一槽)
MGET key1 key2  # 可能返回错误

# - 跨槽的事务
MULTI
SET key1 value1
SET key2 value2
EXEC  # 可能失败

# - 跨槽的批量操作

6.2 解决方案

// 1. 使用 Hash Tag 确保 key 在同一槽
// Hash Tag 用大括号包裹
// {user:1}:profile  和 {user:1}:orders  会在同一槽

// 2. 使用客户端聚合
List<String> keys = Arrays.asList("key1", "key2", "key3");
// 计算每个 key 的槽,然后按槽分组
Map<Integer, List<String>> slots = new HashMap<>();
for (String key : keys) {
    int slot = crc16(key) % 16384;
    slots.computeIfAbsent(slot, k -> new ArrayList<>()).add(key);
}
💡

使用 Hash Tag 是解决跨槽操作的标准方法。例如按用户 ID 分组时,用 {user:123}:profile{user:123}:orders 可以保证在同一槽。

【面试官心理】 能说出"Hash Tag"解决跨槽问题的候选人,基本都有实际使用 Redis Cluster 的经验。这是 P6+ 的水准。