2056 字
10 分钟
Redis 高可用详解(2)集群模式

本文为 Redis 高可用架构 子篇,单独讲解 Redis 集群模式。

Redis 集群模式#

Redis Cluster 是官方的分片 + 高可用方案:数据按槽位分散到多个主节点,每个主节点可带从节点,集群自己完成故障检测与故障转移。

一个最小生产集群:3 主 3 从,16384 个槽均分到 3 个主节点,每个主节点在自己的从节点上有一份副本。

flowchart TB
    subgraph CL["客户端"]
        APP["应用"]
    end

    subgraph SHARD1["分片1 槽0-5460"]
        M1[(master-1)]
        S1[(replica-1)]
        M1 == "异步复制" ==> S1
    end

    subgraph SHARD2["分片2 槽5461-10922"]
        M2[(master-2)]
        S2[(replica-2)]
        M2 == "异步复制" ==> S2
    end

    subgraph SHARD3["分片3 槽10923-16383"]
        M3[(master-3)]
        S3[(replica-3)]
        M3 == "异步复制" ==> S3
    end

    APP -- "CRC16 mod 16384=5461 → 分片2" --> M2

    M1 <-. "gossip" .-> M2
    M2 <-. "gossip" .-> M3
    M3 <-. "gossip" .-> M1

    M1 -. "跨槽请求 → -MOVED 5461 10.0.0.12:6379" .-> APP
    M2 -. "槽迁移中 → -ASK" .-> APP

    classDef master fill:#fde2e4,stroke:#c9184a,stroke-width:2px
    classDef replica fill:#e2ece9,stroke:#2d6a4f
    class M1,M2,M3 master
    class S1,S2,S3 replica

读图要点:

  • 槽是分片的最小单位:客户端先算 CRC16(key) mod 16384 得到槽,再由本地映射表找到负责该槽的主节点。3 个主节点各分到约 5461 个槽。
  • MOVED 与 ASK 是”纠错”而非”代理”:节点不会代转命令,只返回重定向错误,因此客户端必须支持 Cluster(否则直接用不了)。
  • 客户端处理流程:本地缓存 键→槽→节点 映射,遇 MOVED 刷新,遇 ASK 单次重定向。
  • gossip 让集群去中心化:没有哨兵这样的独立控制面,节点之间互探、互投,故障检测(cluster-node-timeout)与选举投票都走 gossip。
  • 每个分片都是一主多从:主节点故障时由从节点发起选举并接管槽位;若某个主节点没有从节点,它挂掉后这些槽无人接管,配合默认的 cluster-require-full-coverage yes 会导致整个集群拒绝服务。
  • 复制依然是异步的:切换同样可能丢”未同步的写入”。

数据分片与槽位#

概念说明
哈希槽(hash slot)固定 16384 个槽,集群的最小分配单位
键 → 槽CRC16(key) mod 16384,客户端与集群用同一套算法
槽 → 节点建集群时分配(redis-cli --cluster create),可在线迁移(resharding)
单键命令限制一个命令涉及的所有 key 必须在同一槽,否则报 CROSSSLOT
Hash Tag用 {} 指定参与哈希的部分:user:{1001}:name 与 user:{1001}:age 落在同槽,这是让多键操作合法的唯一手段
Terminal window
# 建集群:3 主 3 从(--cluster-replicas 1 表示每个主配 1 个从)
redis-cli --cluster create \
10.0.0.11:6379 10.0.0.12:6379 10.0.0.13:6379 \
10.0.0.14:6379 10.0.0.15:6379 10.0.0.16:6379 \
--cluster-replicas 1
redis-cli --cluster info 10.0.0.11:6379 # 集群概览
redis-cli --cluster check 10.0.0.11:6379 # 体检:槽覆盖、配置纪元、主从关系
redis-cli -c -h 10.0.0.11 CLUSTER NODES # 每个节点的角色、槽范围、flags

重定向:MOVED 与 ASK#

客户端把命令发错节点时,节点不会代转,而是返回重定向错误:

返回含义客户端动作
-MOVED 3999 10.0.0.12:6379该槽已经永久属于另一个节点更新本地槽位映射表,然后重发到目标节点
-ASK 3999 10.0.0.12:6379该槽正在迁移,这个 key 已搬到目标节点只对本次请求发 ASKING + 命令到目标节点,不更新本地映射

理解要点:MOVED 是”搬家完成”,ASK 是”搬家进行中”。支持集群的客户端会对槽映射做缓存,所以正常运行时重定向很少;一旦频繁出现 MOVED,通常说明客户端没缓存好映射,或集群刚做过 resharding。

扩容与缩容(resharding)#

Terminal window
# 加入新节点(先以空主节点加入,再重新分配槽)
redis-cli --cluster add-node 10.0.0.17:6379 10.0.0.11:6379
# 让新节点成为某主节点的从节点
redis-cli --cluster add-node 10.0.0.18:6379 10.0.0.11:6379 --cluster-slave --cluster-master-id <node-id>
# 迁移槽:交互式(会提示迁移多少个槽、从哪些节点迁)
redis-cli --cluster reshard 10.0.0.11:6379
# 非交互:把 1000 个槽从 source 迁到 target
redis-cli --cluster reshard 10.0.0.11:6379 \
--cluster-from <source-id> --cluster-to <target-id> \
--cluster-slots 1000 --cluster-yes
# 删除节点(先从集群里移除空节点,缩容前要先把槽迁走)
redis-cli --cluster del-node 10.0.0.11:6379 <node-id>

