Chapter 08

Redis Cluster 集群

从单机到 Cluster——四种部署模式的演进、16384 个槽的分片设计与高可用原理

四种部署模式演进

模式一:单机模式

Client ──▶ Redis(单节点) 优点:简单 缺点:单点故障,容量受单机内存限制

模式二:主从复制(Replication)

Client ──写──▶ Master ──异步复制──▶ Slave1 ──读──▶ Slave1 ──▶ Slave2 优点:读写分离,Slave 可作备份 缺点:Master 故障需手动切换,有数据丢失风险(异步复制)
# Slave 配置(redis.conf)
replicaof 192.168.1.10 6379   # 指定 Master
replica-read-only yes           # Slave 只读
replica-lazy-flush no           # 全量同步时是否异步清空旧数据

模式三:哨兵模式(Sentinel)

Sentinel 高可用架构 Sentinel1 Sentinel2 Sentinel3 ← 奇数个 Sentinel ↑ ↑ ↑ └───────────┴───────────┘ 监控 Master (6379) ↙ ↘ Slave1 (6380) Slave2 (6381) Master 故障时:Sentinel 投票选出新 Master,自动完成故障转移
# sentinel.conf
sentinel monitor mymaster 192.168.1.10 6379 2  # 2个Sentinel认为故障才切换
sentinel down-after-milliseconds mymaster 5000 # 5秒无响应认为下线
sentinel failover-timeout mymaster 60000       # 故障转移超时60秒

模式四:Cluster 集群

Redis Cluster 是官方原生的分布式解决方案,支持数据自动分片、节点间通信、自动故障转移,是大规模部署的首选。

Redis Cluster 最小配置(3主3从) Master1 Master2 Master3 槽 0-5460 槽 5461-10922 槽 10923-16383 ↕ ↕ ↕ Slave1 Slave2 Slave3 节点间通过 gossip 协议通信(端口 = 服务端口 + 10000)

哈希槽(Hash Slot)分片算法

Redis Cluster 使用固定的 16384 个哈希槽(Hash Slot)对数据分片:

键到槽的映射 slot = CRC16(key) % 16384 SET user:1001 "Alice" CRC16("user:1001") = 12891 12891 % 16384 = 12891 落在 Master3 负责的槽范围(10923-16383) 请求路由到 Master3

为什么是 16384 个槽?

16384(2^14)是一个平衡点:槽数量越多,节点间传播槽信息的心跳包越大;越少,节点数量扩展受限。作者认为 16384 个槽,1000 个节点的集群中心跳包约 2KB,可以接受。

哈希标签(Hash Tag)

通过 {} 标签强制多个 key 落在同一槽,解决 MSET、pipeline、事务跨槽的限制:

# 不带标签:三个 key 可能在不同槽
SET user:1001:name "Alice"
SET user:1001:age  25
SET user:1001:city "Shanghai"

# 带哈希标签:花括号内的内容决定槽
# CRC16("{user:1001}") % 16384,三个 key 在同一槽
SET {user:1001}:name "Alice"
SET {user:1001}:age  25
SET {user:1001}:city "Shanghai"

# 现在可以在 pipeline 中一起操作

搭建 Cluster(Docker Compose)

# docker-compose.yml
version: '3.8'
services:
  redis-1: { image: redis:7-alpine, command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly yes --port 7001, ports: ["7001:7001"] }
  redis-2: { image: redis:7-alpine, command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly yes --port 7002, ports: ["7002:7002"] }
  redis-3: { image: redis:7-alpine, command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly yes --port 7003, ports: ["7003:7003"] }
  redis-4: { image: redis:7-alpine, command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly yes --port 7004, ports: ["7004:7004"] }
  redis-5: { image: redis:7-alpine, command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly yes --port 7005, ports: ["7005:7005"] }
  redis-6: { image: redis:7-alpine, command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly yes --port 7006, ports: ["7006:7006"] }
