4. Redis 主从与哨兵架构:主从复制原理、全量复制与增量复制、哨兵模式故障转移、读写分离实践
4.1 主从复制架构与核心原理
Redis 主从复制(Replication)是构建高可用 Redis 服务的基础。主节点(Master)负责处理写请求,从节点(Slave)通过复制主节点的数据来提供读服务。这种架构实现了数据的冗余备份,并为读写分离提供了基础。
4.1.1 主从复制流程
当从节点向主节点发起连接时,复制过程分为以下步骤:
- 建立连接:从节点通过
REPLICAOF命令(或旧版SLAVEOF)指定主节点 IP 和端口,建立 TCP 连接。 - 身份验证:如果主节点设置了
requirepass,从节点需使用masterauth密码进行认证。 - 同步数据:主节点根据情况执行全量复制或增量复制,将数据同步给从节点。
- 持续复制:同步完成后,主节点将后续的写命令以命令流(Replication Stream)的形式持续发送给从节点。
关键命令:
# 在从节点上执行,指定主节点
REPLICAOF 192.168.1.10 6379
# 取消复制,从节点变回独立节点
REPLICAOF NO ONE
4.1.2 主从复制拓扑结构
Redis 支持多种主从拓扑,常见的有:
| 拓扑类型 | 结构描述 | 适用场景 |
|---|---|---|
| 一主一从 | 一个主节点,一个从节点 | 基础数据备份,简单读写分离 |
| 一主多从 | 一个主节点,多个从节点 | 读多写少场景,扩展读能力 |
| 树形主从 | 从节点下再挂载从节点 | 跨机房部署,减轻主节点网络压力 |
4.2 全量复制与增量复制
Redis 根据场景选择不同的复制策略。全量复制用于初次同步或断连后无法增量恢复的情况;增量复制用于网络断开后重新连接且数据差异较小的情况。
4.2.1 全量复制(Full Resynchronization)
全量复制发生在以下场景:
- 从节点第一次连接到主节点。
- 从节点断开时间过长,主节点的复制积压缓冲区(Replication Backlog)已无法覆盖缺失的数据。
- 从节点主动发起
REPLICAOF命令。
全量复制流程:
- 主节点执行
BGSAVE生成 RDB 快照文件。 - 主节点将 RDB 文件发送给从节点。
- 从节点清空当前所有数据,加载 RDB 文件。
- 主节点将生成 RDB 期间累积的写命令(存储在缓冲区中)发送给从节点。
- 从节点执行这些写命令,完成数据同步。
性能影响: 全量复制非常消耗资源。主节点需要 fork 子进程执行 BGSAVE,占用 CPU 和内存;传输 RDB 文件会占用大量网络带宽。在生产环境中,应避免频繁触发全量复制。
4.2.2 增量复制(Partial Resynchronization)
增量复制利用 复制积压缓冲区 和 复制偏移量 实现高效的数据续传。
| 核心概念 | 说明 |
|---|---|
| Replication ID | 主节点的唯一标识,用于标识数据集的版本 |
| Replication Offset | 主从节点各自记录的复制进度偏移量 |
| Replication Backlog | 主节点维护的固定大小环形缓冲区,存储最近执行的写命令 |
增量复制流程:
- 从节点重连主节点后,发送自己的
Replication ID和Offset。 - 主节点检查从节点的 Offset 是否仍在自己的 Backlog 范围内。
- 如果在范围内,主节点将从 Offset 之后的数据发送给从节点。
- 如果不在范围内,则触发全量复制。
优化建议: 适当增大
repl-backlog-size 参数(默认 1MB),可以降低因网络短暂中断而触发全量复制的概率。建议设置为 repl-backlog-size 64mb 或更大。
4.3 哨兵模式与故障转移
Redis Sentinel(哨兵)是一个分布式系统,用于监控 Redis 主从架构的健康状态,并在主节点发生故障时自动执行故障转移(Failover),将从节点提升为新的主节点。
4.3.1 哨兵的核心功能
| 功能 | 说明 |
|---|---|
| 监控(Monitoring) | 持续检查主节点和从节点是否正常运行 |
| 通知(Notification) | 当被监控的 Redis 实例出现问题时,通知系统管理员 |
| 自动故障转移(Automatic Failover) | 主节点不可用时,自动选举一个从节点升级为新的主节点 |
| 配置提供(Configuration Provider) | 客户端通过哨兵获取当前主节点的地址 |
4.3.2 故障转移流程
- 主观下线(SDOWN):单个哨兵实例发现主节点在
down-after-milliseconds时间内没有响应。 - 客观下线(ODOWN):多个哨兵(达到
quorum数量)都认为主节点主观下线。 - 领导者选举:哨兵集群通过 Raft 算法选举一个领导者哨兵来执行故障转移。
- 从节点选举:领导者哨兵从健康的从节点中选举一个作为新的主节点。选举依据优先级(
slave-priority)、复制偏移量、运行 ID。 - 角色切换:将选中的从节点执行
REPLICAOF NO ONE提升为主节点。 - 重新配置:将其他从节点指向新的主节点,并通知客户端更新主节点地址。
哨兵配置示例(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 和延迟指标决定是否启用。