Redis主从复制与哨兵机制深度解析:从单机到高可用架构!

难度:⭐⭐⭐⭐ | 适合人群:想掌握Redis高可用方案的开发者


💥 开场:一次"惨痛"的宕机事故

时间: 周六凌晨2点
地点: 家里(睡梦中)
事件: 告警电话

📱 铃声大作...

运维(急促): "Redis服务器挂了!所有接口都超时了!"

我: "什么???" 😱(瞬间清醒)

运维: "磁盘满了,Redis进程被kill了!"

我: "赶紧重启啊!" 😰

运维: "在重启了,但是..."


5分钟后,Redis重启成功

运维: "重启好了,但用户在说数据丢了..."

我: "怎么会丢?有持久化啊!"

运维: "持久化配置的是RDB,5分钟一次,最后5分钟的数据全丢了..."

我: "......" 😭


第二天,紧急会议:

技术总监: "这次事故暴露了我们的架构问题:单点故障!"

哈吉米: "是的,Redis是单机部署,挂了整个系统就崩了。"

南北绿豆: "我们需要主从架构 + 哨兵机制,实现高可用。"

阿西噶阿西: "主从复制可以备份数据,哨兵可以自动故障转移,我给大家讲讲..."


🎯 第一问:什么是主从复制?

主从架构

主从复制 = Master-Slave(一主多从)

        Master(主节点)
        /      |      \
       /       |       \
    Slave1  Slave2  Slave3(从节点)

角色分工:

Master(主节点):
- 负责写操作
- 负责同步数据到从节点

Slave(从节点):
- 负责读操作
- 从主节点复制数据
- 主节点挂了可以升级为主节点

主从复制的作用

哈吉米: "主从复制有三大作用。"

1. 读写分离

客户端
    ↓
写请求 → Master(主节点)
读请求 → Slave(从节点,多个)

优势:

  • ✅ 分担Master压力
  • ✅ 提高并发能力
  • ✅ 读性能提升

2. 数据备份

Master(主数据)
    ↓ 实时同步
Slave(备份数据)

Master宕机:
    ↓
数据在Slave上
    ↓
可以恢复

3. 高可用

Master正常:
    Master提供服务

Master宕机:
    ↓
Slave升级为Master
    ↓
继续提供服务

🔄 第二问:主从复制原理

全量复制

第一次连接时,进行全量复制

sequenceDiagram
    participant Slave
    participant Master
    
    Slave->>Master: 1. 发送PSYNC命令
    Note over Slave: PSYNC ? -1<br/>(首次复制)
    
    Master-->>Slave: 2. 回复FULLRESYNC
    Note over Master: FULLRESYNC runid offset
    
    Master->>Master: 3. 执行BGSAVE,生成RDB文件
    Note over Master: 后台fork子进程<br/>不影响主进程
    
    Master->>Master: 4. 将生成期间的写命令<br/>缓存到复制缓冲区
    
    Master->>Slave: 5. 发送RDB文件
    Note over Slave: 清空旧数据<br/>加载RDB文件
    
    Master->>Slave: 6. 发送复制缓冲区的命令
    Note over Slave: 执行命令
    
    Note over Slave,Master: 全量复制完成!

增量复制

全量复制后,后续使用增量复制

sequenceDiagram
    participant Client
    participant Master
    participant Slave
    
    Client->>Master: SET key1 "value1"
    Master-->>Client: OK
    
    Master->>Master: 写入复制缓冲区
    Master->>Slave: 同步命令:SET key1 "value1"
    
    Client->>Master: DEL key2
    Master-->>Client: OK
    
    Master->>Master: 写入复制缓冲区
    Master->>Slave: 同步命令:DEL key2
    
    Note over Slave: 实时同步<br/>数据一致

复制过程详解

南北绿豆: "我们详细看看复制过程。"

步骤1:建立连接

# 从节点配置
replicaof 192.168.1.100 6379  # 主节点地址
masterauth password123        # 主节点密码

步骤2:从节点发送PSYNC

