Files
cs-note/hhs/Redis/07-集群方案.md
T
2026-06-08 23:08:57 +08:00

26 KiB
Raw Blame History

tags, create time
tags create time
Redis
缓存
高可用
Cluster
分布式
2026-05-15 18:14

Redis Cluster 集群方案

概述

Redis Cluster 是 Redis 官方的分布式解决方案,通过数据分片(sharding)将数据分布在多个节点上,支持横向扩展和自动故障转移。核心设计:**哈希槽(Hash Slots)**而非一致性哈希。

flowchart TD
    C1["Client"] --> N1["Node A: slot 0~5460"]
    C1 --> N2["Node B: slot 5461~10922"]
    C1 --> N3["Node C: slot 10923~16383"]

    N1 -->|"复制"| NA["Node A Replica"]
    N2 -->|"复制"| NB["Node B Replica"]
    N3 -->|"复制"| NC["Node C Replica"]

哈希槽机制

为什么不用一致性哈希?

[!NOTE] 一致性哈希是什么? 一致性哈希是一种分布式分片算法,它将所有节点和数据映射到一个虚拟的哈希环上。数据沿顺时针找到的第一个节点就是它的归属。核心优势是:增减节点时,只需要迁移相邻区间的少量数据。更多细节见 hhs/Redis/07-集群方案/一致性哈希。

对比项 一致性哈希 哈希槽
增节点迁移量 ~N/K(N 为总 key 数,K 为节点数) 新节点分得 16384/(K+1) slots,约 2730 个(6 Master 时)
客户端透明性 ✅ 通常透明,按哈希环顺时针归属 ✅ 主流客户端(go-redis/jedis)内置处理 MOVED/ASK 重定向,对业务透明
数据倾斜 依赖虚拟节点数量平衡 ❌ slots 分配不均时会倾斜
复杂度 简单,纯哈希环 略复杂(Gossip + 槽位元数据同步)

[!QUESTION] 思考 Redis 选择了 16384 个槽而不是其他数字,你觉得为什么是这个数?太少了会怎样?太多了又会怎样?

[!NOTE] 为什么是 16384(2¹⁴)? Redis 作者 antirez 在 GitHub Issue #2576 中明确解释过:

  1. Gossip 消息大小限制:每个节点的槽位信息用 bitmap 表示,16384 slots = 2KB(16384/8 字节),放入心跳包后带宽可控;若用 65536 slots 则需 8KB,心跳报文膨胀明显。
  2. 节点数量上限:Redis Cluster 官方建议最多 1000 个节点。16384 个槽在 1000 节点时,平均每个节点 16 个槽,粒度已经足够细。
  3. 计算效率:16384 是 2 的幂,CRC16 取模可直接用位运算 & 0x3FFF,无需真正执行除法。

因此,16384 是槽位粒度与心跳带宽之间的工程折中,而非哈希冲突的考量。

槽位计算

Redis 使用 CRC16 算法(Cyclic Redundancy Check,循环冗余校验——一种广泛用于数据校验的快速哈希算法)对 key 取模,结果映射到 0~16383 共 16384 个槽:

# CRC16(key) % 16384 = slot 编号
# 例:CRC16("foo") % 16384 = 5798 → 属于 Node B (5461~10922)
// Go redis 库内部自动计算,开发者无需关心
rdb.Set(ctx, "foo", "bar", 0) // 自动路由到对应节点

[!TIP] 多 key 在同一节点的技巧——用 {} 包裹 tag:

SET {user:100}:name alice   → slot X
SET {user:100}:age 25       → 同 slot X
SADD {user:100}:friends user:200

这样 user:100 的所有操作都在同一个节点上,支持 MULTI/LUA 事务。

[!NOTE] hash tag 的边界

  • {user:100}:name 中只有 {user:100} 部分参与 hash 计算,{} 后面的 :name 是实际 key 的一部分。
  • 如果 key 中没有 {},则整个 key 参与 hash 计算。
  • 务必确保 tag 逻辑正确,否则同一用户的不同属性可能分散在不同节点,导致事务失败。

