5483 字
27 分钟
Redis 高可用详解(1)

本文行文:三种高可用架构是什么、怎么选 → 主从复制的原理与一致性代价 → 主从切换的完整流程(含数据丢失风险)→ 基于哨兵的自动切换 → 集群模式与故障转移 → 选型与注意事项。

Redis 高可用架构#

Redis 高可用要解决两件事:数据不丢太多(冗余)、服务不停太久(自动接管)。围绕这两件事有三级方案,复杂度与能力依次递增。

架构数据冗余自动故障转移横向扩展(容量/写)客户端复杂度适用场景
单机无无无低开发测试、可短时中断的非核心缓存
主从(Replication)有(从节点副本)无(需人工 REPLICAOF)只能扩展读低读多写少、可容忍人工介入的分钟级恢复
哨兵(Sentinel)有有(秒~分钟级)只能扩展读中(需接入哨兵)大多数生产缓存/会话场景,数据量可控在一台主节点内
集群(Cluster)有(每个分片自带从节点)有分片扩展容量与写能力高(需支持 MOVED/ASK 的客户端)数据量大、写 QPS 高、需要水平扩容

三者的关系不是替代而是叠加:

  • 哨兵建立在主从之上(没有主从就无从”切换”),它负责的是”谁来当主”这一决策;
  • 集群内部同样有主从(每个分片是一主多从),但它额外解决了”数据放哪儿”,因此把”哨兵”这一角色换成了集群内部的 gossip 与选举机制;
  • 无论用哪种,真正的最后一道防线都是持久化与备份(RDB/AOF)——复制只能防机器故障,防不了误删和逻辑写坏。

主从复制#

Redis 的复制是异步复制:主节点(master)接收写命令,执行后把命令流(replication stream)推给从节点(replica)。从节点默认只读,用于分担读流量和提供故障接管的能力。

原理#

全量/增量同步#

从节点首次连接(或无法增量续传时)走全量同步(full resync),否则走部分同步(partial resync)。

阶段full resyncpartial 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: 客户端收到成功响应时,副本可能还没执行该命令

关键点:

  1. 全量同步的代价:主节点 BGSAVE 时 fork 子进程生成 RDB,会带来 CPU 与内存开销(写时复制导致内存可能翻倍),网络要传整个 RDB,从节点载入时阻塞。所以避免频繁全量同步是复制调优的核心。
  2. master_replid + master_repl_offset:两者共同决定”从节点是否还能续传”。runid 变了(例如主节点重启后换了 replid)就意味着只能全量。
  3. 复制积压缓冲区(repl_backlog):一个固定大小的环形缓冲,保存最近一段命令流。从节点短暂断线重连时,只要缺失的 offset 还在缓冲区里,就能增量续传。
Terminal window
# 缓冲区大小经验值:按"从节点最长断线时间 × 主节点平均写入速率"估算
repl-backlog-size 256mb
repl-backlog-ttl 3600 # 最后一个从节点断开多久后释放缓冲区(0=不释放)

配置方法#

Terminal window
# 方式一:配置文件(推荐,重启后仍生效)
replicaof 10.0.0.11 6379 # 5.0 之前叫 slaveof
masterauth StrongPassw0rd! # 主节点 requirepass 时的认证密码
replica-read-only yes # 从节点只读
replica-priority 100 # 哨兵选主时的优先级,越小越优先
# 方式二:运行时命令(临时生效,重启丢失)
redis-cli> REPLICAOF 10.0.0.11 6379
redis-cli> REPLICAOF NO ONE # 断开复制,自己升为主节点(手工切换用这条)
Terminal window
# 查看复制状态
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 通过网络发给从节点,省去落盘(适合磁盘慢、网络快的环境)。
Terminal window
repl-diskless-sync yes
repl-diskless-sync-delay 5 # 等待窗口,多个从节点可复用同一次传输
repl-diskless-load on-empty-db # 从节点无盘加载

从节点只读的意义#

Terminal window
replica-read-only yes # 默认值,强烈建议保持

从节点可写会让”主从数据不一致”变得不可控(切主后这些写入可能被覆盖或产生冲突),除非你明确在做分片式客户端方案,否则一律保持只读。

一致性代价:异步复制#

问题原因表现
写后读不一致复制是异步的,从节点可能落后写完立刻读到旧值
故障丢数据主节点返回成功但命令还没到达从节点就宕机已确认的写入丢失
回滚式覆盖主节点事后恢复并重新被复制,或旧主带着”孤儿写”被重新纳入新主的数据被旧数据覆盖

缓解手段:

Terminal window
# 客户端侧:写入后要求至少 N 个副本确认(同步等待,有超时)
redis-cli> SET k v
redis-cli> WAIT 1 100 # 等 1 个副本确认,最多等 100ms;返回实际确认数
# 主节点侧:要求最小副本数与最大延迟,否则拒绝写入(可用性换一致性)
min-replicas-to-write 1
min-replicas-max-lag 10

主从切换#

手工切换#

没有哨兵时,切换全靠人:

Terminal window
# 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,即获得多数派授权,多数哨兵认为该节点下线。

其中:

quorum=⌊n/2⌋+1\mathrm {quorum} = \lfloor n/2 \rfloor +1

总节点数的一半向下取整后加1,即大于总节点数一半的最小整数。详细解读可参考 Raft 共识 - Redis Sentinel / K8s etcd 共识机制详解。

切换流程#

切换时的数据丢失#

异步复制的切换必然面临一个问题:新主不一定拥有旧主已确认的全部写入。

旧主 master_repl_offset = 1000
从节点 A offset = 995,从节点 B offset = 990
旧主宕机 → A 被提升为新主 → B 上原本属于旧主 996~1000 的数据(如果 B 曾经有过)会被清掉