从节点向主节点发送PSYNC命令:
PSYNC <runid> <offset>

首次复制:
PSYNC ? -1

断线重连:
PSYNC <之前的runid> <之前的offset>

步骤3:主节点判断

if (从节点是首次复制 || 复制偏移量太旧) {
    执行全量复制
} else {
    执行增量复制(只发送缺失的命令)
}

步骤4:数据同步

全量复制:
Master:生成RDB → 发送RDB → 发送缓冲区命令
Slave:清空数据 → 加载RDB → 执行命令

增量复制:
Master:发送缺失的命令
Slave:执行命令

🔍 第三问:主从延迟问题

什么是主从延迟?

阿西噶阿西: "主从复制不是瞬间完成的,有延迟!"

场景演示:

sequenceDiagram
    participant Client
    participant Master
    participant Slave
    
    Client->>Master: SET user:1 "张三"
    Master-->>Client: OK
    
    Note over Master,Slave: 网络延迟:10ms
    
    Master->>Slave: 同步命令(延迟中...)
    
    Client->>Slave: GET user:1
    Slave-->>Client: nil(还没同步到)
    
    Note over Slave: 收到同步命令
    Slave->>Slave: SET user:1 "张三"
    
    Client->>Slave: GET user:1
    Slave-->>Client: "张三"(现在有了)

问题: 写入后立即读,可能读到旧数据!


延迟原因

1. 网络延迟
   - 主从节点网络传输耗时
   
2. 主节点写入量大
   - 复制缓冲区积压
   
3. 从节点处理慢
   - 从节点负载高
   
4. 大Key复制慢
   - 单个key值太大

解决方案

方案1:强制读主节点

@Service
public class UserService {
    
    @Autowired
    private RedisTemplate<String, String> masterRedis;  // 主节点
    
    @Autowired
    private RedisTemplate<String, String> slaveRedis;   // 从节点
    
    public void updateUser(User user) {
        String key = "user:" + user.getId();
        
        // 写入主节点
        masterRedis.opsForValue().set(key, JSON.toJSONString(user));
    }
    
    public User getUser(Long userId, boolean forceReadMaster) {
        String key = "user:" + userId;
        
        String json;
        if (forceReadMaster) {
            // 强制读主节点(保证一致性)
            json = masterRedis.opsForValue().get(key);
        } else {
            // 读从节点(性能更好)
            json = slaveRedis.opsForValue().get(key);
        }
        
        return JSON.parseObject(json, User.class);
    }
}

方案2:缓存标记

public void updateUser(User user) {
    String key = "user:" + user.getId();
    
    // 写入主节点
    masterRedis.opsForValue().set(key, JSON.toJSONString(user));
    
    // 设置一个标记(1秒后过期)
    String flagKey = "updated:user:" + user.getId();
    masterRedis.opsForValue().set(flagKey, "1", 1, TimeUnit.SECONDS);
}

public User getUser(Long userId) {
    String key = "user:" + userId;
    String flagKey = "updated:user:" + userId;
    
    // 检查是否刚更新过
    if (masterRedis.hasKey(flagKey)) {
        // 刚更新,读主节点
        return JSON.parseObject(masterRedis.opsForValue().get(key), User.class);
    } else {
        // 没有更新标记,读从节点
        return JSON.parseObject(slaveRedis.opsForValue().get(key), User.class);
    }
}

🔭 第四问:哨兵(Sentinel)机制

什么是哨兵?

哨兵 = Sentinel(监控者)

南北绿豆: "哨兵就是Redis的'守护天使',监控Redis健康状态。"

架构图:

         Sentinel1   Sentinel2   Sentinel3
            ↓           ↓           ↓
         监控        监控         监控
            ↓           ↓           ↓
        ┌──────────────────────────────┐
        │         Master               │
        └──────────────────────────────┘
              /          |          \
             /           |           \
         Slave1       Slave2      Slave3

哨兵的作用

1. 监控(Monitoring)
   - 监控Master和Slave是否正常运行
   
