Redis 主从、哨兵、Cluster:三套高可用方案怎么选

三套方案的丢失窗口其实是同一个 —— 异步复制的延迟。故障转移过程、脑裂防护、16384 个槽的由来,以及按规模选型的判断标准。

Redis 的高可用有三套方案:主从、哨兵、Cluster。很多人知道这三层递进关系,但不清楚「什么规模该用哪套」以及「切换时会丢多少数据」。

这篇文章把三套方案的原理、故障切换过程、数据丢失窗口和适用规模讲清楚,最后是一个从单机到集群的演进路径。

一、主从复制:基础但不够用

建立复制

# 从节点配置
replicaof 192.168.1.10 6379
masterauth your-password          # 主节点有密码时必须配
replica-read-only yes

建立过程分三步:

  1. 全量同步:从节点发 PSYNC,主节点 BGSAVE 生成 RDB,传输给从节点
  2. 缓冲增量:全量同步期间的新写入暂存在 repl-backlog buffer
  3. 命令传播:之后主节点把每个写命令异步发给从节点

异步复制的含义:主节点写成功就返回给客户端,不等从节点确认。 这意味着主节点宕机时,没同步到从节点的数据就丢了。

数据丢失窗口有多大

# 缓冲区大小,默认 1MB
repl-backlog-size 1mb

假设主节点写入 2 万 QPS、每条命令 100 字节,1 MB 缓冲区只能存 0.5 秒的写入。网络抖动超过 0.5 秒,从节点就要重新做全量同步(而不是增量)——这时候主节点的 BGSAVE 会 fork 子进程,内存翻倍,可能直接 OOM。

生产环境 repl-backlog-size 建议设 64~256 MB。

主从的问题

  • 切换要人工:主节点挂了,得手动 replicaof no one 提升某个从节点
  • 客户端不知道:应用还连着旧主节点,写入全部失败
  • 不知道谁是主:多个从节点,提升哪个?

这就是哨兵要解决的问题。

二、哨兵:自动故障转移

哨兵的三个职责

  1. 监控:每秒 ping 所有节点
  2. 通知:节点状态变化时通过 Pub/Sub 告警
  3. 自动切换:主节点故障时选一个从节点提升

主观下线与客观下线

哨兵 A 发现主节点 5 秒没响应  → 主观下线(sdown)
哨兵 A 询问其他哨兵:"你们也觉得它挂了吗?"
≥ quorum 个哨兵同意            → 客观下线(odown)→ 开始故障转移

配置:

sentinel monitor mymaster 192.168.1.10 6379 2     # 最后一个是 quorum
sentinel down-after-milliseconds mymaster 5000    # 5 秒无响应算主观下线
sentinel failover-timeout mymaster 180000         # 切换超时
sentinel parallel-syncs mymaster 1                # 同时向新主同步的从节点数

quorum 怎么定:3 个哨兵设 2,5 个哨兵设 3。规则是「多数派」,且哨兵数量应该是奇数。

哨兵本身必须部署 ≥ 3 个且在不同机器上。 只部署 2 个时,一个挂了就无法形成多数派,故障转移永远不会发生。

故障转移的完整过程

1. 多个哨兵确认主节点 odown
2. 哨兵间投票选出一个 Leader 哨兵(Raft 变种)
3. Leader 从从节点中选新主,选择依据:
   a. 优先级(replica-priority,越小越优先,0 表示永不提升)
   b. 复制偏移量最大(数据最新)
   c. runid 最小(随机兜底)
4. 向新主发 REPLICAOF NO ONE
5. 向其他从节点发 REPLICAOF 新主IP 端口
6. 更新哨兵自身配置(后续客户端通过哨兵查询新主地址)
7. 通知客户端(Pub/Sub 频道 +switch-master)

切换耗时:down-after-milliseconds(5s)+ 投票(1~2s)+ 从节点同步(几秒)≈ 10~30 秒。

这期间写入全部失败。应用必须能扛住这段时间——要么重试,要么降级。

客户端怎么连哨兵

// Lettuce
RedisClient client = RedisClient.create();
RedisSentinelClient sentinel = RedisSentinelClient.create(client,
    RedisURI.create("redis-sentinel://192.168.1.20:26379,192.168.1.21:26379,192.168.1.22:26379/0"));

注意配置多个哨兵地址。只配一个,那个哨兵挂了客户端就不知道新主在哪。

Spring Boot 3.x 的写法:

spring:
  data:
    redis:
      sentinel:
        master: mymaster
        nodes:
          - 192.168.1.20:26379
          - 192.168.1.21:26379
          - 192.168.1.22:26379

哨兵的坑

坑 1:客户端长连接不感知切换

哨兵切完后,老客户端可能还连着旧主。Lettuce / Jedis 有重连机制,但要确保:

