Redis Cluster集群深度解析:数据分片与高可用完美结合!

难度:⭐⭐⭐⭐⭐ | 适合人群:想掌握Redis大规模部署方案的开发者


💥 开场:一次"爆仓"危机

时间: 双12大促前一天
地点: 公司作战室
事件: 容量评估

技术总监: "明天预计流量是平时的50倍,Redis能扛住吗?"

我: "应该没问题,我们用的主从 + 哨兵架构。"

哈吉米: "等等,你们现在数据量多大?"

我: "大概80GB。"

南北绿豆: "Redis单机内存多大?"

我: "100GB,还有20GB余量。" 😊

阿西噶阿西: "那明天流量暴增,数据量也会暴增啊!"

我: "这..." 😰


紧急计算:

当前数据量:80GB
预计流量增长:50倍
预计数据量:80GB × 5 = 400GB

单机内存:100GB
    ↓
内存不够!💥

技术总监: "怎么办?"

我: "加内存?买个1TB内存的服务器?" 🤔

哈吉米: "来不及了!而且单机内存太大,持久化都会很慢。"

南北绿豆: "这种情况必须用Redis Cluster集群,数据分片存储!"

阿西噶阿西: "对,多台机器分担数据,每台只存一部分,我给你们讲讲..."


🎯 第一问:为什么需要Cluster?

主从架构的局限性

哈吉米画图:

主从 + 哨兵架构:

        Master(100GB数据)
        /      |      \
     Slave1  Slave2  Slave3
    (100GB)(100GB)(100GB)

问题:
1. 每个节点都存储全量数据
2. 容量受限于单机内存
3. 写操作都在Master,压力大
4. 数据量大时,持久化慢

Cluster集群架构

Redis Cluster(数据分片):

Node1(33GB)  Node2(33GB)  Node3(34GB)
    ↓              ↓              ↓
  Slot 0-5460  Slot 5461-10922  Slot 10923-16383
    
总数据:100GB
每个节点:约33GB

优势:
✅ 容量可扩展(加节点)
✅ 写操作分散到多个节点
✅ 高可用(节点挂了,数据不丢)

Cluster vs 主从对比

维度主从+哨兵Cluster集群
数据存储每个节点全量数据数据分片存储
容量受限于单机内存可水平扩展
写性能Master单点分散到多个节点
高可用哨兵故障转移自动故障转移
适用场景数据量小(<100GB)数据量大(>100GB)
复杂度简单较复杂

🎲 第二问:Slot(槽)分片原理

什么是Slot?

南北绿豆: "Redis Cluster把数据分成16384个槽(Slot)。"

16384个槽(0-16383)
    ↓
分配给不同节点
    ↓
每个key通过哈希算法映射到某个槽
    ↓
存储到对应节点

槽分配

3个节点的分配:

Node1:Slot 0-5460      (5461个槽)
Node2:Slot 5461-10922  (5462个槽)
Node3:Slot 10923-16383 (5461个槽)

总共:16384个槽

6个节点的分配:

Node1:Slot 0-2729
Node2:Slot 2730-5460
Node3:Slot 5461-8191
Node4:Slot 8192-10922
Node5:Slot 10923-13652
Node6:Slot 13653-16383

数据路由算法

哈吉米: "key如何映射到槽?"

// 计算槽位
int slot = CRC16(key) % 16384;

// 例子:
CRC16("user:1") = 52143
52143 % 16384 = 2991
    ↓
Slot 2991 → 在Node2(5461-10922)
    ↓
数据存储到Node2

Hash Tag(哈希标签):

问题: 如果想让某些key存在同一个节点怎么办?

// 使用Hash Tag:{}中的内容用于计算槽
String key1 = "user:{123}:profile";
String key2 = "user:{123}:orders";
String key3 = "user:{123}:cart";

// 计算槽时,只用{123}计算
CRC16("123") % 16384 = 5555
    ↓
三个key都在Slot 5555,存在同一个节点
    ↓
可以使用MGET等批量操作

🔄 第三问:数据重定向

MOVED重定向

客户端请求流程:

