4. Redis 主从与哨兵架构:主从复制原理、全量复制与增量复制、哨兵模式故障转移、读写分离实践

4.1 主从复制架构与核心原理

Redis 主从复制(Replication)是构建高可用 Redis 服务的基础。主节点(Master)负责处理写请求,从节点(Slave)通过复制主节点的数据来提供读服务。这种架构实现了数据的冗余备份,并为读写分离提供了基础。

4.1.1 主从复制流程

当从节点向主节点发起连接时,复制过程分为以下步骤:

  1. 建立连接:从节点通过 REPLICAOF 命令(或旧版 SLAVEOF)指定主节点 IP 和端口,建立 TCP 连接。
  2. 身份验证:如果主节点设置了 requirepass,从节点需使用 masterauth 密码进行认证。
  3. 同步数据:主节点根据情况执行全量复制或增量复制,将数据同步给从节点。
  4. 持续复制:同步完成后,主节点将后续的写命令以命令流(Replication Stream)的形式持续发送给从节点。
关键命令:
# 在从节点上执行,指定主节点
REPLICAOF 192.168.1.10 6379

# 取消复制,从节点变回独立节点
REPLICAOF NO ONE

4.1.2 主从复制拓扑结构

Redis 支持多种主从拓扑,常见的有:

拓扑类型 结构描述 适用场景
一主一从 一个主节点,一个从节点 基础数据备份,简单读写分离
一主多从 一个主节点,多个从节点 读多写少场景,扩展读能力
树形主从 从节点下再挂载从节点 跨机房部署,减轻主节点网络压力
Redis 主从复制拓扑结构 Master 主节点(写) Slave 1 从节点(读) Slave 2 从节点(读) Slave 3 从节点(级联) 拓扑说明 • 一主多从:Master → Slave1, Slave2 • 树形级联:Slave2 → Slave3 • 级联可减轻 Master 网络负载

4.2 全量复制与增量复制

Redis 根据场景选择不同的复制策略。全量复制用于初次同步或断连后无法增量恢复的情况;增量复制用于网络断开后重新连接且数据差异较小的情况。

4.2.1 全量复制(Full Resynchronization)

全量复制发生在以下场景:

  • 从节点第一次连接到主节点。
  • 从节点断开时间过长,主节点的复制积压缓冲区(Replication Backlog)已无法覆盖缺失的数据。
  • 从节点主动发起 REPLICAOF 命令。

全量复制流程:

  1. 主节点执行 BGSAVE 生成 RDB 快照文件。
  2. 主节点将 RDB 文件发送给从节点。
  3. 从节点清空当前所有数据,加载 RDB 文件。
  4. 主节点将生成 RDB 期间累积的写命令(存储在缓冲区中)发送给从节点。
  5. 从节点执行这些写命令,完成数据同步。
性能影响: 全量复制非常消耗资源。主节点需要 fork 子进程执行 BGSAVE,占用 CPU 和内存;传输 RDB 文件会占用大量网络带宽。在生产环境中,应避免频繁触发全量复制。

4.2.2 增量复制(Partial Resynchronization)

增量复制利用 复制积压缓冲区 和 复制偏移量 实现高效的数据续传。

核心概念 说明
Replication ID 主节点的唯一标识,用于标识数据集的版本
Replication Offset 主从节点各自记录的复制进度偏移量
Replication Backlog 主节点维护的固定大小环形缓冲区,存储最近执行的写命令

增量复制流程:

  1. 从节点重连主节点后,发送自己的 Replication ID 和 Offset。
  2. 主节点检查从节点的 Offset 是否仍在自己的 Backlog 范围内。
  3. 如果在范围内,主节点将从 Offset 之后的数据发送给从节点。
  4. 如果不在范围内,则触发全量复制。
优化建议: 适当增大 repl-backlog-size 参数(默认 1MB),可以降低因网络短暂中断而触发全量复制的概率。建议设置为 repl-backlog-size 64mb 或更大。
全量复制与增量复制流程对比 全量复制 1. 从节点发起连接 2. 主节点执行 BGSAVE 3. 发送 RDB 文件 4. 从节点清空并加载 RDB 5. 发送缓冲区增量命令 ✅ 同步完成 增量复制 1. 从节点重连并发送 Offset 2. 主节点检查 Backlog 3. 发送缺失的增量命令 4. 从节点执行增量命令 ✅ 同步完成(无需 RDB) ⚡ 性能高,资源消耗低

4.3 哨兵模式与故障转移

Redis Sentinel(哨兵)是一个分布式系统,用于监控 Redis 主从架构的健康状态,并在主节点发生故障时自动执行故障转移(Failover),将从节点提升为新的主节点。