2. 通知(Notification)
   - 当Redis节点出现问题时,通知管理员或其他程序
   
3. 自动故障转移(Automatic Failover)
   - Master宕机时,自动选举一个Slave升级为Master
   
4. 配置提供者(Configuration Provider)
   - 客户端连接哨兵,获取当前Master地址

故障转移流程

完整时序图:

sequenceDiagram
    participant Client
    participant Sentinel1
    participant Sentinel2
    participant Sentinel3
    participant Master
    participant Slave1
    participant Slave2
    
    Note over Master: Master正常运行
    
    Sentinel1->>Master: PING
    Master-->>Sentinel1: PONG
    
    Note over Master: Master宕机!💥
    
    Sentinel1->>Master: PING
    Note over Sentinel1: 超时无响应
    
    Sentinel1->>Sentinel1: 主观下线判断
    Note over Sentinel1: Master可能挂了
    
    Sentinel1->>Sentinel2: 询问:Master是否下线?
    Sentinel2-->>Sentinel1: 是,我也ping不通
    
    Sentinel1->>Sentinel3: 询问:Master是否下线?
    Sentinel3-->>Sentinel1: 是,我也ping不通
    
    Note over Sentinel1,Sentinel3: 客观下线判断<br/>超过半数认为下线
    
    Sentinel1->>Sentinel1: 发起选举
    Note over Sentinel1: 我要当Leader进行故障转移
    
    Sentinel2->>Sentinel1: 投票给你
    Sentinel3->>Sentinel1: 投票给你
    
    Note over Sentinel1: 成为Leader
    
    Sentinel1->>Slave1: 选举你为新Master
    Note over Slave1: Slave1升级为Master
    
    Sentinel1->>Slave2: 重新配置:复制Slave1
    Slave2->>Slave1: 开始复制新Master
    
    Sentinel1->>Client: 通知:Master地址变更
    Client->>Slave1: 连接新Master

主观下线 vs 客观下线

哈吉米: "哨兵判断节点下线分两步。"

主观下线(SDOWN):

单个哨兵判断:
    ↓
Sentinel1 PING Master
    ↓
超时无响应(连续N次)
    ↓
Sentinel1认为Master主观下线

配置:

# 30秒内PING不通,认为主观下线
sentinel down-after-milliseconds mymaster 30000

客观下线(ODOWN):

多个哨兵判断:
    ↓
Sentinel1认为Master主观下线
    ↓
询问其他哨兵
    ↓
超过半数哨兵认为下线
    ↓
客观下线(真的挂了)
    ↓
触发故障转移

配置:

# 至少2个哨兵认为下线才算客观下线
sentinel monitor mymaster 192.168.1.100 6379 2
                                              ↑
                                          quorum(法定人数)

⚙️ 第五问:哨兵配置与搭建

配置文件

sentinel.conf:

# 哨兵端口
port 26379

# 工作目录
dir /var/lib/redis/sentinel

# 监控的Master
# sentinel monitor <master-name> <ip> <port> <quorum>
sentinel monitor mymaster 192.168.1.100 6379 2

# 多久无响应算主观下线(毫秒)
sentinel down-after-milliseconds mymaster 30000

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

# 同时进行复制的Slave数量
sentinel parallel-syncs mymaster 1

# Master密码
sentinel auth-pass mymaster password123

# 通知脚本(可选)
sentinel notification-script mymaster /var/redis/notify.sh

# 故障转移脚本(可选)
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

搭建主从 + 哨兵

南北绿豆: "我演示一下搭建过程。"

步骤1:配置Master

# redis-master.conf
bind 0.0.0.0
port 6379
daemonize yes
pidfile /var/run/redis-master.pid
logfile /var/log/redis/redis-master.log
dir /var/lib/redis/master

# 持久化
appendonly yes
appendfsync everysec

步骤2:配置Slave