sequenceDiagram
    participant Client
    participant Node1
    participant Node2
    
    Client->>Node1: GET user:1
    Note over Node1: 计算槽位:2991<br/>不在Node1(0-5460)
    
    Node1-->>Client: MOVED 2991 192.168.1.102:6379
    Note over Client: 收到重定向
    
    Client->>Node2: GET user:1
    Note over Node2: 槽2991在Node2
    Node2-->>Client: "张三"

ASK重定向

槽迁移过程中:

sequenceDiagram
    participant Client
    participant Node1
    participant Node2
    
    Note over Node1,Node2: 槽2991正在从Node1迁移到Node2
    
    Client->>Node1: GET user:1
    Note over Node1: 槽2991正在迁移中<br/>key可能在Node2
    
    Node1-->>Client: ASK 2991 192.168.1.102:6379
    
    Client->>Node2: ASKING
    Client->>Node2: GET user:1
    Node2-->>Client: "张三"

MOVED vs ASK

阿西噶阿西: "两种重定向的区别:"

类型含义客户端行为场景
MOVED槽已经迁移完成更新本地槽映射正常请求
ASK槽正在迁移中临时重定向,不更新映射槽迁移中

⚙️ 第四问:集群配置与搭建

最小集群(3主3从)

配置文件(每个节点一个):

# redis-7000.conf(Master1)
port 7000
cluster-enabled yes                    # 启用集群
cluster-config-file nodes-7000.conf    # 集群配置文件
cluster-node-timeout 5000              # 节点超时时间
appendonly yes
daemonize yes
pidfile /var/run/redis-7000.pid
logfile /var/log/redis/redis-7000.log
dir /var/lib/redis/7000

其他5个节点类似(改端口和路径):

redis-7000.conf(Master1)
redis-7001.conf(Master2)
redis-7002.conf(Master3)
redis-7003.conf(Slave1 - 对应Master1)
redis-7004.conf(Slave2 - 对应Master2)
redis-7005.conf(Slave3 - 对应Master3)

启动节点

# 启动6个Redis实例
redis-server /etc/redis/redis-7000.conf
redis-server /etc/redis/redis-7001.conf
redis-server /etc/redis/redis-7002.conf
redis-server /etc/redis/redis-7003.conf
redis-server /etc/redis/redis-7004.conf
redis-server /etc/redis/redis-7005.conf

# 检查是否启动
ps aux | grep redis

创建集群

Redis 5.0+使用redis-cli:

redis-cli --cluster create \
  192.168.1.100:7000 \
  192.168.1.100:7001 \
  192.168.1.100:7002 \
  192.168.1.100:7003 \
  192.168.1.100:7004 \
  192.168.1.100:7005 \
  --cluster-replicas 1

参数说明:

  • --cluster-replicas 1:每个Master有1个Slave

创建过程:

>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383

Adding replica 192.168.1.100:7003 to 192.168.1.100:7000
Adding replica 192.168.1.100:7004 to 192.168.1.100:7001
Adding replica 192.168.1.100:7005 to 192.168.1.100:7002

>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster

Waiting for the cluster to join...

>>> Performing Cluster Check
[OK] All nodes agree about slots configuration.
[OK] All 16384 slots covered.

查看集群状态

# 连接任意节点
redis-cli -c -p 7000

# 查看集群信息
redis> CLUSTER INFO

cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_known_nodes:6
cluster_size:3

# 查看节点信息
redis> CLUSTER NODES

a1b2c3d4... 192.168.1.100:7000@17000 myself,master - 0 1705900000 1 connected 0-5460
e5f6g7h8... 192.168.1.100:7001@17001 master - 0 1705900001 2 connected 5461-10922
i9j0k1l2... 192.168.1.100:7002@17002 master - 0 1705900001 3 connected 10923-16383
m3n4o5p6... 192.168.1.100:7003@17003 slave a1b2c3d4... 0 1705900002 1 connected
q7r8s9t0... 192.168.1.100:7004@17004 slave e5f6g7h8... 0 1705900002 2 connected
u1v2w3x4... 192.168.1.100:7005@17005 slave i9j0k1l2... 0 1705900003 3 connected