通信协议与 Gossip 协议

Gossip 协议是一种去中心化的信息传播协议,灵感来自"八卦传播"——每个节点定期向随机选择的邻居交换状态信息,几轮之后信息便传遍集群。Redis Cluster 用它同步节点状态和槽位分配,无需中心化协调者。详见 hhs/Redis/07-集群方案/Gossip协议。

节点间通信端口

节点通信端口 = client_port + 10000
例如: 6379 → 16379 (cluster bus)

[!WARNING] 防火墙配置 生产环境中必须开放 port 和 port+10000 两个端口。很多云服务器安全组默认只开一个端口,会导致集群组建失败或节点状态异常。

Gossip 协议运作方式

每个节点每秒向随机选择的节点发送 PING/PONG,维持集群状态的一致性。选择数量为固定公式:1 个长期未通信节点 + min(5, node_count/10) 个随机节点:

sequenceDiagram
    participant A as Node A
    participant B as Node B

    loop 每秒钟
        A->>B: PING (包含自身状态 + 随机选 3 个其他节点)
        B->>A: PONG (含自身 + 选中节点的状态)
    end

    Note over A: Gossip 收敛原理:<br/>每节点每轮向 min 6 台对等体发消息,<br/>O(log N) 轮后信息传遍集群

Gossip 传播的三类信息:

  1. PING/PONG — 心跳,携带发送者的状态摘要
  2. MEET — 手动加入集群,向指定节点发送 MEET 指令
  3. FAIL — 某节点不可达,广播给其他节点

[!QUESTION] 思考 Redis 的 Gossip 选择策略(源码 cluster.c:clusterCron()):每次选 1 个长时间未通信的随机节点 + 5 个随机节点(上限为集群节点总数的 1/10),而非广播给所有节点。这是固定公式,不因集群规模动态切换。为什么不广播所有节点?1000 节点的集群中,每秒广播意味着 O(N²) 条消息——这正是 Gossip 用随机子集换取带宽的关键设计。

PING/PONG 报文详解

PING 报文体包含:
├── 发送节点的 nodeId
├── 槽位分配表(哪些节点负责哪些 slot)
├── 节点状态(master/slave/fail)
├── 最后通信时间戳
└── 当前集群 epoch(配置版本号,每次集群拓扑变更如 failover 或 reshard 时递增,用于解决节点间的状态冲突——epoch 更大的配置被视为最新真相)

数据迁移流程

移动一个 slot(跨节点)

# 在源节点执行(准备阶段)
CLUSTER SETSLOT <slot_id> MIGRATING <target_node_id>

# 在目标节点执行(确认阶段)
CLUSTER SETSLOT <slot_id> IMPORTING <source_node_id>

# 逐 key 迁移
# GETKEYSINSLOT: 返回指定 slot 中的 key 列表,每次最多 100 个
# MIGRATE: 原子地将 key 从源节点转移到目标节点(同步阻塞,最后一个参数 5000 是超时 ms)
for key in $(redis-cli -h $SRC -p $SRC_PORT CLUSTER GETKEYSINSLOT $slot 100); do
    redis-cli -h $SRC -p $SRC_PORT MIGRATE $DST $DST_PORT $key 0 5000
done

# 完成迁移
CLUSTER SETSLOT <slot_id> NODE <target_node_id>

[!NOTE] 迁移期间的 ASK 临时定向 当 slot 处于 MIGRATING/IMPORTING 中间状态时:

  • 源节点拥有该 key → 正常处理
  • 源节点找不到该 key(已迁移走) → 返回 ASK <target-node>,客户端只需本次请求转发到目标节点,后续请求仍发原节点。

这保证了迁移过程对客户端透明,且不会造成路由表的提前混乱。

自动化迁移 —— redis-cli cluster reshard

redis-cli --cluster reshard node-host:6379 \
    --cluster-from <src-node-id> \
    --cluster-to <dst-node-id> \
    --cluster-slots 5461 \
    --cluster-yes

