引言

单个Redis实例内存有限(通常16-64GB),如何存储TB级数据?如何实现水平扩展? Redis Cluster提供了原生的分布式解决方案。

一、Cluster概述

1.1 为什么需要Cluster?

主从复制+哨兵的局限

主从架构:
  ┌──────────┐
  │ Master   │  (单节点存储所有数据)
  └────┬─────┘
       │ 复制
  ┌────┴────┬────────┐
  │ Slave1  │ Slave2 │  (仅读扩展,不能写扩展)
  └─────────┴────────┘

问题:
❌ 单点容量瓶颈(受限于单机内存)
❌ 写性能无法扩展(所有写操作都在主节点)
❌ 主节点故障影响所有数据访问

Cluster分片架构

Cluster:
  ┌───────┐  ┌───────┐  ┌───────┐
  │Master1│  │Master2│  │Master3│  (数据分片存储)
  │Slave1 │  │Slave2 │  │Slave3 │
  └───────┘  └───────┘  └───────┘
     ↓          ↓          ↓
  存储1/3    存储1/3    存储1/3
   数据        数据        数据

优势:
✅ 容量扩展(添加节点)
✅ 写性能扩展(多个主节点)
✅ 高可用(每个主节点有从节点)

1.2 核心概念

  1. 分片(Sharding):数据分散存储在多个节点
  2. 槽位(Slot):16384个槽位,分配给不同节点
  3. 节点(Node):独立的Redis实例
  4. 主从复制:每个主节点可有多个从节点

二、槽位机制

2.1 什么是槽位?

Redis Cluster共有16384个槽位(0-16383)

槽位分配示例(3个主节点):
Master1:0-5460    (5461个槽位)
Master2:5461-10922  (5461个槽位)
Master3:10923-16383 (5461个槽位)

2.2 key到槽位的映射

计算公式

slot = CRC16(key) % 16384

示例:
key="user:1001"
CRC16("user:1001") = 51234
slot = 51234 % 16384 = 1698

查找:slot 1698在Master1上 → 访问Master1

哈希标签(Hash Tag)

// 确保多个key在同一个节点
jedis.set("{user:1001}:name", "Alice");
jedis.set("{user:1001}:age", "25");

// {user:1001}部分用于计算slot
// 两个key都在同一个节点上

2.3 槽位分配

手动分配

# 添加节点
redis-cli --cluster add-node 127.0.0.1:7000 127.0.0.1:7001

# 分配槽位
redis-cli --cluster reshard 127.0.0.1:7000
# 提示:How many slots? 5461
# 提示:What is the receiving node ID? <node-id>
# 提示:Source node? all

自动分配

# 创建集群(自动分配槽位)
redis-cli --cluster create \
  127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \
  --cluster-replicas 1

三、节点通信

3.1 Gossip协议

每个节点维护集群元数据

  • 所有节点列表
  • 槽位分配信息
  • 节点状态(在线/下线)

通信方式

节点1 → 节点2:发送PING(携带部分元数据)
节点2 → 节点1:回复PONG(携带部分元数据)

效果:
- 最终一致性(元数据最终在所有节点同步)
- 去中心化(无需配置中心)

3.2 节点发现

新节点加入

1. redis-cli --cluster add-node <new-node> <existing-node>
2. 新节点与集群中任一节点建立连接
3. 通过Gossip协议,新节点发现所有其他节点
4. 所有节点发现新节点

3.3 故障检测

节点1定期向所有节点发送PING:
  ↓
节点2超时无响应(超过cluster-node-timeout)
  ↓
节点1标记节点2为PFAIL(疑似下线)
  ↓
节点1通过Gossip通知其他节点
  ↓
多数主节点也认为节点2下线
  ↓
标记节点2为FAIL(确认下线)
  ↓
触发故障转移

四、客户端重定向

4.1 MOVED重定向

场景:key不在当前节点

客户端 → 节点1:GET user:1001
          ↓
       计算slot=1698,发现slot在节点2
          ↓
节点1 → 客户端:MOVED 1698 127.0.0.1:7001
          ↓
客户端 → 节点2:GET user:1001
          ↓
节点2 → 客户端:返回value

客户端优化

  • 缓存槽位分配信息
  • 直接访问目标节点(避免重定向)

4.2 ASK重定向

场景:槽位正在迁移

槽位1698正在从节点1迁移到节点2:
  ↓
客户端 → 节点1:GET user:1001
          ↓
节点1查找:key已迁移 → 返回ASK 1698 127.0.0.1:7002
          ↓
客户端 → 节点2:ASKING + GET user:1001
          ↓
节点2 → 客户端:返回value

MOVED vs ASK

  • MOVED:槽位永久分配给其他节点
  • ASK:槽位临时迁移(迁移完成后恢复)