📊 第五问:集群扩容与缩容

扩容:新增节点

场景: 数据量又增长了,3个节点不够,要加到6个

步骤1:启动新节点

# 启动2个新节点(1主1从)
redis-server /etc/redis/redis-7006.conf
redis-server /etc/redis/redis-7007.conf

步骤2:加入集群

# 添加Master节点
redis-cli --cluster add-node 192.168.1.100:7006 192.168.1.100:7000

# 添加Slave节点
redis-cli --cluster add-node 192.168.1.100:7007 192.168.1.100:7000 \
  --cluster-slave \
  --cluster-master-id <7006节点的ID>

步骤3:重新分配槽

# 重新分片
redis-cli --cluster reshard 192.168.1.100:7000

# 交互式操作:
How many slots do you want to move? 4096  # 移动4096个槽给新节点
What is the receiving node ID? <7006节点ID>
Source node: all  # 从所有节点平均分配

分片过程:

sequenceDiagram
    participant Node1
    participant Node2
    participant Node3
    participant Node4(new)
    
    Note over Node1,Node3: 原来的槽分配<br/>Node1: 0-5460<br/>Node2: 5461-10922<br/>Node3: 10923-16383
    
    Node1->>Node4(new): 迁移槽 0-1364
    Node2->>Node4(new): 迁移槽 5461-6825
    Node3->>Node4(new): 迁移槽 10923-12287
    
    Note over Node1,Node4(new): 新的槽分配<br/>Node1: 1365-5460<br/>Node2: 6826-10922<br/>Node3: 12288-16383<br/>Node4: 0-1364, 5461-6825, 10923-12287

缩容:删除节点

步骤1:迁移槽

# 把要删除节点的槽迁移到其他节点
redis-cli --cluster reshard 192.168.1.100:7000 \
  --cluster-from <要删除节点的ID> \
  --cluster-to <目标节点ID> \
  --cluster-slots <槽数量>

步骤2:删除节点

# 先删除Slave
redis-cli --cluster del-node 192.168.1.100:7007 <7007节点ID>

# 再删除Master(必须已经没有槽)
redis-cli --cluster del-node 192.168.1.100:7006 <7006节点ID>

💥 第六问:故障转移

自动故障转移

当Master宕机时:

sequenceDiagram
    participant Slave1
    participant Slave2
    participant Master
    participant Node2
    participant Node3
    
    Note over Master: Master宕机!💥
    
    Slave1->>Master: PING
    Note over Slave1: 无响应
    
    Slave1->>Slave1: 标记Master为PFAIL<br/>(主观下线)
    
    Slave1->>Node2: 询问:Master状态?
    Node2-->>Slave1: PFAIL
    
    Slave1->>Node3: 询问:Master状态?
    Node3-->>Slave1: PFAIL
    
    Note over Slave1: 超过半数节点认为PFAIL<br/>标记为FAIL(客观下线)
    
    Slave1->>Slave1: 发起选举
    Note over Slave1: 我要升级为Master
    
    Node2->>Slave1: 投票
    Node3->>Slave1: 投票
    
    Note over Slave1: 获得超过半数投票<br/>升级为Master
    
    Slave1->>Slave1: SLAVEOF NO ONE
    Note over Slave1: 现在是Master了
    
    Slave1->>Node2: 广播:我是新Master
    Slave1->>Node3: 广播:我是新Master
    
    Slave2->>Slave1: 复制新Master

故障转移条件

触发条件:
1. Master被标记为FAIL
2. Master负责的槽至少有一个Slave
3. Slave数据最新(复制偏移量最大)

选举规则:
1. 复制偏移量最大的Slave优先
2. 获得超过半数节点投票
3. 升级为Master

💻 第七问:Java客户端使用

Jedis连接集群

@Configuration
public class RedisClusterConfig {
    