4.3.1 哨兵的核心功能

功能 说明
监控(Monitoring) 持续检查主节点和从节点是否正常运行
通知(Notification) 当被监控的 Redis 实例出现问题时,通知系统管理员
自动故障转移(Automatic Failover) 主节点不可用时,自动选举一个从节点升级为新的主节点
配置提供(Configuration Provider) 客户端通过哨兵获取当前主节点的地址

4.3.2 故障转移流程

  1. 主观下线(SDOWN):单个哨兵实例发现主节点在 down-after-milliseconds 时间内没有响应。
  2. 客观下线(ODOWN):多个哨兵(达到 quorum 数量)都认为主节点主观下线。
  3. 领导者选举:哨兵集群通过 Raft 算法选举一个领导者哨兵来执行故障转移。
  4. 从节点选举:领导者哨兵从健康的从节点中选举一个作为新的主节点。选举依据优先级(slave-priority)、复制偏移量、运行 ID。
  5. 角色切换:将选中的从节点执行 REPLICAOF NO ONE 提升为主节点。
  6. 重新配置:将其他从节点指向新的主节点,并通知客户端更新主节点地址。
哨兵配置示例(sentinel.conf):
# 监控 mymaster,2 个哨兵同意即判定客观下线
sentinel monitor mymaster 127.0.0.1 6379 2

# 主节点 30000 毫秒无响应即判定主观下线
sentinel down-after-milliseconds mymaster 30000

# 故障转移超时时间
sentinel failover-timeout mymaster 180000

# 故障转移后,同时向新主节点发起复制的从节点数量
sentinel parallel-syncs mymaster 1

4.4 读写分离实践

读写分离是主从架构最常见的应用模式。写请求发送到主节点,读请求分散到多个从节点,从而提升系统的整体吞吐量。

4.4.1 读写分离架构

客户端需要自行实现或通过中间件实现读写分离逻辑。常见的实现方式包括:

  • 客户端 SDK 直连:在业务代码中维护主节点和从节点列表,根据操作类型选择不同的连接。
  • 代理中间件:如 Twemproxy、Codis、Redis Cluster 等,代理层自动路由读写请求。
  • Sentinel 配合客户端:客户端通过哨兵获取主节点地址,从节点地址可通过哨兵或配置中心获取。

4.4.2 读写分离的注意事项

问题 说明 解决方案
数据延迟 主从复制存在异步延迟,从节点可能读到旧数据 对一致性要求高的读请求强制走主节点;使用 WAIT 命令等待复制完成
过期数据 从节点使用惰性删除 + 定时删除策略,过期 key 可能未及时清理 设置合理的 lazyfree-lazy-eviction 参数;业务层容忍短暂过期数据
连接管理 从节点数量多时,客户端需要维护大量连接 使用连接池;通过代理层统一管理后端连接
故障转移感知 主节点切换后,客户端需要更新主节点地址 集成 Sentinel 客户端,自动监听主节点切换事件
最佳实践: 在业务中,将读请求分为“强一致性读”和“最终一致性读”。强一致性读(如用户余额查询)直接走主节点;最终一致性读(如商品详情页)走从节点。这样可以平衡性能和一致性。

4.4.3 读写分离代码示例(Java + Jedis)

// 使用 JedisSentinelPool 实现读写分离
public class RedisReadWriteSplit {

    private final JedisSentinelPool masterPool;
    private final List<JedisPool> slavePools;

    public RedisReadWriteSplit(String masterName, Set<String> sentinels,
                                List<String> slaveHosts) {
        // 主节点连接池(通过哨兵获取)
        this.masterPool = new JedisSentinelPool(masterName, sentinels);
        // 从节点连接池列表
        this.slavePools = slaveHosts.stream()
                .map(host -> new JedisPool(host, 6379))
                .collect(Collectors.toList());
    }

    // 写操作:强制走主节点
    public String set(String key, String value) {
        try (Jedis jedis = masterPool.getResource()) {
            return jedis.set(key, value);
        }
    }

    // 读操作:轮询从节点
    public String get(String key) {
        // 简单轮询策略
        int index = ThreadLocalRandom.current().nextInt(slavePools.size());
        try (Jedis jedis = slavePools.get(index).getResource()) {
            return jedis.get(key);
        }
    }

    // 强一致性读:直接走主节点
    public String getStrongConsistency(String key) {
        try (Jedis jedis = masterPool.getResource()) {
            return jedis.get(key);
        }
    }
}
避坑指南: 不要在所有场景下都无脑使用读写分离。如果业务写多读少,或者对数据一致性要求极高,读写分离反而会增加系统复杂度和延迟。建议在压测后根据实际 QPS 和延迟指标决定是否启用。