本文行文:三种高可用架构是什么、怎么选 → 主从复制的原理与一致性代价 → 主从切换的完整流程(含数据丢失风险)→ 基于哨兵的自动切换 → 集群模式与故障转移 → 选型与注意事项。
Redis 高可用架构
Redis 高可用要解决两件事:数据不丢太多(冗余)、服务不停太久(自动接管)。围绕这两件事有三级方案,复杂度与能力依次递增。
| 架构 | 数据冗余 | 自动故障转移 | 横向扩展(容量/写) | 客户端复杂度 | 适用场景 |
|---|---|---|---|---|---|
| 单机 | 无 | 无 | 无 | 低 | 开发测试、可短时中断的非核心缓存 |
| 主从(Replication) | 有(从节点副本) | 无(需人工 REPLICAOF) | 只能扩展读 | 低 | 读多写少、可容忍人工介入的分钟级恢复 |
| 哨兵(Sentinel) | 有 | 有(秒~分钟级) | 只能扩展读 | 中(需接入哨兵) | 大多数生产缓存/会话场景,数据量可控在一台主节点内 |
| 集群(Cluster) | 有(每个分片自带从节点) | 有 | 分片扩展容量与写能力 | 高(需支持 MOVED/ASK 的客户端) | 数据量大、写 QPS 高、需要水平扩容 |
三者的关系不是替代而是叠加:
- 哨兵建立在主从之上(没有主从就无从”切换”),它负责的是”谁来当主”这一决策;
- 集群内部同样有主从(每个分片是一主多从),但它额外解决了”数据放哪儿”,因此把”哨兵”这一角色换成了集群内部的 gossip 与选举机制;
- 无论用哪种,真正的最后一道防线都是持久化与备份(RDB/AOF)——复制只能防机器故障,防不了误删和逻辑写坏。
主从复制
Redis 的复制是异步复制:主节点(master)接收写命令,执行后把命令流(replication stream)推给从节点(replica)。从节点默认只读,用于分担读流量和提供故障接管的能力。
原理
全量/增量同步
从节点首次连接(或无法增量续传时)走全量同步(full resync),否则走部分同步(partial resync)。
| 阶段 | full resync | partial resync(psync) |
|---|---|---|
| 1 | 从节点发送 PSYNC ? -1 | 从节点发送 PSYNC <runid> <offset> |
| 2 | 主节点 BGSAVE 生成 RDB,同时把新写入缓存在复制缓冲区 | 主节点检查 runid 是否匹配、offset 是否还在复制积压缓冲区内 |
| 3 | 主节点发送 RDB 文件(期间从节点可配置继续对外提供旧数据服务) | 主节点把缺失的那段命令流直接补发 |
| 4 | 从节点清空并载入 RDB(此时阻塞,无法响应请求) | 从节点直接执行命令流,几乎无阻塞 |
| 5 | 主节点把缓冲区里堆积的写命令补发;此后进入命令传播常态 | 进入命令传播常态 |
命令传播
同步完成后,主从之间建立长连接:主节点每收到一个写命令,都会异步发送给从节点。并互相发送心跳包(Ping/Ack)确认对方活着。
时序图
sequenceDiagram
autonumber
participant R as 从节点 replica
participant M as 主节点 master
participant C as 客户端
Note over R,M: 握手:PSYNC 请求
R->>M: PSYNC runid offset(首次为 ? -1)
alt 全量同步(无法续传)
Note over C,R: 首次连接:全量同步
M->>M: BGSAVE fork 子进程生成 RDB
Note over M: 新写入同时缓存在 复制缓冲区
M-->>C: 正常响应读写
M->>R: +FULLRESYNC replid offset
M->>R: 传输 RDB 文件
R->>R: 清空旧数据并载入 RDB
M->>R: 补发缓冲区中堆积的写命令
else 部分同步(增量续传)
Note over C,R: 断线重连:增量同步
M->>R: +CONTINUE replid
M->>R: 只补发积压缓冲区中缺失的那段命令流
Note over R: 无需清库、几乎无阻塞
end
Note over C,R: 命令传播:主从进入常态
C->>M: 写命令
M-->>C: 本地执行并返回(不等副本,异步复制)
M->>R: 异步推送命令流,offset 持续递增
R->>R: 执行命令
R->>M: REPLCONF ACK offset(周期性上报进度)
Note over C,R: 客户端收到成功响应时,副本可能还没执行该命令关键点:
- 全量同步的代价:主节点
BGSAVE时 fork 子进程生成 RDB,会带来 CPU 与内存开销(写时复制导致内存可能翻倍),网络要传整个 RDB,从节点载入时阻塞。所以避免频繁全量同步是复制调优的核心。 master_replid+master_repl_offset:两者共同决定”从节点是否还能续传”。runid变了(例如主节点重启后换了 replid)就意味着只能全量。- 复制积压缓冲区(repl_backlog):一个固定大小的环形缓冲,保存最近一段命令流。从节点短暂断线重连时,只要缺失的 offset 还在缓冲区里,就能增量续传。
# 缓冲区大小经验值:按"从节点最长断线时间 × 主节点平均写入速率"估算repl-backlog-size 256mbrepl-backlog-ttl 3600 # 最后一个从节点断开多久后释放缓冲区(0=不释放)配置方法
# 方式一:配置文件(推荐,重启后仍生效)replicaof 10.0.0.11 6379 # 5.0 之前叫 slaveofmasterauth StrongPassw0rd! # 主节点 requirepass 时的认证密码replica-read-only yes # 从节点只读replica-priority 100 # 哨兵选主时的优先级,越小越优先
# 方式二:运行时命令(临时生效,重启丢失)redis-cli> REPLICAOF 10.0.0.11 6379redis-cli> REPLICAOF NO ONE # 断开复制,自己升为主节点(手工切换用这条)# 查看复制状态redis-cli> INFO replication# role:master# connected_slaves:2# slave0:ip=10.0.0.12,port=6379,state=online,offset=1234567,lag=0# master_replid:8a1f...:0# master_repl_offset:1234567# repl_backlog_active:1# repl_backlog_size:1048576# repl_backlog_first_byte_offset:185688# repl_backlog_histlen:1048576注意事项
复制风暴与级联复制
- 复制风暴:主节点挂多个从节点,每个从节点重连都各触发一次
BGSAVE。Redis 会尽量复用同一次 RDB(repl-diskless-sync-delay),但仍要避免”几十个从节点挂同一个主”。 - 级联复制:让部分从节点从”从节点”再复制(
replicaof指向上级从节点),把复制流量分流。代价是延迟累加、链路更长。 - 无盘复制:
repl-diskless-sync yes直接把 RDB 通过网络发给从节点,省去落盘(适合磁盘慢、网络快的环境)。
repl-diskless-sync yesrepl-diskless-sync-delay 5 # 等待窗口,多个从节点可复用同一次传输repl-diskless-load on-empty-db # 从节点无盘加载从节点只读的意义
replica-read-only yes # 默认值,强烈建议保持从节点可写会让”主从数据不一致”变得不可控(切主后这些写入可能被覆盖或产生冲突),除非你明确在做分片式客户端方案,否则一律保持只读。
一致性代价:异步复制
| 问题 | 原因 | 表现 |
|---|---|---|
| 写后读不一致 | 复制是异步的,从节点可能落后 | 写完立刻读到旧值 |
| 故障丢数据 | 主节点返回成功但命令还没到达从节点就宕机 | 已确认的写入丢失 |
| 回滚式覆盖 | 主节点事后恢复并重新被复制,或旧主带着”孤儿写”被重新纳入 | 新主的数据被旧数据覆盖 |
缓解手段:
# 客户端侧:写入后要求至少 N 个副本确认(同步等待,有超时)redis-cli> SET k vredis-cli> WAIT 1 100 # 等 1 个副本确认,最多等 100ms;返回实际确认数
# 主节点侧:要求最小副本数与最大延迟,否则拒绝写入(可用性换一致性)min-replicas-to-write 1min-replicas-max-lag 10主从切换
手工切换
没有哨兵时,切换全靠人:
# 1. 确认主节点确实不可用,并选出"数据最新"的从节点(比较 master_repl_offset)redis-cli -h 10.0.0.12 INFO replication | grep master_repl_offset
# 2. 把选定从节点提升为主redis-cli -h 10.0.0.12 REPLICAOF NO ONE
# 3. 让其他从节点指向新主redis-cli -h 10.0.0.13 REPLICAOF 10.0.0.12 6379
# 4. 切换客户端连接的地址(VIP 漂移 / 改配置 / 改 DNS)人工切换的问题是 RTO 长且易错:告警到人工介入通常已是分钟级,而第 4 步”所有引用旧主地址的地方”(应用配置、监控、定时任务、其他中间件)漏改一处就是持续故障。
自动切换
故障判定
手工切换的故障判定依赖人工检查,在自动切换中则通过哨兵实现:持续 PING Redis 节点确认主/从节点存活。
我们在 MySQL 高可用详解(2)故障转移 中讲过:故障检测的核心原则在于避免误判。那么假设当前哨兵未收到节点响应,如何判断是网络抖动还是节点真的挂了?与 MySQL 的故障判定不同,Redis 哨兵模式引入了 quorum(法定人数/多数派)机制:
- 主观下线:哨兵未收到响应,仅该哨兵认为节点下线;
- 下线共识:该哨兵向其他哨兵确认,该节点真的下线了吗?
- 客观下线:认为下线的哨兵数 ≥
quorum,即获得多数派授权,多数哨兵认为该节点下线。
其中:
总节点数的一半向下取整后加1,即大于总节点数一半的最小整数。详细解读可参考 Raft 共识 - Redis Sentinel / K8s etcd 共识机制详解。
切换流程
切换时的数据丢失
异步复制的切换必然面临一个问题:新主不一定拥有旧主已确认的全部写入。
旧主 master_repl_offset = 1000从节点 A offset = 995,从节点 B offset = 990旧主宕机 → A 被提升为新主 → B 上原本属于旧主 996~1000 的数据(如果 B 曾经有过)会被清掉具体行为:
- 新主提升后,其他从节点会与它做同步,落后于新主的从节点数据会被覆盖(这是期望行为);
- 但旧主上未同步出去的写入永久丢失;
- 更危险的是旧主”假死复活”:它以为自己还是主,继续接受写入;等它被重新纳为从节点时,这段”孤儿写”会被全量同步清空——数据静默丢失。
防御手段:
| 手段 | 说明 |
|---|---|
min-replicas-to-write + min-replicas-max-lag | 副本不可达/延迟过大时主节点拒绝写入,把”孤儿写”的可能性压到最低 |
WAIT N timeout | 关键写入要求副本确认 |
合理的 down-after-milliseconds | 太短会把”网络抖动/主节点慢查询”误判为故障,触发无谓切换 |
| 切换后清理旧主 | 旧主恢复后必须先 REPLICAOF 指向新主,确认全量同步完成再纳入服务 |
| 客户端写重试与幂等 | 切换瞬间的失败请求需要重试,且重试要幂等 |
哨兵模式
Sentinel(哨兵)是 Redis 官方的独立进程,本身是不存数据的特殊 Redis 实例,职责有四:
- 监控(Monitoring):持续 PING 主节点、从节点、其他哨兵;
- 通知(Notification):通过 Pub/Sub 事件把故障与切换结果推给客户端与运维系统;
- 自动故障转移(Automatic failover):主节点不可用时提升从节点、重配其他从节点、通知客户端;
- 配置提供者(Configuration provider):客户端启动时向哨兵询问”当前主节点是谁”,切换后由哨兵通知客户端新地址。
部署拓扑与配置
一个生产级拓扑:3 个哨兵 + 1 主 2 从,分布在三台机器(或三个可用区)上,哨兵不与 Redis 同机同故障域。
architecture-beta
group sentinel(cloud)[Sentinel]
group data(cloud)[Data]
group client(cloud)[Client]
service s1(server)[Sentinel1] in sentinel
service s2(server)[Sentinel2] in sentinel
service s3(server)[Sentinel3] in sentinel
service m(database)[Master] in data
service r1(database)[Replica1] in data
service r2(database)[Replica2] in data
service app(server)[App] in client
s1:R <--> L:s2
s2:R <--> L:s3
m:R <--> L:r1
r1:R <--> L:r2
s1:B --> T:m
s2:B --> T:m
s2:B --> T:r1
s2:B --> T:r2
s3:B --> T:r2
app:T <--> L:s1
app:R --> L:m读图要点:
- 控制面与数据面分离:哨兵自己不转发任何请求,它只负责”告知谁是主”和”在故障时改拓扑”。客户端的读写仍然直连 Redis 节点。
- 客户端必须走哨兵发现:否则切换后客户端会一直连着一个已经变成从节点的旧主(写命令报
READONLY)。 - 三个哨兵互相监控并投票,因此它们之间必须两两网络可达;少一个哨兵仍能工作(2 ≥ majority 2),少两个就永远切不了。
- 每个哨兵都要 PING 所有数据节点,因此哨兵到 Redis 节点的网络与认证(
sentinel auth-pass)必须配齐。(由于mermaid绘图布局原因,图中未全部连接)
# sentinel.conf(三个哨兵节点内容基本一致,仅端口/自身地址不同)port 26379daemonize yeslogfile "/var/log/redis/sentinel.log"
# 监控名为 mymaster 的主节点;quorum=2 表示至少 2 个哨兵认为下线才判 ODOWNsentinel monitor mymaster 10.0.0.11 6379 2
# 主节点/从节点密码(Redis 6+ 支持分别配置 sentinel auth-user)sentinel auth-pass mymaster StrongPassw0rd!
# 30s 内无有效响应 → 主观下线(需 > 业务最大抖动与慢查询耗时)sentinel down-after-milliseconds mymaster 30000
# 故障转移时,最多允许多少个从节点同时向新主发起同步(越小切换越慢但压力越低)sentinel parallel-syncs mymaster 1
# 故障转移超时(超过则重新选举 leader 再试)sentinel failover-timeout mymaster 180000
# 安全加固:禁止通过 SENTINEL SET 在运行时改写通知脚本(不接受主节点名参数)sentinel deny-scripts-reconfig yes# 启动与观察redis-sentinel /etc/redis/sentinel.confredis-cli -p 26379 SENTINEL masters # 主节点状态:flags、quorum、num-slavesredis-cli -p 26379 SENTINEL replicas mymasterredis-cli -p 26379 SENTINEL sentinels mymasterredis-cli -p 26379 SENTINEL get-master-addr-by-name mymasterredis-cli -p 26379 SENTINEL failover mymaster # 手工触发一次切换(演练用!)完整的故障转移过程
- 判定主观下线:某哨兵在
down-after-milliseconds(30s)内没收到主节点的有效 PING 回复,把flags标为s_down。 - 判定客观下线:该哨兵向其他哨兵询问对同一主节点的判断;当认为下线的哨兵数达到
quorum(2),主节点被标为o_down。 - 选举 leader 哨兵:所有哨兵基于
current_epoch发起投票(Raft 式),每个哨兵在一个纪元内只投一票,得票超过多数派(多数 = 哨兵总数/2 + 1)者成为 leader。没有 leader 就等下一轮。 - 选择新主节点:leader 按以下顺序筛选——
- 排除已下线、断线、
replica-priority为 0 的从节点; - 选
replica-priority最小的(优先级越高越小); - 优先级相同时选复制偏移量最大的(数据最新);
- 仍相同时选
runid字典序最小的(保证确定性)。
- 排除已下线、断线、
- 提升新主并重配拓扑:对选中的从节点发送
REPLICAOF NO ONE;对其他从节点发送REPLICAOF <新主>;parallel-syncs控制并发同步数。 - 更新自身配置与纪元:哨兵把自己的配置(以及
current_epoch、configEpoch)改写进sentinel.conf(持久化,重启不丢失),并通过+switch-master事件通知所有客户端。 - 持续监控旧主:旧主恢复后,哨兵会把它降级为从节点(
REPLICAOF新主),确认同步完成后再让它参与读流量。
# 订阅哨兵事件,观察整个过程redis-cli -p 26379 PSUBSCRIBE '+sdown' '+odown' '+switch-master'# 完整事件流可先 PSUBSCRIBE '*' 观察,常见事件:# +sdown / +odown / +try-failover / +vote-for-leader / +elected-leader# +failover-state-select-slave / +promoted-slave / +switch-master客户端如何接入
| 接入方式 | 做法 | 说明 |
|---|---|---|
| 哨兵感知型客户端 | 如 Lettuce、Jedis(Sentinel 模式)、Redisson | 客户端把哨兵地址列表作为”发现入口”,订阅 +switch-master 自动更新连接池 |
| 代理层 | HAProxy + 健康检查脚本(查询哨兵或 INFO replication 的 role) | 客户端无感,但代理自身要能识别”哪个节点是主”,且代理会引入一跳网络与新的故障点 |
| VIP 漂移 | keepalived + 哨兵通知脚本 | 不动客户端,但引入 VIP 单点与漂移窗口 |
脑裂与多数派
哨兵自身也会遇到网络分区。规则:
- 少数派分区内的哨兵无法凑够多数派票数,因此无法执行故障转移;
- 多数派分区会推进纪元(
current_epoch)并完成切换; - 分区恢复后,落后者按”纪元号大者为准”(
configEpoch大者胜)收敛,不会产生两个主节点同时有效。
因此部署要求:
| 要求 | 原因 |
|---|---|
| 哨兵数量 ≥ 3 且为奇数 | 需要多数派投票;2 个哨兵的多数派也是 2,挂一个就永远无法切换 |
| 哨兵分散在不同物理机/可用区 | 与 Redis 节点同机部署时,机器宕机等于”数据面 + 控制面”同时消失 |
quorum 取”半数以上” | quorum 太低会误判,太高则迟迟无法判定下线 |
| 大多数哨兵网络互通 | 否则分区两侧都凑不齐多数派,谁都切不了 |
常见坑
| 坑 | 现象 | 处理 |
|---|---|---|
| 哨兵配置未持久化 | 重启后丢失切换结果,重新指回旧主 | 用 SENTINEL SET 改配置(会落盘),别手改内存状态;配置文件目录需可写 |
| 主节点有密码、哨兵没配 | 哨兵监控正常但切换后连不上 | sentinel auth-pass(6.0+ 还有 sentinel auth-user) |
| 主节点内存打满 → 触发切换 | maxmemory + 淘汰策略引发长时间阻塞,哨兵误判 | 合理设置 maxmemory、避免 allkeys-lru 下的写入风暴 |
从节点过多、parallel-syncs 过大 | 切换后多从节点同时全量同步,新主被打挂 | parallel-syncs 1 并适当提高 repl-backlog-size 避免全量 |
down-after-milliseconds 太短 | 网络抖动/慢查询导致频繁切换 | 结合 INFO 中的 latency 与业务超时设置(通常 10~30s) |
| 切换后仍连旧主 | 写失败但监控看起来正常 | 客户端必须走哨兵发现或代理;切换后校验 INFO replication 的 role |
| 忘记演练 | 真故障时脚本/流程失效 | 用 SENTINEL failover <master> 定期演练,记录实测 RTO |
集群模式
Redis Cluster 是官方的分片 + 高可用方案:数据按槽位分散到多个主节点,每个主节点可带从节点,集群自己完成故障检测与故障转移。具体查看第二篇 Redis 高可用详解(2)集群模式 。
区别
一句话概括三者的能力边界:
| 维度 | 主从 | 哨兵 | 集群 |
|---|---|---|---|
| 数据分片 | 无(每个节点都是全量) | 无(同主从) | 有(16384 槽分散) |
| 容量上限 | 单机内存 | 单机内存 | 所有主节点内存之和 |
| 写能力 | 单主 | 单主 | 多主并行(按槽分散) |
| 故障检测 | 无 | 哨兵独立进程,down-after-milliseconds | 节点间 gossip,cluster-node-timeout |
| 仲裁机制 | 无 | 哨兵多数派(Raft 式选举 leader) | 持槽主节点多数派投票 |
| 切换执行者 | 人工 | leader 哨兵 | 从节点自荐 + 主节点投票 |
| 客户端要求 | 知道主从地址 | 支持 Sentinel(发现 + 订阅通知) | 支持 Cluster(槽映射 + MOVED/ASK) |
| 多键/事务 | 无限制 | 无限制 | 同槽限制(需 hash tag) |
| 脑裂风险 | 无(无自动切换) | 少数派无法切换,靠纪元收敛 | 少数派无法选主,靠 configEpoch 收敛 |
| 附加成本 | 低 | 需部署 ≥3 个哨兵 | 运维复杂度最高(端口、迁移、槽健康) |
| 典型 RTO | 分钟级(人工) | 秒~分钟级 | 秒~分钟级 |
选型决策树:
- 数据量小、可接受人工恢复 → 主从(甚至单机 + 定期备份)。
- 数据量能装进单机、要自动切换 → 主从 + 哨兵(最主流)。
- 数据量超单机、或写 QPS 超过单主 → Cluster(并接受同槽限制)。
- 需要跨地域容灾 → 在上述任一方案之上叠加异地副本(异步,接受 RPO),或依赖云厂商的多可用区部署。
小结
- 主从提供了数据冗余与读扩展,但切换要靠人;哨兵在主从之上加了故障检测、选举与自动切换,解决了”谁来当主”;集群在此基础上加了槽位分片,解决了”数据放哪儿、写不够怎么办”。
- 三者共有的关键事实:Redis 复制是异步的。写后读不一致、切换丢数据、旧主”孤儿写”被覆盖,这三类一致性问题无法靠架构选择消除,只能用
WAIT、min-replicas-to-write、合理的超时参数和客户端重试幂等来收敛。 - 自动切换的正确性依赖多数派:哨兵要 ≥3 个且分散部署,集群要保证每个主节点都有从节点(否则
cluster-require-full-coverage会让整个集群停服);网络分区时少数派一律不许切换,这是防脑裂的根本。 - 高可用的最后一道防线仍是持久化与备份:复制防不住误删、
FLUSHALL和逻辑写坏。RDB/AOF 的取舍、备份与恢复演练是另一条独立主线(详见 Redis 持久化专题)。 - 无论选哪种方案,没演练过的切换等于没有高可用:用
SENTINEL failover或CLUSTER FAILOVER定期演练,记录实测 RTO,并把”切换后校验role、延迟、数据量”写进流程。
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 40 天
部分内容可能已过时