Redis 主从、哨兵、Cluster:三套高可用方案怎么选
三套方案的丢失窗口其实是同一个 —— 异步复制的延迟。故障转移过程、脑裂防护、16384 个槽的由来,以及按规模选型的判断标准。
Redis 的高可用有三套方案:主从、哨兵、Cluster。很多人知道这三层递进关系,但不清楚「什么规模该用哪套」以及「切换时会丢多少数据」。
这篇文章把三套方案的原理、故障切换过程、数据丢失窗口和适用规模讲清楚,最后是一个从单机到集群的演进路径。
一、主从复制:基础但不够用
建立复制
# 从节点配置
replicaof 192.168.1.10 6379
masterauth your-password # 主节点有密码时必须配
replica-read-only yes
建立过程分三步:
- 全量同步:从节点发
PSYNC,主节点BGSAVE生成 RDB,传输给从节点 - 缓冲增量:全量同步期间的新写入暂存在 repl-backlog buffer
- 命令传播:之后主节点把每个写命令异步发给从节点
异步复制的含义:主节点写成功就返回给客户端,不等从节点确认。 这意味着主节点宕机时,没同步到从节点的数据就丢了。
数据丢失窗口有多大
# 缓冲区大小,默认 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提升某个从节点 - 客户端不知道:应用还连着旧主节点,写入全部失败
- 不知道谁是主:多个从节点,提升哪个?
这就是哨兵要解决的问题。
二、哨兵:自动故障转移
哨兵的三个职责
- 监控:每秒 ping 所有节点
- 通知:节点状态变化时通过 Pub/Sub 告警
- 自动切换:主节点故障时选一个从节点提升
主观下线与客观下线
哨兵 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
这是个经典问题。原因有三:
- 心跳包大小:每个节点在心跳里带一个 bitmap 表示自己负责哪些槽。16384 个槽 = 2 KB,65536 个槽 = 8 KB。每秒多次心跳,8 KB × 节点数 × 频率 的开销不可忽略。
- 集群规模上限:作者建议最大 1000 个节点。16384 槽 / 1000 节点 ≈ 16 槽/节点,还够用。
- 位图压缩: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 是一次架构变更,不是配置调整。 主要影响:
- 客户端要换成
JedisCluster/Lettuce的 cluster 模式 - 跨 key 操作要改造成 hash tag 或应用层拆分
- 数据库选号(
SELECT)不能用,只有 db0 - Lua 脚本涉及多 key 时受限
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 操作 | 自由 | 自由 | 受限 |
| 运维复杂度 | 低 | 中 | 高 |
| 适用 | 小规模/测试 | 单机内存够用 | 数据超单机 |
两个必须记住的事实:
- 所有方案的丢失窗口都是「异步复制的延迟」。要减少丢失就配
min-replicas-to-write,代价是可能拒绝写入。 evicted_keys > 0一定要告警。它意味着内存在被淘汰,缓存命中率下降,或者数据正在丢失。
选型的判断起点很简单:内存够不够? 够就用哨兵(最简单),不够就上 Cluster(但准备好改 key 设计)。
正在加载评论…