4. Redis高可用架构

单节点 Redis 一旦宕机,缓存层整体不可用,所有请求会瞬间压到数据库。高可用架构要解决两个问题:数据有副本可切换,故障能被自动发现并完成主从角色调整。Redis 提供的主从复制负责数据同步,哨兵(Sentinel)负责监控、选主与通知,客户端负责在拓扑变化后重新连接并正确分流读写。

主从复制原理

主从复制分为全量同步与增量同步两个阶段。从节点首次连接主节点时发送 PSYNC ? -1,主节点执行 BGSAVE 生成 RDB 快照,同时把快照期间的新写命令缓存到复制缓冲区。快照发送完成后,主节点把缓冲区中的命令补发给从节点,从节点加载 RDB 并执行增量命令,进入在线状态。此后主节点每写入一条命令,都会异步传播给所有从节点。

断线重连时,从节点携带上次的 runid 与复制偏移量 offset 发起 PSYNC。若主节点判断偏移量仍在复制积压缓冲区(repl-backlog)范围内,则只补发缺失部分,即部分重同步;否则退化为全量同步。复制积压缓冲区大小由 repl-backlog-size 控制,默认 1MB,写流量大或断线时间长时应适当调大,例如 64MB。

# 从节点配置示例
replicaof 192.168.10.11 6379
masterauth Redis@2024
replica-read-only yes
repl-backlog-size 64mb
repl-timeout 60

复制是异步的,主节点写入成功不代表从节点已收到,因此主从切换存在数据丢失窗口。可通过 WAIT numreplicas timeout 在关键写入后等待指定数量的副本确认,但会牺牲部分可用性与延迟。

哨兵模式部署

哨兵是独立进程,不存储数据,职责是监控、通知、自动故障转移和充当配置提供者。生产环境至少部署 3 个哨兵且为奇数,分布在不同的故障域,避免单点误判。哨兵通过 INFO 与发布订阅 __sentinel__:hello 互相发现并交换主节点状态。

# sentinel.conf 关键配置
port 26379
sentinel monitor mymaster 192.168.10.11 6379 2
sentinel auth-pass mymaster Redis@2024
sentinel down-after-milliseconds mymaster 30000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1

其中 quorum=2 表示至少 2 个哨兵认为主节点主观下线,才判定为客观下线并触发选举。选举采用 Raft 类似机制,获得多数票的哨兵成为 Leader 执行故障转移。parallel-syncs 控制同时向新主发起同步的从节点数量,设为 1 可避免新主在切换瞬间被多个全量同步压垮。

故障转移流程

故障转移的完整链路是:哨兵对主节点心跳超时后标记主观下线;达到 quorum 后标记客观下线;Leader 哨兵从剩余从节点中按优先级、复制偏移量、runid 排序选出新主;向新主发送 REPLICAOF NO ONE 使其升主;向其余从节点发送 REPLICAOF 指向新主;最后更新自身配置并通过发布订阅通知客户端。旧主恢复后会被哨兵降级为从节点。

Redis 哨兵故障转移与客户端重连链路 Sentinel-1 quorum 投票 Sentinel-2 Leader 选举 Sentinel-3 配置提供者 旧主 Master 192.168.10.11:6379 心跳超时 → 下线 新主 Master 192.168.10.12:6379 REPLICAOF NO ONE 从节点 Replica 192.168.10.13:6379 客户端 / 连接池 订阅 +switch-master 刷新拓扑 → 重连新主 写走主 / 读走从 监控 升主 重定向 通知 旧主降级

客户端重连与读写分离实践

哨兵只负责通知,真正的重连由客户端完成。以 Jedis 为例,通过 JedisSentinelPool 传入哨兵地址集合与主节点名称,客户端启动时向哨兵询问当前主节点,并订阅 +switch-master 事件。故障转移发生后,客户端收到通知,关闭旧连接、重新向哨兵查询新主地址并重建连接池。

Set<String> sentinels = new HashSet<>();
sentinels.add("192.168.10.21:26379");
sentinels.add("192.168.10.22:26379");
sentinels.add("192.168.10.23:26379");

JedisSentinelPool pool = new JedisSentinelPool(
    "mymaster", sentinels, "Redis@2024");

try (Jedis jedis = pool.getResource()) {
    jedis.set("user:1001", "active");   // 写走主节点
}

读写分离需要区分场景。缓存读多写少时,把读请求分到从节点可以提升吞吐,但必须接受复制延迟带来的短暂不一致。对一致性敏感的操作,例如“写后立即读”的会话数据,应强制走主节点。Spring Data Redis 中可通过 LettuceClientConfiguration 设置 ReadFrom.REPLICA_PREFERRED,让读优先落到从节点,主节点不可用时再回退。

读写分离的常见坑:从节点默认 replica-read-only yes,若误改为可写,会造成主从数据分叉;另外从节点执行全量同步期间会清空自身数据并阻塞读请求,切换后短时间内读延迟可能升高。

验证与判断依据

部署完成后应逐项验证。执行 redis-cli -p 26379 sentinel master mymaster 查看 num-slaves、flags 与 quorum;执行 INFO replication 确认从节点 master_link_status:up 且 master_repl_offset 持续增长。模拟故障时直接 kill 主节点进程,观察哨兵日志出现 +odown、+switch-master,并在 30 秒内确认新主可写、旧主恢复后角色变为 slave。客户端侧应观察连接池是否在切换窗口内抛出 JedisConnectionException,并通过重试与降级策略保证业务不中断。

观察项正常表现异常判断
master_link_statusupdown 表示复制中断
哨兵 flagsmasters_down / o_down 表示已下线
切换耗时小于 failover-timeout超时说明选举或同步受阻
客户端重连自动指向新主持续报错需检查哨兵地址与订阅

高可用的关键不在于“配了哨兵”,而在于切换窗口内客户端能否正确感知并恢复。把哨兵数量、quorum、超时参数、复制积压缓冲区和客户端重试策略作为一个整体来设计,才能让 Redis 在主节点故障时真正保持业务连续。