# redis-slave1.conf
bind 0.0.0.0
port 6380
daemonize yes
pidfile /var/run/redis-slave1.pid
logfile /var/log/redis/redis-slave1.log
dir /var/lib/redis/slave1

# 主从配置
replicaof 192.168.1.100 6379  # 主节点地址
masterauth password123         # 主节点密码

# 从节点只读
replica-read-only yes

# 持久化
appendonly yes

步骤3:配置哨兵(3个)

# sentinel1.conf
port 26379
daemonize yes
pidfile /var/run/redis-sentinel1.pid
logfile /var/log/redis/sentinel1.log
dir /var/lib/redis/sentinel1

sentinel monitor mymaster 192.168.1.100 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000

sentinel2.conf、sentinel3.conf类似(改端口和路径)


步骤4:启动

# 启动Master
redis-server /etc/redis/redis-master.conf

# 启动Slave
redis-server /etc/redis/redis-slave1.conf
redis-server /etc/redis/redis-slave2.conf

# 启动Sentinel
redis-sentinel /etc/redis/sentinel1.conf
redis-sentinel /etc/redis/sentinel2.conf
redis-sentinel /etc/redis/sentinel3.conf

步骤5:验证

# 连接Master
redis-cli -p 6379

# 查看主从信息
redis> INFO replication

# 输出:
role:master
connected_slaves:2
slave0:ip=192.168.1.101,port=6380,state=online,offset=1234
slave1:ip=192.168.1.102,port=6381,state=online,offset=1234
# 连接Sentinel
redis-cli -p 26379

# 查看Master信息
sentinel> SENTINEL masters

# 查看Slave信息
sentinel> SENTINEL slaves mymaster

# 查看其他Sentinel
sentinel> SENTINEL sentinels mymaster

💻 第六问:Java客户端使用

Jedis连接哨兵

@Configuration
public class RedisConfig {
    
    @Bean
    public JedisSentinelPool jedisSentinelPool() {
        
        // 哨兵地址列表
        Set<String> sentinels = new HashSet<>();
        sentinels.add("192.168.1.100:26379");
        sentinels.add("192.168.1.101:26379");
        sentinels.add("192.168.1.102:26379");
        
        // 创建连接池
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(100);
        poolConfig.setMaxIdle(20);
        poolConfig.setMinIdle(10);
        
        // 连接哨兵(master-name、密码、连接池配置)
        return new JedisSentinelPool(
            "mymaster",      // 主节点名称
            sentinels,       // 哨兵地址
            poolConfig,      // 连接池配置
            2000,            // 超时时间
            "password123"    // 密码
        );
    }
}

使用:

@Service
public class UserService {
    
    @Autowired
    private JedisSentinelPool sentinelPool;
    
    public void setUser(String key, String value) {
        try (Jedis jedis = sentinelPool.getResource()) {
            jedis.set(key, value);
        }
    }
    
    public String getUser(String key) {
        try (Jedis jedis = sentinelPool.getResource()) {
            return jedis.get(key);
        }
    }
}

优势:

  • ✅ 自动发现Master地址
  • ✅ Master切换时自动重连
  • ✅ 对业务代码透明

Redisson连接哨兵

@Configuration
public class RedissonConfig {
    
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        
        // 哨兵模式配置
        config.useSentinelServers()
            .setMasterName("mymaster")
            .addSentinelAddress(
                "redis://192.168.1.100:26379",
                "redis://192.168.1.101:26379",
                "redis://192.168.1.102:26379"
            )
            .setPassword("password123")
            .setDatabase(0)
            .setConnectTimeout(3000)
            .setTimeout(3000)
            .setRetryAttempts(3)
            .setRetryInterval(1500);
        
        return Redisson.create(config);
    }
}

🧪 第七问:故障转移实战演示

模拟Master宕机

阿西噶阿西: "我们实际测试一下故障转移。"

初始状态:

# Master
redis-cli -p 6379 INFO replication
role:master
connected_slaves:2

# Slave1
redis-cli -p 6380 INFO replication
role:slave
master_host:192.168.1.100
master_port:6379