    @Bean
    public JedisCluster jedisCluster() {
        
        // 集群节点
        Set<HostAndPort> nodes = new HashSet<>();
        nodes.add(new HostAndPort("192.168.1.100", 7000));
        nodes.add(new HostAndPort("192.168.1.100", 7001));
        nodes.add(new HostAndPort("192.168.1.100", 7002));
        nodes.add(new HostAndPort("192.168.1.100", 7003));
        nodes.add(new HostAndPort("192.168.1.100", 7004));
        nodes.add(new HostAndPort("192.168.1.100", 7005));
        
        // 连接池配置
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(100);
        poolConfig.setMaxIdle(20);
        poolConfig.setMinIdle(10);
        
        // 创建集群连接
        return new JedisCluster(
            nodes,           // 节点列表
            2000,            // 连接超时
            2000,            // 读取超时
            5,               // 重试次数
            "password123",   // 密码
            poolConfig       // 连接池配置
        );
    }
}

使用:

@Service
public class UserService {
    
    @Autowired
    private JedisCluster jedisCluster;
    
    public void setUser(String userId, String userInfo) {
        // 自动路由到正确的节点
        jedisCluster.set("user:" + userId, userInfo);
    }
    
    public String getUser(String userId) {
        return jedisCluster.get("user:" + userId);
    }
    
    /**
     * 批量操作(必须用Hash Tag)
     */
    public void batchSet(String userId, Map<String, String> data) {
        
        // 使用Hash Tag保证在同一个槽
        for (Map.Entry<String, String> entry : data.entrySet()) {
            String key = "user:{" + userId + "}:" + entry.getKey();
            jedisCluster.set(key, entry.getValue());
        }
    }
}

Redisson连接集群

@Configuration
public class RedissonClusterConfig {
    
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        
        // 集群模式配置
        config.useClusterServers()
            .addNodeAddress(
                "redis://192.168.1.100:7000",
                "redis://192.168.1.100:7001",
                "redis://192.168.1.100:7002",
                "redis://192.168.1.100:7003",
                "redis://192.168.1.100:7004",
                "redis://192.168.1.100:7005"
            )
            .setPassword("password123")
            .setMasterConnectionMinimumIdleSize(10)
            .setMasterConnectionPoolSize(64)
            .setSlaveConnectionMinimumIdleSize(10)
            .setSlaveConnectionPoolSize(64)
            .setReadMode(ReadMode.SLAVE)  // 读从Slave
            .setSubscriptionMode(SubscriptionMode.MASTER);
        
        return Redisson.create(config);
    }
}

🎨 第八问:集群常用命令

集群管理命令

# 查看集群信息
CLUSTER INFO

# 查看集群节点
CLUSTER NODES

# 查看槽分配
CLUSTER SLOTS

# 查看某个key在哪个槽
CLUSTER KEYSLOT user:1
# 输出:2991

# 查看某个槽有多少key
CLUSTER COUNTKEYSINSLOT 2991

# 获取某个槽的key列表
CLUSTER GETKEYSINSLOT 2991 10  # 获取10个key

槽管理命令

# 将槽分配给节点
CLUSTER ADDSLOTS 0 1 2 3 4 5

# 移除节点的槽
CLUSTER DELSLOTS 0 1 2 3 4 5

# 设置槽的迁移状态
CLUSTER SETSLOT 2991 MIGRATING <目标节点ID>
CLUSTER SETSLOT 2991 IMPORTING <源节点ID>
CLUSTER SETSLOT 2991 NODE <节点ID>

💡 最佳实践

1. 节点数量规划

推荐:
    至少6个节点(3主3从)
    生产环境:6-12个节点
    
原因:
    - 至少3个Master(分片)
    - 每个Master至少1个Slave(高可用)
    - 奇数个Master(便于选举)
    
不推荐:
    - 少于6个节点:可用性不够
    - 超过20个节点:管理复杂

2. 硬件配置

推荐配置(每个节点):
    CPU:8核
    内存:32GB-64GB
    磁盘:SSD 500GB
    网络:万兆网卡
    
数据分布:
    每个节点存储总数据的 1/N(N=Master数量)
    
例如:
    总数据300GB,3个Master
    每个Master约100GB
    建议单机内存:128GB