// Lettuce 会自动订阅 switch-master 事件,无需额外配置
// Jedis 需要配置
JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels, poolConfig, timeout);
// pool.getResource() 每次都会查询当前主节点

坑 2:脑裂

主节点与哨兵网络隔离(但仍在服务客户端),哨兵认为它挂了,提升了一个从节点。此时有两个主节点同时接受写入——数据就冲突了。

Redis 用 min-replicas-to-write 缓解:

# 至少有 1 个从节点正常连接,才允许写入
min-replicas-to-write 1
min-replicas-max-lag 10      # 从节点延迟不超过 10 秒

不满足条件时主节点拒绝写入,宁可不可用也不写脏数据。

坑 3:重启的旧主仍以为自己是主

旧主恢复后,哨兵会把它降级为从节点。但在这个窗口内,如果它先接受了一批写入,这些数据会丢失(被新主的数据覆盖)。所以下线时间长的节点,重启后应该先清理数据再作为从节点加入。

三、Cluster:分片集群

哨兵解决了高可用,但没解决容量——所有数据仍在一个主节点上,内存受单机限制。

Cluster 用分片解决容量问题。

槽位分配

Redis Cluster 有 16384 个哈希槽,每个 key 通过 CRC16 映射到某个槽:

slot = CRC16(key) % 16384

每个主节点负责一段槽(比如 3 主节点各 5461 个槽)。

# 查看集群状态
redis-cli -c -p 7000 cluster info
redis-cli -c -p 7000 cluster nodes
redis-cli -c -p 7000 cluster slots

为什么是 16384 而不是 65536

这是个经典问题。原因有三:

  1. 心跳包大小:每个节点在心跳里带一个 bitmap 表示自己负责哪些槽。16384 个槽 = 2 KB,65536 个槽 = 8 KB。每秒多次心跳,8 KB × 节点数 × 频率 的开销不可忽略。
  2. 集群规模上限:作者建议最大 1000 个节点。16384 槽 / 1000 节点 ≈ 16 槽/节点,还够用。
  3. 位图压缩:16384 时 bitmap 的压缩率更好。

MOVED 重定向

客户端访问错误节点时,节点返回:

-MOVED 3999 127.0.0.1:6381

意思是「槽 3999 在 6381 节点上,你重新请求」。

智能客户端会缓存这个映射关系(JedisCluster / Lettuce 都支持),后续请求直接打对的节点。

ASK 重定向(迁移中):

-ASK 3999 127.0.0.1:6381

区别是 ASK 表示「这次去那边,但这个映射不要缓存」——因为槽还在迁移中。

跨槽操作的限制

这是 Cluster 最大的使用约束:涉及多个 key 的命令要求所有 key 在同一个槽。

# 报错:CROSSSLOT Keys in request don't hash to the same slot
MGET user:1:name user:2:name

# 解决:用 hash tag 强制同槽
MGET {user:1}:name {user:1}:age      # 花括号内的部分参与计算
# 事务也一样
MULTI
SET {user:1}:name "a"
SET {user:1}:age 20
EXEC          # 同槽可行

同样受限的:SUNION、SINTERSTORE、ZUNIONSTORE、Lua 脚本操作多个 key、KEYS(实际是逐节点扫,性能差)。

设计 key 时就要考虑这一点。 如果业务上必然要跨 key 操作(比如排行榜 + 用户信息),要么用 hash tag 把它们绑在一起(代价是热点集中在一个槽),要么在应用层拆分。

集群的故障转移

cluster-node-timeout 15000      # 15 秒判定节点失联
cluster-require-full-coverage no  # 部分槽不可用时是否拒绝全部请求

cluster-require-full-coverage yes(默认)时,只要有任何一个槽不可用,整个集群拒绝服务。生产环境建议设 no——牺牲一部分数据可用性,保住整体可用。

Cluster 切换比哨兵快吗?其实差不多,都是 10~30 秒量级。但 Cluster 是去中心的,靠 Gossip 协议选举,不需要额外的哨兵进程。

四、数据丢失窗口:三套方案对比

这是最该关心的部分。Redis 主从复制是异步的,任何方案在主节点突然宕机时都会丢数据。

方案 切换耗时 最坏丢失 丢失原因
主从(手动) 人工介入 从节点落后的全部数据 异步复制
哨兵 10~30s 从节点落后的全部数据 异步复制
Cluster 10~30s 从节点落后的全部数据 异步复制

关键结论:三套方案的数据丢失窗口是同一个——「从节点落后主节点的那部分」。

要减少丢失,靠的是这两个参数:

# 主节点至少有 N 个从节点正常,才接受写入
min-replicas-to-write 1
# 从节点延迟不超过 M 秒
min-replicas-max-lag 10

代价是:网络抖动时主节点拒绝写入,服务变不可用。