# 启动并初始化集群(--cluster-replicas 1 表示每个 Master 1个 Slave)
docker-compose up -d
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

# 查看集群状态
redis-cli -p 7001 cluster info
redis-cli -p 7001 cluster nodes

Python 连接 Cluster

from redis.cluster import RedisCluster, ClusterNode

# 连接集群(只需指定部分节点,客户端自动发现其余节点)
startup_nodes = [
    ClusterNode("127.0.0.1", 7001),
    ClusterNode("127.0.0.1", 7002),
]
rc = RedisCluster(
    startup_nodes=startup_nodes,
    decode_responses=True,
    skip_full_coverage_check=True
)

# 单 key 操作:自动路由到正确节点
rc.set("user:1001", "Alice")
print(rc.get("user:1001"))

# 批量操作:使用哈希标签确保同一槽
with rc.pipeline() as pipe:
    pipe.set("{user:1001}:name", "Alice")
    pipe.set("{user:1001}:age", "25")
    pipe.execute()

gossip 协议与脑裂问题

gossip 通信

Cluster 节点通过 gossip 协议相互传播节点状态、槽分配信息。每个节点定期向其他节点发送 PING,维护完整的集群视图,无需中心协调节点。

脑裂问题

网络分区时,部分节点互相隔离。如果隔离的 Slave 被选为 Master,而原 Master 也继续服务,就会出现"双 Master"(脑裂),导致数据不一致。

# 防止脑裂配置:当副本数不足时,Master 停止写入
min-replicas-to-write 1   # 至少有1个副本在同步才允许写
min-replicas-max-lag 10   # 副本延迟不超过10秒

Cluster 的限制

限制原因解决方案
MSET/MGET 跨槽报错多个 key 分布在不同节点使用哈希标签 {tag}
事务(MULTI/EXEC)跨槽报错同上哈希标签 / Lua 脚本(同槽内有效)
只有 DB 0 可用Cluster 模式禁用 SELECT改用 key 前缀区分业务
Pub/Sub 广播效率低消息需广播到所有节点使用 SSUBSCRIBE(分片 Pub/Sub)
Lua 脚本只能操作同槽 key同上设计时保证 Lua 访问的 key 在同一槽

Cluster 下的 SCAN 陷阱:在 Cluster 模式下,SCAN 命令只扫描当前连接节点上的 key,无法跨节点扫描全部 key。如需扫描所有 key,需要对每个 Master 节点分别执行 SCAN,并在客户端合并结果。redis-py 的 RedisCluster 提供了 scan_iter() 方法,自动处理多节点扫描。

gossip 协议深度解析

Redis Cluster 没有集中式的协调节点(如 ZooKeeper),而是通过 gossip 协议在节点间自主传播集群状态,实现去中心化管理:

PING/PONG 心跳
每个节点每秒随机选择几个节点发送 PING。PING 包含自己的状态和已知的其他节点信息。收到 PING 的节点回复 PONG,包含自己的最新状态。通过这种病毒式传播,整个集群的状态变化在 O(log N) 个节点间隔内同步完成。
MEET 消息
当新节点加入集群时,向已有节点发送 MEET 消息。收到 MEET 的节点会记录新节点,并通过后续 gossip 将新节点信息扩散到整个集群,无需人工配置每对节点关系。
FAIL 标记传播
当某节点被超过半数 Master 标记为疑似下线(PFAIL),集群广播 FAIL 消息。收到 FAIL 的节点立即将该节点标记为确认下线,触发故障转移流程。
Cluster 故障转移流程 Master3 宕机 其他节点发现 PING 超时 → 标记 Master3 为 PFAIL 超半数 Master 确认 → 广播 FAIL 消息 Slave3 发起选举:向所有 Master 请求投票 获得超半数 Master 投票 → Slave3 提升为 Master Slave3(新 Master)接管槽 10923-16383,广播新槽分配 整个故障转移时间:通常 cluster-node-timeout × 2 = 10 秒