[!WARNING] reshard 期间服务不中断 但涉及迁移的 slot 在高负载下可能有额外延迟,低峰期操作更稳妥。

创建集群

手动创建(3 Master + 3 Replica)

# 1. 启动 6 个实例(不同端口)
for port in $(seq 7001 7006); do
  mkdir -p /tmp/$port && redis-server \
    --port $port --cluster-enabled yes \
    --appendonly yes --cluster-config-file nodes.conf \
    --cluster-node-timeout 5000 \
    --loglevel warning > /tmp/$port/redis.log 2>&1 &
done

# 2. 一行命令组建集群
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

Docker Compose(开发环境)

version: "3.9"
services:
  redis-1:
    image: redis:7-alpine
    command: >
      redis-server
      --cluster-enabled yes
      --cluster-config-file nodes.conf
      --cluster-node-timeout 5000
      --appendonly yes
    ports:
      - "7001:7001"
      - "17001:17001"  # cluster bus 端口,必须暴露
    volumes:
      - redis-data-1:/data
    networks:
      - redis-cluster
  # redis-2 ~ redis-6 同理,修改端口即可
  # 注意每个实例的 port 也需要通过 --port 指定
  redis-2:
    image: redis:7-alpine
    command: >
      redis-server
      --port 7002
      --cluster-enabled yes
      --cluster-config-file nodes.conf
      --cluster-node-timeout 5000
      --appendonly yes
    ports:
      - "7002:7002"
      - "17002:17002"
    volumes:
      - redis-data-2:/data
    networks:
      - redis-cluster
  # ... redis-3 ~ redis-6 类推

networks:
  redis-cluster:
    driver: bridge

volumes:
  redis-data-1:
  redis-data-2:
  # ...

[!WARNING] Docker 网络注意 集群组建时各节点需要通过 IP 互相访问,如果在不同机器部署,--net=host 模式或自定义网络(指定 cluster-announce-ip)是必须的。本地开发用 127.0.0.1 即可,但生产环境切勿依赖 localhost。

连接集群客户端

import "github.com/redis/go-redis/v9"

rdb := redis.NewClusterClient(&redis.ClusterOptions{
    Addrs:      []string{":7001", ":7002", ":7003", ":7004", ":7005", ":7006"},
    Password:   "your-password",
    MaxRetries: 3,
})
defer rdb.Close()

// SET/GET 自动路由到正确的节点
rdb.Set(ctx, "key", "value", 0)
val, _ := rdb.Get(ctx, "key").Result()

Cluster 的局限性

限制 说明 替代方案
不支持多数据库 固定只有 db 0 应用层模拟 db(key 前缀)
命令子集受限 KEYS, MIGRATE, SORT 等不可用 用 SCAN 替代 KEYS
跨节点多 key 命令受限 DEL, MGET, MSET, RENAME 等多 key 操作要求 key 同 slot 用 hash tag 或应用层拆分为多次单 key 调用
Lua 脚本有限 所有 key 必须映射到同一 slot;不支持非确定性函数(time/random) 同 Lua 内 key 用同一 hash tag;用 EVALSHA 减少网络传输
Pipeline 受限 同一 pipeline 中所有 key 必须在同一节点 用 hash tag {user:100}:x 对齐
MULTI/EXEC 受限 同上,跨 key 事务不支持 用 Lua 脚本实现原子操作

[!TIP] 解决 Pipeline 跨节点问题

// ❌ 错误:key 分布在不同节点
pipe.Pipeline().
    Get(ctx, "user:1:name").
    Get(ctx, "user:2:name")

// ✅ 正确:用 hash tag 对齐到同一节点
pipe.Pipeline().
    Get(ctx, "{user:1}:name").
    Get(ctx, "{user:1}:age")

运维命令速查

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