要真正不丢,只能上 Redis Enterprise 的 WAIT 命令(或客户端自己实现「写后等待复制确认」):

# 等待 1 个副本确认收到(最多等 100ms)
SET key value
WAIT 1 100

WAIT 会阻塞直到指定数量的副本确认,或者超时。代价是每次写入多一次往返,吞吐下降 30~50%。

要不要用 WAIT,取决于业务对丢数据的容忍度。缓存场景完全不需要;如果 Redis 被当主存储用(比如分布式锁、计数器),就得考虑。

五、怎么选:规模决定方案

数据量 方案 节点数 说明
< 10 GB 主从 + 哨兵 1 主 2 从 + 3 哨兵 最简单,切换自动
10~50 GB 哨兵 1 主 2 从 + 3 哨兵 单机内存够,不必分片
50~200 GB Cluster 3 主 3 从 开始需要分片
> 200 GB Cluster 6 主 6 从 每主 30~40 GB

判断标准不是数据量本身,而是「单机内存是否够用」。 Redis 实例建议不超过 32 GB(RDB fork 时内存翻倍,太大容易 OOM),单实例超过 20 GB 就该考虑分片。

从单机到集群的演进路径

单机
  ↓ 需要高可用
主从 + 哨兵(不改代码,改配置)
  ↓ 内存不够
Cluster(需要客户端支持,注意跨槽限制)
  ↓ 更大规模 / 多租户
多个 Cluster 或 Redis 云服务

注意:从哨兵迁到 Cluster 是一次架构变更,不是配置调整。 主要影响:

  1. 客户端要换成 JedisCluster / Lettuce 的 cluster 模式
  2. 跨 key 操作要改造成 hash tag 或应用层拆分
  3. 数据库选号(SELECT)不能用,只有 db0
  4. Lua 脚本涉及多 key 时受限
  5. KEYS、SCAN 要逐节点执行

六、运维要点

监控指标

redis-cli info replication

必看:

指标 含义 告警阈值
master_link_status 从节点连接状态 不是 up 就告警
master_last_io_seconds_ago 距上次与主节点通信 > 10 秒
slave_repl_offset vs master_repl_offset 复制延迟字节数 差值持续增长
connected_slaves 从节点数 少于预期
redis-cli info stats
指标 含义
rejected_connections 超过 maxclients 被拒的连接,说明连接数不够
evicted_keys 因内存不足被淘汰的 key,> 0 就是问题
keyspace_misses 缓存未命中数

evicted_keys > 0 特别值得警惕——它说明内存已满,正在按 LRU 淘汰数据。如果是缓存场景,这意味着命中率下降;如果是当存储用,这就是数据丢失。

# 设置内存上限和淘汰策略
maxmemory 4gb
maxmemory-policy allkeys-lru

慢查询

# 记录超过 10ms 的命令,最多 128 条
redis-cli config set slowlog-log-slower-than 10000
redis-cli config set slowlog-max-len 128
redis-cli slowlog get 10

Redis 是单线程的,一个慢命令会阻塞所有请求。常见的慢命令:

命令 风险
KEYS * 全库扫描,绝对禁止生产使用(用 SCAN)
HGETALL(大 hash) 返回几十万字段
LRANGE key 0 -1(大 list) 同上
SMEMBERS(大 set) 同上
FLUSHALL 清空全库
Lua 脚本死循环 无限阻塞

容量规划

# 看当前使用量
redis-cli info memory | grep used_memory_human
# 看每个 key 的实际大小(Redis 4+)
redis-cli --bigkeys
redis-cli memory usage <key>

规划公式:

所需内存 = 数据总量 × 1.5(碎片+开销)+ 副本缓冲

RDB fork 时需要额外 100% 的写时复制内存(如果写入频繁),所以单实例内存使用率不应超过 50%。

小结

三套方案的核心差异:

主从 哨兵 Cluster
高可用 手动 自动 自动
分片 无 无 有
跨 key 操作 自由 自由 受限
运维复杂度 低 中 高
适用 小规模/测试 单机内存够用 数据超单机

两个必须记住的事实:

  1. 所有方案的丢失窗口都是「异步复制的延迟」。要减少丢失就配 min-replicas-to-write,代价是可能拒绝写入。
  2. evicted_keys > 0 一定要告警。它意味着内存在被淘汰,缓存命中率下降,或者数据正在丢失。

选型的判断起点很简单:内存够不够? 够就用哨兵(最简单),不够就上 Cluster(但准备好改 key 设计)。

查看「中间件」全部文章 →

同分类文章

查看 0 评论 0

评论

正在加载评论…

留言提交后自动审核:正常内容立即显示,含敏感信息的会转人工复核。

Redis 主从、哨兵、Cluster:三套高可用方案怎么选 - 起航社区