# Sentinel
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
192.168.1.100
6379

模拟宕机:

# 杀掉Master进程
kill -9 <master-pid>

观察哨兵日志:

[26379] 15:30:00 # +sdown master mymaster 192.168.1.100 6379
    ↓ 主观下线

[26379] 15:30:02 # +odown master mymaster 192.168.1.100 6379 #quorum 2/2
    ↓ 客观下线

[26379] 15:30:02 # +vote-for-leader abc123def456...
    ↓ 选举Leader

[26379] 15:30:03 # +elected-leader master mymaster 192.168.1.100 6379
    ↓ 成为Leader

[26379] 15:30:03 # +failover-state-select-slave master mymaster 192.168.1.100 6379
    ↓ 选择Slave

[26379] 15:30:03 # +selected-slave slave 192.168.1.101:6380
    ↓ 选中Slave1

[26379] 15:30:04 # +failover-state-send-slaveof-noone slave 192.168.1.101:6380
    ↓ 让Slave1成为Master

[26379] 15:30:05 # +failover-state-reconf-slaves master mymaster 192.168.1.100 6379
    ↓ 重新配置其他Slave

[26379] 15:30:06 # +slave-reconf-sent slave 192.168.1.102:6381
    ↓ 配置Slave2复制新Master

[26379] 15:30:07 # +failover-end master mymaster 192.168.1.100 6379
    ↓ 故障转移完成

[26379] 15:30:08 # +switch-master mymaster 192.168.1.100 6379 192.168.1.101 6380
    ↓ Master地址切换

验证新Master:

# 连接原来的Slave1(现在的Master)
redis-cli -p 6380 INFO replication

role:master  # 已经变成Master了
connected_slaves:1  # 有1个从节点
slave0:ip=192.168.1.102,port=6381

客户端自动切换:

// 使用哨兵模式的客户端会自动切换
redissonClient.getBucket("test").set("hello");
// 自动连接到新的Master(192.168.1.101:6380)

整个过程对业务代码透明!


🎯 第八问:哨兵选举规则

选举新Master的规则

哈吉米: "哨兵选举新Master有优先级。"

选举流程:

1. 过滤掉不健康的Slave
   - 断线的
   - 5秒内没回复PING的
   - 与Master断开超过10倍down-after-milliseconds的
   
2. 选择优先级最高的
   - replica-priority配置(默认100)
   
3. 优先级相同,选择复制偏移量最大的
   - 数据最完整的
   
4. 还相同,选择运行ID最小的
   - runid字典序最小

配置优先级:

# slave1.conf
replica-priority 100  # 默认

# slave2.conf
replica-priority 90  # 优先级更高,更容易被选为Master

# slave3.conf
replica-priority 0  # 永远不会被选为Master(只做备份)

💡 最佳实践

1. 哨兵数量

推荐:奇数个(3个或5个)

原因:
- 需要超过半数才能判定客观下线
- 3个哨兵:允许1个挂掉
- 5个哨兵:允许2个挂掉

不推荐:
- 1个哨兵:单点故障
- 2个哨兵:1个挂了达不到半数
- 偶数个:容易脑裂

2. 哨兵部署

✅ 推荐:哨兵部署在不同机器
    Sentinel1 → 服务器A
    Sentinel2 → 服务器B
    Sentinel3 → 服务器C

❌ 不推荐:哨兵和Redis在同一机器
    服务器挂了,哨兵和Redis一起挂

3. 主从数量

推荐:
    1个Master + 2-3个Slave

原因:
    - 2个Slave足够备份
    - Slave太多会增加Master同步压力
    
不推荐:
    - 只有1个Slave:备份不够
    - 超过5个Slave:同步压力大

4. 读写分离配置

@Configuration
public class RedisConfig {
    
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        
        config.useSentinelServers()
            .setMasterName("mymaster")
            .addSentinelAddress("redis://192.168.1.100:26379")
            .setReadMode(ReadMode.SLAVE)  // ← 读从Slave
            .setSubscriptionMode(SubscriptionMode.MASTER);  // 订阅从Master
        