# 查看集群整体信息(快速判断集群是否健康)
redis-cli -c -p 7001 CLUSTER INFO
# 关键字段:
#   cluster_state:ok          ← ok 表示正常,fail 表示有 slot 不可用
#   cluster_slots_assigned:16384  ← 应为 16384,否则有 slot 未分配
#   cluster_slots_ok:16384
#   cluster_known_nodes:6     ← 集群已知节点总数
#   cluster_size:3            ← Master 节点数量
#   cluster_current_epoch:6   ← 当前集群纪元,每次 failover/reshard 递增

# 查看节点信息
redis-cli -p 7001 CLUSTER NODES
# 输出格式: nodeId ip:port@busPort flags master/slot-range self

# 示例输出解析
e3a1b2c3... 192.168.1.10:7001@17001 master - 0 1715000000000 1 connected 0-5460
# ↑ node-id        ↑ addr              ↑ bus-port  ↑ role  ↑ epoch  ↑ slots

# 统计各节点 key 数量
redis-cli -p 7001 DBSIZE
redis-cli -c -p 7001 --cluster call DBSIZE  # 在所有节点执行

# 强制某个 slot 下线(紧急情况)
redis-cli --cluster fix 127.0.0.1:7001

扩容指南

flowchart LR
    Before["3 Master<br/>各 5461 slots"] -->|添加 3 新节点| Middle["6 Master<br/>slot 全空"]
    Middle -->|reshard 迁 slot| After["6 Master<br/>各 ~2730 slots"]
    After -->|加 replica| Final["6 Master + 6 Replica"]

[!TIP] 扩容节奏建议

  1. 先加 Master 并分 slot
  2. 再给每个 Master 配 Replica
  3. 分批迁移(每次 1~2 台),避免流量剧变

数据倾斜检测与 Rebalance

扩容或运行一段时间后,各节点的 key 数量可能不均——有些节点"撑"了很多 key,有些却"空"着,这就是数据倾斜。

# 查看各节点 key 数量,快速判断是否倾斜
redis-cli -c -p 7001 CLUSTER COUNTKEYSINSLOT 0    # 查某个 slot 有多少 key
redis-cli --cluster rebalance 127.0.0.1:7001      # 自动均衡 slot 分布

[!NOTE] reshard vs rebalance

  • reshard:手动指定从 A 搬多少 slot 到 B,精确控制
  • rebalance:自动计算各节点应有 slot 数,自动迁移,省心但不可控

生产环境建议先用 rebalance --dry-run 预览迁移计划,确认无误后再执行。

[!WARNING] Rebalance 不能解决 key 分布不均 rebalance 均衡的是 slot 数量,而非 key 数量。如果某些 slot 内 key 特别多(比如大量 key 哈希到同一 slot),rebalance 无法解决。这种情况需要检查是否缺少 hash tag 设计,或考虑在应用层做 key 拆分。

故障转移机制(Failover)

[!QUESTION] 思考 假设一个 Master 宕机了,谁来决定哪个 Replica 接任?如果让客户端来判断,会有什么问题?

故障检测三阶段

[!NOTE] Raft 式投票是什么? Raft 是一种分布式共识算法,核心思想之一是领导者选举:当 Leader 宕机时,Follower 发起选举,获得多数派投票的节点成为新 Leader。Redis Cluster 的 Failover 选举借鉴了这个机制——Replica 通过递增 epoch 并向所有 Master 请求投票,获得多数票后晋升为新 Master。但 Redis 只使用了 Raft 的选举部分,数据复制仍然走自身协议。详见 hhs/Redis/07-集群方案/Raft共识算法。

flowchart LR
    A["PFAIL 潜在失效<br/>单个节点判定"] -->|"超过 cluster-node-timeout"| B["FAIL 确认失效<br/>多数 Master 同意"]
    B -->|"Raft 式投票"| C["选举 Leader<br/>Replica 竞选"]
    C -->|"最高优先级获胜"| D["Failover<br/>接管 slots"]

    style A fill:#fff3e0,stroke:#ff9800
    style B fill:#fce4ec,stroke:#e91e63
    style C fill:#e8eaf6,stroke:#3f51b5
    style D fill:#e8f5e9,stroke:#4caf50