3. 槽分配优化

# 手动优化槽分配(让分布更均匀)
redis-cli --cluster rebalance 192.168.1.100:7000 \
  --cluster-use-empty-masters  # 包括空Master

4. 监控指标

# 监控这些指标

# 1. 集群状态
CLUSTER INFO
cluster_state  # 必须是ok

# 2. 节点状态
CLUSTER NODES
# 检查是否有fail的节点

# 3. 槽覆盖
cluster_slots_assigned  # 必须是16384
cluster_slots_ok        # 必须是16384

# 4. 内存使用
INFO memory
used_memory_human

# 5. 网络延迟
CLUSTER NODES
# 查看节点间延迟

💡 知识点总结

Redis Cluster核心要点

Cluster架构

  • 数据分片存储(16384个槽)
  • 无中心架构(节点对等)
  • 自动故障转移

槽分片

  • CRC16(key) % 16384计算槽位
  • Hash Tag控制key分布
  • 槽均匀分配到Master

数据路由

  • MOVED重定向(槽已迁移)
  • ASK重定向(槽迁移中)
  • 客户端智能路由

扩容缩容

  • 添加节点:add-node
  • 分配槽:reshard
  • 删除节点:del-node

故障转移

  • 主观下线(PFAIL)
  • 客观下线(FAIL)
  • Slave自动升级为Master

最佳实践

  • 至少6个节点(3主3从)
  • 每个Master有Slave
  • 监控集群状态

记忆口诀

Cluster集群数据分,
一万六千槽来存。
哈希算法定位准,
三个Master是基础。
每个Master带Slave,
故障转移自动搞。
扩容加节点再分片,
缩容迁移槽再删。
MOVED槽已迁移完,
ASK迁移进行中。
生产至少六节点,
高可用来又能扩。

🤔 常见面试题

Q1: Redis Cluster如何分片?

A:

分片方式:
1. 16384个槽(0-16383)
2. CRC16(key) % 16384 计算槽位
3. 每个Master负责一部分槽
4. 数据根据槽位存储到对应Master

例如:
3个Master:
  Master1: 0-5460
  Master2: 5461-10922
  Master3: 10923-16383

key "user:1" → CRC16 → 槽2991 → 存到Master1

Q2: Cluster如何实现高可用?

A:

高可用机制:

1. 每个Master配Slave
   - Master挂了,Slave自动升级

2. 故障检测
   - 节点间互相PING
   - 超过半数认为下线才算下线

3. 自动故障转移
   - Slave发起选举
   - 获得半数以上投票
   - 升级为Master

4. 客户端自动切换
   - 连接哨兵或集群
   - 自动发现新Master

Q3: Cluster vs 主从+哨兵?

A:

选择Cluster的场景:
✅ 数据量大(>100GB)
✅ 需要横向扩展
✅ 写操作压力大

选择主从+哨兵的场景:
✅ 数据量小(<100GB)
✅ 架构简单
✅ 运维成本低

对比:
         容量      扩展性    复杂度    适用场景
Cluster   大       可扩展    复杂      大数据量
主从      受限     不可扩展   简单      中小数据量

Q4: Cluster有什么限制?

A:

限制:

1. 批量操作受限
   - MGET、MSET只能操作同一槽的key
   - 需要用Hash Tag

2. 事务受限
   - 只能对同一槽的key进行事务

3. 数据库选择受限
   - 只能使用DB 0

4. 复制结构单一
   - 只支持一层复制(Master-Slave)
   - 不支持Slave的Slave

💬 写在最后

从单机到主从,再到集群,我们深入学习了Redis的架构演进:

  • 🎲 理解了Slot槽分片原理
  • 🔄 掌握了数据路由和重定向
  • ⚙️ 学会了集群的搭建和管理
  • 📊 完成了扩容缩容操作

这篇文章,希望能让你掌握Redis大规模部署方案!

如果这篇文章对你有帮助,请:

  • 👍 点赞支持
  • ⭐ 收藏备用
  • 🔄 转发分享
  • 💬 评论交流