Cluster 数据迁移(槽重新分配)

向集群添加新节点时,需要将部分槽从旧节点迁移到新节点,期间不影响服务:

# 添加新节点到集群
redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7001

# 重新分片:将 1000 个槽迁移到新节点
redis-cli --cluster reshard 127.0.0.1:7001 \
  --cluster-from all \
  --cluster-to <new-node-id> \
  --cluster-slots 1000 \
  --cluster-yes

# 查看迁移进度
redis-cli --cluster check 127.0.0.1:7001

# 删除节点(需先将其槽全部迁走)
redis-cli --cluster del-node 127.0.0.1:7001 <node-id>

迁移期间的 MOVED 和 ASK 重定向:迁移期间,客户端请求某个槽的 key 时,可能收到 MOVED(槽已完全迁走)或 ASK(槽正在迁移中)响应。redis-py 等客户端会自动处理重定向,但如果使用裸连接或自研客户端,必须实现这两种重定向逻辑,否则会出现"访问错误节点"的错误。

Cluster 模式下的 ACL

Redis 6.0+ 的 ACL 在 Cluster 中为每个节点独立配置,但可以使用 ACL 文件在所有节点保持一致:

# 在所有节点的 redis.conf 中引用同一份 ACL 文件
aclfile /etc/redis/users.acl

# /etc/redis/users.acl
# 格式:user <username> <flags> <password> <commands> <keys> <channels>

# 应用服务用户:只能读写 app: 前缀的 key,允许所有频道
user app-service on >app-secret-2024 ~app:* &* +@read +@write +@string +@hash -FLUSHDB -FLUSHALL

# 只读监控用户:查看信息但不能修改数据
user monitor on >monitor-pass ~* &* +INFO +DBSIZE +CLUSTER +LATENCY -@write

# 热更新 ACL(无需重启,所有节点执行)
redis-cli -a <admin-pass> ACL LOAD

# 验证权限
redis-cli -u "redis://app-service:app-secret-2024@localhost:7001" ACL WHOAMI

Cluster 监控与运维命令

# 集群整体状态(cluster_state: ok 表示正常)
redis-cli -p 7001 CLUSTER INFO

# 各节点状态(查看主从关系、槽分配、节点 ID)
redis-cli -p 7001 CLUSTER NODES

# 检查集群健康状态(包括槽覆盖情况)
redis-cli --cluster check 127.0.0.1:7001

# 修复集群(处理槽未完全覆盖等问题)
redis-cli --cluster fix 127.0.0.1:7001

# 各节点负载均衡情况
redis-cli --cluster rebalance 127.0.0.1:7001 --cluster-use-empty-masters

# 查看某个 key 属于哪个槽和节点
redis-cli -p 7001 CLUSTER KEYSLOT user:1001
redis-cli -p 7001 CLUSTER GETKEYSINSOT <slot> <count>
本章小结

Redis 部署模式从简到复杂依次是:单机 → 主从复制(读写分离)→ Sentinel(自动故障转移)→ Cluster(数据分片 + 高可用)。
Cluster 核心机制:16384 个哈希槽平均分配给 Master 节点,CRC16(key)%16384 决定 key 落在哪个槽。哈希标签 {tag} 可强制多 key 同槽,解决 MSET/事务跨槽问题。
高可用:gossip 协议实现去中心化节点通信;超半数 Master 确认故障才触发转移,防止误判;min-replicas-to-write 防止脑裂导致数据丢失。
运维注意:Cluster 只能使用 DB 0;SCAN 需对每个 Master 分别执行;Lua 脚本和事务只能操作同槽 key;ACL 需同步到所有节点。
何时使用 Cluster:单节点内存不足(>100GB)、或需要写操作水平扩展时。中小规模(单机 32~64GB)优先使用 Sentinel,运维更简单。