在线迁移单个槽的过程(对应 ASK 的来源):

  1. 目标节点执行 CLUSTER SETSLOT <slot> IMPORTING <source-id>;
  2. 源节点执行 CLUSTER SETSLOT <slot> MIGRATING <target-id>;
  3. 源节点对该槽内的 key 逐个 MIGRATE 到目标节点(迁移期间该槽的请求:key 还在源 → 正常处理;key 已迁走 → 返回 ASK);
  4. 全部 key 迁完后,向所有节点广播 CLUSTER SETSLOT <slot> NODE <target-id>,迁移完成,此后变为 MOVED。

集群的故障转移#

步骤说明
1. 主观下线(PFAIL)节点在 cluster-node-timeout 内未收到某节点的 PONG,把它标为 PFAIL
2. 客观下线(FAIL)节点通过 gossip 收集其他节点的判断,当超过半数主节点也认为该节点 PFAIL,则广播 FAIL
3. 从节点发起选举故障主节点的从节点在延迟 (rank * 1s) 后发起选举请求(rank 与数据新旧相关,数据越新的从节点越早发起)
4. 主节点投票持有槽的主节点投票,每个主节点在每个纪元(epoch)只能投一票,得票需超过半数持有槽的主节点
5. 提升与接管当选从节点执行 CLUSTER FAILOVER 的等价动作,提升为主、接管槽位,并广播新的 configEpoch
6. 客户端更新映射客户端收到 MOVED 或通过 CLUSTER SLOTS/CLUSTER SHARDS 刷新槽映射
sequenceDiagram
    autonumber
    participant CS as 客户端
    participant M1 as master-1<br/>(故障)
    participant MR as 其他持槽主节点<br/>master-2 / master-3
    participant S1 as replica-1

    Note over M1,S1: 阶段一:故障检测(经 gossip 传播)
    MR->>M1: PING,cluster-node-timeout 内无 PONG
    MR->>MR: 标记 M1 为 PFAIL(主观下线)
    MR->>S1: gossip 广播"我认为 M1 PFAIL",互相确认
    Note over MR,S1: 认为 PFAIL 的持槽主节点数 > 半数<br/>→ 广播 FAIL(客观下线达成)

    Note over MR,S1: 阶段二:从节点自荐 + 主节点投票
    activate S1
    S1->>S1: 延迟 rank × 1s 后发起选举
    S1->>MR: FAILOVER_AUTH_REQUEST(携带 currentEpoch)
    MR->>S1: FAILOVER_AUTH_ACK(每个主节点每纪元仅一票)
    Note over S1: 得票 > 半数持槽主节点 → 当选
    deactivate S1

    Note over CS,S1: 阶段三:接管槽位与客户端纠正
    activate S1
    S1->>S1: 提升为 master,接管 M1 的槽<br/>并广播新 configEpoch
    CS->>M1: 请求 M1 的槽(客户端映射已过期)
    M1-->>CS: -MOVED slot 10.0.0.14:6379
    CS->>S1: 重发到新主
    S1-->>CS: 正常响应,客户端刷新槽映射
    deactivate S1
Terminal window
# 手工触发故障转移(演练用)
redis-cli -h 10.0.0.14 -p 6379 CLUSTER FAILOVER # 在当前从节点上执行
redis-cli -h 10.0.0.14 -p 6379 CLUSTER FAILOVER TAKEOVER # 无需与主节点协商,强制接管(危险)
# 观察
redis-cli --cluster check 10.0.0.11:6379
redis-cli -h 10.0.0.11 CLUSTER INFO # cluster_state:ok / cluster_slots_assigned 等

什么情况下集群会整体不可用?

情况结果
某主节点故障且没有从节点该主节点的槽无人接管,集群状态变为 fail,整个集群拒绝所有请求(CLUSTERDOWN),不只是这部分数据不可用
故障主节点的从节点也全挂同上
多数主节点不可达无法形成多数派,故障转移无法完成,集群不可用
cluster-require-full-coverage yes(默认)只要有槽未覆盖就整体拒绝服务;设为 no 则只影响未覆盖的槽(牺牲一致性换可用性)

集群通信与限制#

  • gossip 协议:节点之间用 CLUSTER MEET 建立连接后,通过 gossip 消息周期性交换”节点状态 + 槽位分配 + 纪元”,实现去中心化的故障检测与配置传播。集群内部总线端口 = 客户端端口 + 10000(cluster-port 可改),防火墙要同时放通两个端口。
  • cluster-node-timeout:故障判定门槛。设太小会因网络抖动误判切换,太大会拖长 RTO。跨机房部署需显著调大(例如 5~15s)。
  • 跨槽限制:MGET、MSET、事务(MULTI)、Lua 脚本涉及的 key 必须在同一槽;跨槽需要客户端拆分或使用 hash tag 归并。
  • 不支持 SELECT 多库:集群只有 db0。
  • KEYS/SCAN 只作用于当前节点:全量遍历必须遍历所有主节点,FLUSHALL 也只在单节点生效。
  • 主从仍是异步复制:切换同样会丢”未同步的写入”,WAIT 依然可用。
  • 客户端必须支持集群:Jedis/Lettuce/Redisson/go-redis 等主流客户端都支持,但要注意连接池与重定向处理配置。

什么时候不该用 Cluster?

场景建议
单实例内存足够主从 + 哨兵,运维复杂度低得多
需要跨槽事务 / 频繁多键操作用 hash tag 归并,或改用客户端分片(Codis/Twemproxy,但需接受其局限)或代理方案
需要 KEYS/SCAN 全量遍历客户端遍历各节点,或另建离线索引
希望”分片对应用透明”考虑 Redis 协议兼容的分布式方案(如 Tendis、云厂商的集群代理模式)
Redis 高可用详解(2)集群模式
https://blog-l7wd3.pages.dev/posts/ha/redis-ha-cluster/
作者
L7WD3-Xiao
发布于
2026-09-17
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取