        return Redisson.create(config);
    }
}

ReadMode选项:

MASTER:只读Master
SLAVE:只读Slave
MASTER_SLAVE:Master和Slave都读

💡 知识点总结

主从复制与哨兵核心要点

主从复制作用

  • 读写分离(提高性能)
  • 数据备份(防止丢失)
  • 高可用(故障转移)

复制原理

  • 全量复制(首次):BGSAVE生成RDB
  • 增量复制(后续):同步写命令
  • 复制缓冲区:暂存命令

主从延迟

  • 网络延迟
  • 主节点写入量大
  • 解决:强制读主、缓存标记

哨兵机制

  • 监控Redis健康
  • 自动故障转移
  • 配置提供者

故障转移流程

  1. 主观下线(单个哨兵判断)
  2. 客观下线(多数哨兵判断)
  3. 选举Leader哨兵
  4. 选举新Master
  5. 重新配置Slave
  6. 通知客户端

最佳实践

  • 哨兵:奇数个(3或5)
  • 哨兵:不同机器部署
  • Slave:2-3个
  • 读写分离:读从Slave

记忆口诀

主从复制分角色,
Master写Slave读。
全量复制传RDB,
增量复制传命令。
哨兵监控来守护,
故障转移自动搞。
主观客观两判断,
超过半数才算数。
选举新主有规则,
优先级高复制全。
生产至少三哨兵,
奇数部署在不同机。

🤔 常见面试题

Q1: 主从复制的流程?

A:

全量复制(首次):
1. Slave发送PSYNC ? -1
2. Master回复FULLRESYNC
3. Master执行BGSAVE生成RDB
4. Master发送RDB文件给Slave
5. Slave加载RDB文件
6. Master发送复制缓冲区的命令
7. Slave执行命令
8. 全量复制完成

增量复制(后续):
1. Master有写操作
2. 写入复制缓冲区
3. 异步发送给Slave
4. Slave执行命令

Q2: 哨兵如何判断Master下线?

A:

两步判断:

1. 主观下线(SDOWN)
   - 单个Sentinel PING Master
   - 超过down-after-milliseconds无响应
   - 认为主观下线

2. 客观下线(ODOWN)
   - Sentinel询问其他Sentinel
   - 超过quorum个Sentinel认为下线
   - 确认客观下线
   - 触发故障转移

配置:
sentinel monitor mymaster 192.168.1.100 6379 2
                                              ↑
                                          quorum=2

Q3: 哨兵如何选举新Master?

A:

选举规则(优先级从高到低):

1. 过滤不健康的Slave
   - 断线的
   - 长时间未响应的

2. 选择replica-priority最高的

3. 优先级相同,选择复制偏移量最大的
   - 数据最完整

4. 还相同,选择runid最小的

最终只有一个Slave被选为Master

Q4: 主从复制有什么缺点?

A:

缺点:

1. 主从延迟
   - 写入Master后,Slave可能还没同步
   - 立即读Slave可能读到旧数据

2. Master写压力
   - 所有写操作都在Master
   - Master是瓶颈

3. 容量限制
   - 所有节点存储全量数据
   - 数据量大时,单机内存不够

解决:
- 延迟:读主节点、缓存标记
- 写压力:Redis Cluster分片
- 容量:Redis Cluster分片

💬 写在最后

从单机到主从,再到哨兵,我们深入学习了Redis的高可用架构:

  • 🔄 理解了主从复制的原理和流程
  • 🔭 掌握了哨兵的监控和故障转移
  • ⚙️ 学会了哨兵的配置和搭建
  • 💻 完成了Java客户端的使用

这篇文章,希望能让你的Redis架构更加稳定可靠!

如果这篇文章对你有帮助,请:

  • 👍 点赞支持
  • ⭐ 收藏备用
  • 🔄 转发分享
  • 💬 评论交流

下一篇我们聊Redis Cluster集群! 👋