详细流程:

阶段 谁触发 条件 持续时间
PFAIL 单个节点 超过 cluster-node-timeout 未收到 PONG 瞬时
FAIL PFAIL 节点广播 大多数 Master 同意该节点不可达 秒级
选举 宕机 Master 下唯一存活 Replica 发起集群纪元递增 + Raft 投票 秒级
Failover 当选 Leader 提升为 Master,接管原 Master 的 slots 亚秒级

选举规则

  1. 只有 Replica 能参与选举,Master 不会主动抢占
  2. 选举时机:Replica 发现自己复制的 Master 进入 FAIL 状态
  3. 竞争排序依据(依次比较):
    • replica-priority 值越小优先级越高(配置为 0 表示永远不参选)
    • 复制偏移量(replication offset)越大越优先——偏移量大意味着数据最新
    • 若偏移量相同,node ID 字典序小的优先(确定性 tiebreaker,避免同时发起投票)
  4. Raft 式投票:每个 Master 一票,先到先得
  5. 获胜条件:获得多数 Master 的选票
  6. 升级步骤:
    • 将自己提升为 Master
    • 接管所有原 Master 管理的 slots
    • 向所有节点广播新的配置(configEpoch 递增)

[!NOTE] 脑裂保护 脑裂是指网络分区把集群割裂成多个子集群,各自选举出 Master 同时对外服务。Redis 通过 epoch 版本号 解决冲突——epoch 更小的 Master 被强制降级。此外,建议配置 min-replicas-to-write 1,当 Master 联系不到足够 Replica 时主动拒绝写入,防止分区恢复后数据丢失。详见 hhs/Redis/07-集群方案/脑裂问题。

旧 Master 恢复后的行为

[!QUESTION] 思考 Master A 宕机,Replica A' 接管。几分钟后 A 重新上线,会发生什么?

旧 Master 恢复后不会抢回 Master 身份,而是自动降级为 Replica,同步新 Master 的数据。流程如下:

  1. 旧 Master 重新加入集群,通过 Gossip 收到最新配置(发现自己的 slots 已被别人接管,且 configEpoch 更大)
  2. 旧 Master 主动将自己降级为 Replica,指向新 Master
  3. 发起全量同步(PSYNC FULL),用新 Master 的数据覆盖自己落后或分歧的数据
  4. 同步完成后,作为 Replica 继续提供读服务(如果客户端配置了读副本)

[!WARNING] 旧 Master 有写入风险 如果旧 Master 在宕机期间被客户端写入了数据(未同步到 Replica),降级同步后这些数据会被覆盖丢失。这就是为什么 min-replicas-to-write 很重要——它能在分区时提前阻止孤立 Master 的写入。

客户端容错处理

集群模式下客户端会收到两种重定向响应:

// MOVED: 永久重定向(槽位已迁移到新节点)
// 客户端应更新拓扑,后续请求直接发到目标节点
MOVED 3999 192.168.1.20:7002

// ASK: 临时重定向(正在迁移中)
// 单次请求发送到目标节点,后续仍发原节点
ASK 3999 192.168.1.20:7002

[!TIP] 重试策略要点

  • MOVED 后更新本地路由表,不再重试旧节点
  • ASK 只重试这一次,之后恢复原来的路由
  • 如果连续收到 MOVED/ASK,说明集群正在剧烈变更,适当退避

软客户端 vs 硬客户端

[!QUESTION] 思考 客户端拿到第一个 MOVED 响应后才去查询新拓扑,还是从一开始就维护完整的集群地图?哪种方式在扩容时更稳定?

两种模式对比

特性 软客户端(Lazy Mode) 硬客户端(Eager Mode)
初始化行为 连接任一节点获取初始拓扑 预取所有节点完整拓扑
拓扑更新 遇到 MOVED/ASK 时按需刷新 后台定时拉取,提前感知变化
扩容时体验 迁移过渡期频繁 MOVED,请求偶发延迟 平滑无感,提前知道新 slot 在哪
内存开销 仅存储已知节点 存储全部节点 + slot→node 映射表
代表库 无(纯 lazy 已少见) go-redis, jedis, lettuce