五、故障转移

5.1 故障检测

节点1(主节点)宕机:
  ↓
从节点1-1检测到主节点下线
  ↓
发起选举:向其他主节点请求投票
  ↓
获得多数投票 → 提升为主节点
  ↓
接管原主节点的槽位

5.2 选举规则

投票规则

  1. 每个主节点有1票
  2. 从节点offset越大(数据越新),越容易获得投票
  3. 获得多数票(N/2+1)的从节点成为新主节点

5.3 手动故障转移

# 在从节点执行
127.0.0.1:7001> CLUSTER FAILOVER

# 强制故障转移(不等待数据同步)
127.0.0.1:7001> CLUSTER FAILOVER FORCE

六、集群扩容与缩容

6.1 扩容

添加主节点

# 1. 添加节点
redis-cli --cluster add-node 127.0.0.1:7003 127.0.0.1:7000

# 2. 分配槽位
redis-cli --cluster reshard 127.0.0.1:7000
# 从现有节点迁移部分槽位到新节点

# 3. 添加从节点
redis-cli --cluster add-node 127.0.0.1:7004 127.0.0.1:7003 --cluster-slave

6.2 缩容

删除节点

# 1. 迁移槽位到其他节点
redis-cli --cluster reshard 127.0.0.1:7000
# 将节点3的槽位全部迁移走

# 2. 删除从节点
redis-cli --cluster del-node 127.0.0.1:7004 <node-id>

# 3. 删除主节点
redis-cli --cluster del-node 127.0.0.1:7003 <node-id>

七、最佳实践

7.1 节点规划

推荐

  • 最少6个节点(3主3从)
  • 奇数个主节点(3、5、7)
  • 分布式部署(不同机器/机房)

7.2 槽位分配

推荐:
- 3个主节点:每个5461-5462个槽位
- 5个主节点:每个约3277个槽位

避免:
- 槽位分配不均(导致热点)

7.3 监控指标

# 集群状态
redis-cli --cluster check 127.0.0.1:7000

# 关键指标:
# - cluster_state: ok/fail
# - cluster_slots_assigned: 16384(全部分配)
# - cluster_slots_ok: 16384(全部正常)
# - cluster_known_nodes: 节点数量

7.4 客户端配置

Jedis示例

import redis.clients.jedis.JedisCluster;

Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("127.0.0.1", 7000));
nodes.add(new HostAndPort("127.0.0.1", 7001));
nodes.add(new HostAndPort("127.0.0.1", 7002));

JedisCluster cluster = new JedisCluster(nodes);

// 使用
cluster.set("key", "value");
String value = cluster.get("key");

// 批量操作(同一个哈希标签)
cluster.mset("{user:1001}:name", "Alice", "{user:1001}:age", "25");

八、Cluster局限性

8.1 不支持的命令

# ❌ 多key操作(key在不同节点)
MGET key1 key2 key3  # 可能在不同节点

# ✅ 使用哈希标签
MGET {user}:key1 {user}:key2  # 确保在同一节点

8.2 事务限制

# ❌ 多key事务(key在不同节点)
MULTI
SET key1 value1  # 可能在节点1
SET key2 value2  # 可能在节点2
EXEC

# ✅ 使用哈希标签或Lua脚本

8.3 性能开销

Gossip协议:
- 每秒发送PING/PONG消息
- 元数据传播
- 网络带宽占用(节点越多越明显)

建议:
- 节点数 < 1000
- 使用万兆网络

九、总结

核心要点

  1. Cluster分片

    • 16384个槽位
    • CRC16(key) % 16384
    • 哈希标签确保多key在同一节点
  2. 节点通信

    • Gossip协议
    • 去中心化
    • 最终一致性
  3. 客户端重定向

    • MOVED:永久重定向
    • ASK:临时重定向
  4. 故障转移

    • 自动检测故障
    • 从节点自动提升
    • 选举机制
  5. 扩容缩容

    • 动态添加/删除节点
    • 槽位在线迁移
  6. 最佳实践

    • 最少6节点(3主3从)
    • 奇数个主节点
    • 使用哈希标签

第二阶段完成总结

🎉 恭喜!架构原理篇(10篇文章)全部完成!

已掌握的核心知识

  1. Redis对象系统与内存模型
  2. SDS、跳表、字典、intset、ziplist底层数据结构
  3. 单线程模型与IO多路复用
  4. 主从复制、哨兵、Cluster高可用架构

下一阶段预告: 第三阶段《进阶特性篇》将深入:

  • Lua脚本编程
  • Bitmap、HyperLogLog、GEO、Stream高级数据类型
  • 分布式锁、布隆过滤器
  • 延迟队列、限流算法

期待继续学习!