具体行为:

  1. 新主提升后,其他从节点会与它做同步,落后于新主的从节点数据会被覆盖(这是期望行为);
  2. 但旧主上未同步出去的写入永久丢失;
  3. 更危险的是旧主”假死复活”:它以为自己还是主,继续接受写入;等它被重新纳为从节点时,这段”孤儿写”会被全量同步清空——数据静默丢失。

防御手段:

手段说明
min-replicas-to-write + min-replicas-max-lag副本不可达/延迟过大时主节点拒绝写入,把”孤儿写”的可能性压到最低
WAIT N timeout关键写入要求副本确认
合理的 down-after-milliseconds太短会把”网络抖动/主节点慢查询”误判为故障,触发无谓切换
切换后清理旧主旧主恢复后必须先 REPLICAOF 指向新主,确认全量同步完成再纳入服务
客户端写重试与幂等切换瞬间的失败请求需要重试,且重试要幂等

哨兵模式#

Sentinel(哨兵)是 Redis 官方的独立进程,本身是不存数据的特殊 Redis 实例,职责有四:

  1. 监控(Monitoring):持续 PING 主节点、从节点、其他哨兵;
  2. 通知(Notification):通过 Pub/Sub 事件把故障与切换结果推给客户端与运维系统;
  3. 自动故障转移(Automatic failover):主节点不可用时提升从节点、重配其他从节点、通知客户端;
  4. 配置提供者(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 26379
daemonize yes
logfile "/var/log/redis/sentinel.log"
# 监控名为 mymaster 的主节点;quorum=2 表示至少 2 个哨兵认为下线才判 ODOWN
sentinel 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
Terminal window
# 启动与观察
redis-sentinel /etc/redis/sentinel.conf
redis-cli -p 26379 SENTINEL masters # 主节点状态:flags、quorum、num-slaves
redis-cli -p 26379 SENTINEL replicas mymaster
redis-cli -p 26379 SENTINEL sentinels mymaster
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
redis-cli -p 26379 SENTINEL failover mymaster # 手工触发一次切换(演练用!)

完整的故障转移过程#

  1. 判定主观下线:某哨兵在 down-after-milliseconds(30s)内没收到主节点的有效 PING 回复,把 flags 标为 s_down。
  2. 判定客观下线:该哨兵向其他哨兵询问对同一主节点的判断;当认为下线的哨兵数达到 quorum(2),主节点被标为 o_down。
  3. 选举 leader 哨兵:所有哨兵基于 current_epoch 发起投票(Raft 式),每个哨兵在一个纪元内只投一票,得票超过多数派(多数 = 哨兵总数/2 + 1)者成为 leader。没有 leader 就等下一轮。
  4. 选择新主节点:leader 按以下顺序筛选——
    • 排除已下线、断线、replica-priority 为 0 的从节点;
    • 选 replica-priority 最小的(优先级越高越小);
    • 优先级相同时选复制偏移量最大的(数据最新);
    • 仍相同时选 runid 字典序最小的(保证确定性)。
  5. 提升新主并重配拓扑:对选中的从节点发送 REPLICAOF NO ONE;对其他从节点发送 REPLICAOF <新主>;parallel-syncs 控制并发同步数。
  6. 更新自身配置与纪元:哨兵把自己的配置(以及 current_epoch、configEpoch)改写进 sentinel.conf(持久化,重启不丢失),并通过 +switch-master 事件通知所有客户端。
  7. 持续监控旧主:旧主恢复后,哨兵会把它降级为从节点(REPLICAOF 新主),确认同步完成后再让它参与读流量。
Terminal window
# 订阅哨兵事件,观察整个过程
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分钟级(人工)秒~分钟级秒~分钟级

选型决策树:

  1. 数据量小、可接受人工恢复 → 主从(甚至单机 + 定期备份)。
  2. 数据量能装进单机、要自动切换 → 主从 + 哨兵(最主流)。
  3. 数据量超单机、或写 QPS 超过单主 → Cluster(并接受同槽限制)。
  4. 需要跨地域容灾 → 在上述任一方案之上叠加异地副本(异步,接受 RPO),或依赖云厂商的多可用区部署。

小结#

  • 主从提供了数据冗余与读扩展,但切换要靠人;哨兵在主从之上加了故障检测、选举与自动切换,解决了”谁来当主”;集群在此基础上加了槽位分片,解决了”数据放哪儿、写不够怎么办”。
  • 三者共有的关键事实:Redis 复制是异步的。写后读不一致、切换丢数据、旧主”孤儿写”被覆盖,这三类一致性问题无法靠架构选择消除,只能用 WAIT、min-replicas-to-write、合理的超时参数和客户端重试幂等来收敛。
  • 自动切换的正确性依赖多数派:哨兵要 ≥3 个且分散部署,集群要保证每个主节点都有从节点(否则 cluster-require-full-coverage 会让整个集群停服);网络分区时少数派一律不许切换,这是防脑裂的根本。
  • 高可用的最后一道防线仍是持久化与备份:复制防不住误删、FLUSHALL 和逻辑写坏。RDB/AOF 的取舍、备份与恢复演练是另一条独立主线(详见 Redis 持久化专题)。
  • 无论选哪种方案,没演练过的切换等于没有高可用:用 SENTINEL failover 或 CLUSTER FAILOVER 定期演练,记录实测 RTO,并把”切换后校验 role、延迟、数据量”写进流程。
Redis 高可用详解(1)
https://blog-l7wd3.pages.dev/posts/ha/redis-ha/
作者
L7WD3-Xiao
发布于
2026-09-16
许可协议
CC BY-NC-SA 4.0

分享文章

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

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取