[!NOTE] 关于 lettuce lettuce 早期版本默认 lazy,但现代版本(6.x+)已支持 ClusterTopologyRefreshOptions 主动定时刷新拓扑,本质上已经是"硬客户端"模式。这里分类仅为说明两种策略的思想差异,实际选型建议直接看各库的官方文档。

// go-redis 示例:硬客户端,自动维护拓扑
rdb := redis.NewClusterClient(&redis.ClusterOptions{
    Addrs:      []string{":7001", ":7002", ":7003"},
    PoolSize:   20,                  // 每个节点独立连接池
    MinIdleConns: 5,                 // 保持最小空闲连接
    ReadTimeout:  3 * time.Second,   // 读超时
    WriteTimeout: 3 * time.Second,   // 写超时
    IdleTimeout:  5 * time.Minute,   // 连接空闲回收
})

// 连接池隔离:每个节点有独立的连接池,互不影响
// GET 请求 go-redis 会自动判断 key 所在的 slot 并路由到正确节点
val, err := rdb.Get(ctx, "user:100:name").Result()
// lettuce 示例:默认按需刷新,可配置主动定时刷新(Java/Spring)
RedisClusterClient client = RedisClusterClient.create("redis://node1:7001");
// 可选:开启定时拓扑刷新(推荐生产环境启用)
ClusterTopologyRefreshOptions refreshOptions = ClusterTopologyRefreshOptions.builder()
    .enablePeriodicRefresh(Duration.ofSeconds(30))
    .enableAllAdaptiveRefreshTriggers()
    .build();
client.setOptions(ClusterClientOptions.builder()
    .topologyRefreshOptions(refreshOptions).build());
StatefulRedisClusterConnection<String, String> conn = client.connect();
String val = conn.sync().get("key");

[!TIP] 推荐配置 生产环境建议采用硬客户端 + 合理连接池大小。每个节点独立连接池可以避免热点 key 占满单个节点的连接资源。

生产部署关键注意点

网络与安全

项目 建议值/做法 原因
防火墙 开放 port 和 port+10000 集群总线通信依赖双端口
TLS Redis 7.0+ 支持 TLS(需编译开启) 加密集群总线通信,防止拓扑泄露
ACL Redis 6+ 启用 ACL 认证 替代传统 AUTH,更细粒度权限控制
bind 绑定内网 IP,不暴露公网 集群总线不鉴权,外网可控制整个集群

参数调优

参数 推荐值 说明
cluster-node-timeout 15000 ms 不要太短,避免网络抖动误判;不要长于业务容忍度
maxmemory-policy 缓存场景 allkeys-lru;持久化场景 noeviction 缓存类应用优先 LRU 避免 OOM;需要严格保证写入成功的场景用 noeviction,由业务层处理满错误
appendonly yes 集群推荐 AOF + 每秒刷盘,兼顾性能与安全
cluster-replica-validity-factor 10 乘以 timeout 作为复制链路容忍阈值,0 表示不拒绝主从链接
cluster-require-full-coverage yes(默认) yes 时任意 slot 无主节点则集群整体拒绝写入;生产环境若追求可用性可设 no,允许部分 slot 不可用时其余 slot 正常服务
cluster-allow-reads-when-down no 设为 yes 时集群即使处于 FAIL 状态也允许读请求(前提:该 key 所在 slot 有可达节点),适合读密集型业务容忍短暂降级

[!IMPORTANT] cluster-require-full-coverage 的取舍 当某个 Master 和它的所有 Replica 同时宕机时,该 Master 负责的 slot 就"裸奔"了。

  • yes(默认):集群整体拒绝读写,宁可停机也不允许数据不一致——适合对一致性要求极高的金融场景。
  • no:集群只拒绝访问故障 slot 的请求,其他 slot 继续服务——适合追求可用性的互联网业务,牺牲部分可用 slot 换取整体不宕机。

连接池配置

集群模式下,客户端为每个节点维护独立连接池(而非共享一个池)。合理配置连接池能避免热点节点耗尽连接、冷节点浪费资源。

参数 建议值 说明
PoolSize(go-redis)/ MaxTotal(jedis) 每节点 10~20 并发量大的业务可适当调高,但不宜超过 50
MinIdleConns 每节点 2~5 预留空闲连接,避免冷启动延迟
ReadTimeout 1~3s 根据业务 P99 延迟设置,别太宽松
WriteTimeout 1~3s 同上
IdleTimeout 3~5min 空闲连接回收时间,太短会频繁建连

[!TIP] 如何估算 PoolSize? 经验公式:PoolSize ≈ QPS × 平均耗时(s)。例如单节点 QPS 为 1000,平均耗时 2ms,则 PoolSize = 1000 × 0.002 = 2,实际上再乘以 3~5 的安全系数,设 10 左右即可。集群有 N 个 Master,每个 Master 的连接池独立,总连接数 = PoolSize × N。

架构最佳实践

flowchart TD
    App["你的应用服务"] --> SA["Service A<br/>:7001-7007"]
    App --> SB["Service B<br/>:7004-7007"]
    App --> SC["Service C<br/>:7010-7017"]
    App --> SD["Service D<br/>:7016-7023"]
    SA --> C1["Cluster 1<br/>独立集群"]
    SB --> C2["Cluster 2<br/>独立集群"]
    SC --> C3["Cluster 3<br/>独立集群"]
    SD --> C4["Cluster 4<br/>独立集群"]

    style App fill:#e3f2fd,stroke:#1565c0
    style C1,C2,C3,C4 fill:#e8f5e9,stroke:#4caf50

[!IMPORTANT] 原则

  1. 一服一群:不同业务线使用独立集群,避免互相干扰
  2. 奇数 Master:选举需要多数派,偶数 Master 不如少一个 Master 多一个 Slave
  3. 至少 3 Master + 3 Replica:单 Master 没有高可用意义
  4. 数据隔离:不要用 FLUSHALL,集群里会影响所有节点
  5. 大 key 慎用:集群下大 key 无法拆分,会导致单个节点内存/带宽瓶颈

大 Key 的检测与处理

[!WARNING] 为什么集群下大 Key 更危险? 单机 Redis 的大 Key 只影响自己;集群模式下,大 Key 不可拆分——它固定落在某个 slot、某台机器。这台机器的 CPU、内存、带宽瞬间成为瓶颈,相当于把集群退化成了单点。

检测:

# redis-cli 自带扫描(Redis 4.0+)
redis-cli --bigkeys              # 只统计每种类型中最大的 key
redis-cli --memkeys              # 按内存占用排序,更精确
redis-cli --bigkeys --i 0.01     # 控制扫描间隔(秒),避免线上打满带宽

# 输出示例
# [00.00%] Biggest string found so far 'user:100:feed' with 536870912 bytes
# [00.01%] Biggest hash   found so far 'order:cache'   with 876543 entries

处理策略:

类型 问题 拆分方案
大 String GET 一次传输全量数据 拆成多个子 key:item:1:chunk1、item:1:chunk2
大 Hash HGETALL 阻塞,cursor 遍历慢 按字段分桶:user:100:profile:v1、user:100:profile:v2
大 List LRANGE 一次返回全量 只保留最近 N 条,历史数据归档到 DB/ES
大 Set/ZSet SMEMBERS/ZREVRANGE 带宽打满 分段查询 + 游标,或业务层拆成多个小集合

[!TIP] 异步删除(Redis 4.0+) 对已有的大 Key,优先用 UNLINK(异步删除)替代 DEL(同步阻塞),防止删除操作拖垮主线程。删除前用 DEBUG OBJECT key 查看 serializedlength 估算影响范围。

关联笔记