Redis主从同步机制深度解析:全量复制vs增量复制,数据一致性如何保证?

难度:⭐⭐⭐⭐⋆ | 适合人群:想深入理解Redis主从原理的开发者


💥 开场:一次"诡异"的数据不一致

时间: 周四晚上
地点: 公司(加班中)
事件: 数据异常

测试妹子: "你的接口有Bug!同一个用户的数据,查两次结果不一样!"

我: "不可能啊..." 😰

测试演示:

# 第1次查询
curl http://api/user/123
{"id":123,"name":"张三","age":25}

# 第2次查询(1秒后)
curl http://api/user/123
{"id":123,"name":"张三","age":24}  # age变了!

# 第3次查询
curl http://api/user/123
{"id":123,"name":"张三","age":25}  # 又变回来了!

我: "这是什么妖术???" 😱


紧急排查:

哈吉米: "你用了主从架构的Redis吧?"

我: "对啊,读写分离,读从Slave。"

南北绿豆: "那就是主从延迟了!"

查看Redis:

# Master
redis> GET user:123
{"id":123,"name":"张三","age":25}  # 最新数据

# Slave
redis> GET user:123
{"id":123,"name":"张三","age":24}  # 旧数据(还没同步)

阿西噶阿西: "你的负载均衡有时读Master,有时读Slave,所以数据不一致!"

我: "那主从同步到底是怎么工作的?为什么会有延迟?" 🤔

哈吉米: "来,我给你讲讲主从同步的详细机制..."


🎯 第一问:主从同步的两种方式

全量复制 vs 增量复制

南北绿豆: "主从同步分两种。"

全量复制(Full Resync):
    - 第一次建立主从关系
    - 传输所有数据
    - 数据量大,耗时长

增量复制(Partial Resync):
    - 断线重连后
    - 只传输缺失的数据
    - 数据量小,快速

什么时候用哪种?

场景1:首次建立主从
    → 全量复制

场景2:Slave短暂断线重连
    → 增量复制

场景3:Slave断线太久,数据差太多
    → 全量复制

场景4:正常运行中
    → 增量复制(实时同步)

📦 第二问:全量复制详细流程

完整时序图

sequenceDiagram
    participant Slave
    participant Master
    
    Note over Slave: 配置:replicaof master-ip master-port
    
    Slave->>Master: 1. 发送PSYNC ? -1
    Note over Slave: PSYNC replicationid offset<br/>首次复制:? -1
    
    Master-->>Slave: 2. 回复+FULLRESYNC <runid> <offset>
    Note over Master: runid: Master的唯一ID<br/>offset: 当前复制偏移量
    
    Slave->>Slave: 3. 保存Master的runid和offset
    
    Master->>Master: 4. 执行BGSAVE生成RDB
    Note over Master: fork子进程<br/>不阻塞主进程
    
    Note over Master: 5. RDB生成期间的写命令<br/>缓存到复制缓冲区
    
    Master->>Slave: 6. 发送RDB文件
    Note over Slave: 清空旧数据<br/>加载RDB文件
    
    Master->>Slave: 7. 发送复制缓冲区的命令
    Note over Slave: 执行命令
    
    Note over Slave,Master: 8. 全量复制完成<br/>进入增量复制阶段

详细步骤解析

步骤1:Slave发送PSYNC

# Slave连接Master后发送
PSYNC ? -1

# 参数说明:
# ?  : 还不知道Master的runid(首次连接)
# -1 : 复制偏移量未知

步骤2:Master回复FULLRESYNC

+FULLRESYNC 8a3b2c1d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9 0

# 参数:
# 8a3b2c... : Master的runid(唯一标识)
# 0         : 当前复制偏移量

步骤3:Slave保存信息

Slave记录:
- master_replid: 8a3b2c1d4e5f6g7h...
- master_repl_offset: 0

步骤4:Master执行BGSAVE

// Master fork子进程
pid_t childpid = fork();

if (childpid == 0) {
    // 子进程:生成RDB文件
    rdbSave("dump.rdb");
    exit(0);
} else {
    // 父进程:继续处理客户端请求
    // 不阻塞!
}

步骤5:缓存写命令

RDB生成期间(假设耗时10秒):
    ↓
这10秒内的写命令怎么办?
    ↓
缓存到"复制缓冲区"
    ↓
等RDB发送完后,再发送这些命令

复制缓冲区:

// 环形缓冲区(默认1MB)
client->repl_backlog

// 如果缓冲区满了:
// - 旧数据被覆盖
// - Slave收到RDB后,缓冲区数据已不全
// - 只能重新全量复制

步骤6-7:发送RDB和命令

Master → Slave:
    ├─ RDB文件(磁盘IO + 网络IO)
    └─ 复制缓冲区的命令
    
Slave接收:
    ├─ 清空旧数据(FLUSHDB)
    ├─ 加载RDB文件到内存
    └─ 执行缓冲区命令

🔄 第三问:增量复制详细流程

复制积压缓冲区

哈吉米: "增量复制的核心是复制积压缓冲区!"

// Master维护一个环形缓冲区
typedef struct {
    char *buffer;           // 缓冲区
    long long offset;       // 复制偏移量
    long long size;         // 缓冲区大小(默认1MB)
} repl_backlog;

工作原理:

Master每执行一个写命令:
    ↓
1. 执行命令
2. 将命令写入复制积压缓冲区
3. 将命令发送给所有Slave
4. 更新复制偏移量

增量复制时序图

sequenceDiagram
    participant Slave
    participant Master
    participant 复制积压缓冲区
    
    Note over Slave,Master: Slave正常复制中
    
    Note over Slave: Slave断线!💥
    
    Note over Master: Master继续接收写命令
    Master->>复制积压缓冲区: 写入命令1
    Master->>复制积压缓冲区: 写入命令2
    Master->>复制积压缓冲区: 写入命令3
    
    Note over Slave: Slave重新连接
    
    Slave->>Master: PSYNC <runid> <offset>
    Note over Slave: 发送之前保存的<br/>runid和offset
    
    Master->>Master: 检查offset是否在缓冲区中
    Note over Master: 在缓冲区范围内 ✅
    
    Master-->>Slave: +CONTINUE
    Note over Master: 可以增量复制
    
    Master->>Slave: 发送offset之后的命令
    Note over Slave: 命令1、命令2、命令3
    
    Slave->>Slave: 执行命令
    
    Note over Slave,Master: 增量复制完成<br/>数据追上了

判断全量还是增量

阿西噶阿西: "Master收到PSYNC后会判断。"

// Master判断逻辑
if (Slave的runid != Master的runid) {
    // runid不匹配,说明Master重启过
    执行全量复制
}
else if (Slave的offset不在复制积压缓冲区中) {
    // offset太旧,缓冲区已覆盖
    执行全量复制
}
else {
    // 可以增量复制
    从offset位置开始发送命令
}

缓冲区大小的影响:

缓冲区大小:1MB(默认)
写入速度:100KB/s
    ↓
缓冲区可容纳:10秒的数据

Slave断线情况:
- 断线5秒 → 增量复制 ✅
- 断线15秒 → 全量复制 ❌(缓冲区数据被覆盖)

优化:增大缓冲区
repl-backlog-size 10mb  # 改成10MB
    ↓
可容纳:100秒的数据
断线容忍度提高

⚠️ 第四问:主从数据一致性问题

一致性级别

南北绿豆: "Redis主从不保证强一致性!"

强一致性(Strong Consistency):
    写入Master后,必须同步到所有Slave才返回
    ↓
    Redis不支持

最终一致性(Eventual Consistency):
    写入Master立即返回
    ↓
    异步同步到Slave
    ↓
    有延迟,但最终会一致
    ↓
    Redis使用这种

延迟原因

1. 网络延迟
   Master → Slave(跨机房:10-100ms)

2. Slave处理慢
   Slave负载高,处理命令慢

3. 复制缓冲区积压
   Master写入速度 > Slave处理速度

4. 大key复制
   单个key值太大(如10MB),传输慢

延迟监控

# Slave上查看复制延迟
redis> INFO replication

# 关键指标:
master_link_status:up               # 与Master的连接状态
master_last_io_seconds_ago:1        # 最后一次IO距今秒数
master_sync_in_progress:0           # 是否正在同步
slave_repl_offset:1234567          # Slave的复制偏移量

# Master上查看
redis> INFO replication

master_repl_offset:1234600          # Master的复制偏移量

# 计算延迟:
延迟 = master_repl_offset - slave_repl_offset
    = 1234600 - 1234567
    = 33字节的数据还没同步

💻 第五问:解决数据一致性问题

方案1:强制读Master

@Service
public class UserService {
    
    @Autowired
    @Qualifier("masterRedisTemplate")
    private RedisTemplate<String, String> masterRedis;
    
    @Autowired
    @Qualifier("slaveRedisTemplate")
    private RedisTemplate<String, String> slaveRedis;
    
    /**
     * 写操作:写Master
     */
    public void updateUser(User user) {
        String key = "user:" + user.getId();
        masterRedis.opsForValue().set(key, JSON.toJSONString(user));
    }
    
    /**
     * 读操作:根据场景选择
     */
    public User getUser(Long userId, boolean strongConsistency) {
        String key = "user:" + userId;
        
        String json;
        if (strongConsistency) {
            // 需要强一致性:读Master
            json = masterRedis.opsForValue().get(key);
        } else {
            // 可以接受延迟:读Slave(性能更好)
            json = slaveRedis.opsForValue().get(key);
        }
        
        return JSON.parseObject(json, User.class);
    }
}

方案2:延迟标记

/**
 * 写入时设置延迟标记
 */
public void updateUser(User user) {
    String key = "user:" + user.getId();
    String flagKey = "updated:user:" + user.getId();
    
    // 1. 写入Master
    masterRedis.opsForValue().set(key, JSON.toJSONString(user));
    
    // 2. 设置延迟标记(1秒后过期)
    masterRedis.opsForValue().set(flagKey, "1", 1, TimeUnit.SECONDS);
}

/**
 * 读取时检查标记
 */
public User getUser(Long userId) {
    String key = "user:" + userId;
    String flagKey = "updated:user:" + userId;
    
    // 1. 检查是否刚更新过
    if (masterRedis.hasKey(flagKey)) {
        // 刚更新,读Master
        String json = masterRedis.opsForValue().get(key);
        return JSON.parseObject(json, User.class);
    }
    
    // 2. 没有更新标记,读Slave
    String json = slaveRedis.opsForValue().get(key);
    return JSON.parseObject(json, User.class);
}

方案3:WAIT命令(Redis 3.0+)

阿西噶阿西: "Redis 3.0引入了WAIT命令,可以等待同步完成!"

# 语法:
WAIT numreplicas timeout

# 示例:
redis> SET key "value"
OK

redis> WAIT 2 1000
(integer) 2  # 2个Slave同步完成

# 等待至少2个Slave同步完成,超时1000ms

Java使用:

public void updateUserWithWait(User user) {
    String key = "user:" + user.getId();
    
    // 1. 写入Master
    redisTemplate.opsForValue().set(key, JSON.toJSONString(user));
    
    // 2. 等待至少1个Slave同步完成(超时1秒)
    Long syncedSlaves = redisTemplate.execute((RedisCallback<Long>) connection -> 
        connection.waitForReplication(1, 1000));
    
    if (syncedSlaves == null || syncedSlaves < 1) {
        System.err.println("⚠️  同步超时,可能数据不一致");
    }
}

注意:

  • WAIT会阻塞客户端
  • 影响性能
  • 生产环境慎用

🔍 第三问:复制偏移量详解

什么是复制偏移量?

复制偏移量(replication offset): 记录复制进度的"书签"

Master维护:
    master_repl_offset: 1234600  # Master已写入的字节数

每个Slave维护:
    slave_repl_offset: 1234567   # Slave已复制的字节数

差值 = 1234600 - 1234567 = 33字节
    ↓
Slave落后33字节

偏移量更新

sequenceDiagram
    participant Client
    participant Master
    participant Slave
    
    Note over Master: offset: 1000
    Note over Slave: offset: 1000
    
    Client->>Master: SET key1 "value1"
    Master->>Master: 执行命令
    Master->>Master: offset += 25(命令字节数)
    Note over Master: offset: 1025
    
    Master->>Slave: 同步命令:SET key1 "value1"
    
    Slave->>Slave: 执行命令
    Slave->>Slave: offset += 25
    Note over Slave: offset: 1025
    
    Note over Master,Slave: 偏移量一致,数据同步

偏移量检查

# Master上查看
redis> INFO replication

master_repl_offset:1234600

slave0:ip=192.168.1.101,port=6380,state=online,offset=1234600,lag=0
slave1:ip=192.168.1.102,port=6381,state=online,offset=1234567,lag=1
                                                    ↑        ↑
                                              Slave1落后  延迟1秒

🛡️ 第四问:复制的风险与优化

风险1:全量复制阻塞

场景:

Master数据量:50GB
    ↓
执行BGSAVE生成RDB
    ↓
fork子进程耗时:5秒(内存越大越慢)
    ↓
RDB生成耗时:30秒
    ↓
网络传输耗时:60秒(50GB)
    ↓
Slave加载耗时:30秒
    ↓
总耗时:2分钟以上!

影响:

  • Master fork时可能有短暂卡顿
  • 网络带宽占用
  • Slave长时间不可用

优化方案:

# 1. 调整fork优化
vm.overcommit_memory = 1
echo never > /sys/kernel/mm/transparent_hugepage/enabled

# 2. 限制BGSAVE频率
repl-diskless-sync-delay 5  # 延迟5秒,合并多个Slave请求

# 3. 无盘复制(Redis 2.8.18+)
repl-diskless-sync yes  # 不生成RDB文件,直接通过网络发送

风险2:复制缓冲区溢出

场景:

复制缓冲区:1MB
Master写入速度:10MB/s
Slave处理速度:5MB/s
    ↓
0.1秒就写满了!
    ↓
旧数据被覆盖
    ↓
Slave的offset在缓冲区中找不到了
    ↓
只能全量复制!

优化:

# 增大复制积压缓冲区
repl-backlog-size 100mb  # 改成100MB

# 计算公式:
# 缓冲区大小 >= 断线时长 × 平均写入速度 × 2

# 例如:
# 可能断线:60秒
# 写入速度:1MB/s
# 缓冲区 >= 60 × 1 × 2 = 120MB

风险3:主从延迟

监控和告警:

@Component
public class ReplMonitor {
    
    @Scheduled(fixedRate = 10000)  // 每10秒检查
    public void checkReplLag() {
        
        // 连接Master
        Properties masterInfo = masterRedis.execute(
            (RedisCallback<Properties>) conn -> conn.info("replication"));
        
        long masterOffset = Long.parseLong(
            masterInfo.getProperty("master_repl_offset"));
        
        // 连接所有Slave
        for (RedisTemplate slaveRedis : slaveRedisList) {
            
            Properties slaveInfo = slaveRedis.execute(
                (RedisCallback<Properties>) conn -> conn.info("replication"));
            
            long slaveOffset = Long.parseLong(
                slaveInfo.getProperty("slave_repl_offset"));
            
            long lag = masterOffset - slaveOffset;
            
            // 延迟超过100KB,告警
            if (lag > 102400) {
                System.err.println("⚠️  主从延迟:" + lag + " 字节");
                // 发送告警...
            }
        }
    }
}

💡 最佳实践

1. 复制配置优化

# redis.conf(Slave配置)

# 1. 复制积压缓冲区大小
repl-backlog-size 100mb

# 2. 缓冲区保留时间(Slave断线后)
repl-backlog-ttl 3600  # 1小时

# 3. 无盘复制(适合网络好但磁盘慢的场景)
repl-diskless-sync yes
repl-diskless-sync-delay 5

# 4. Slave优先级(0表示永不提升为Master)
replica-priority 100

# 5. 最小Slave数量(Master写入要求)
min-replicas-to-write 1        # 至少1个Slave在线才允许写
min-replicas-max-lag 10        # Slave延迟不超过10秒

2. 避免全量复制

触发全量复制的情况:
1. 首次建立主从
2. runid不匹配
3. offset不在缓冲区

避免方法:
1. 增大缓冲区
2. 避免Master重启(会生成新runid)
3. 监控Slave延迟
4. 避免长时间断线

3. 读写分离策略

@Service
public class ReadWriteService {
    
    /**
     * 根据业务场景选择读取源
     */
    public User getUser(Long userId, ScenarioType scenario) {
        
        switch (scenario) {
            case STRONG_CONSISTENCY:
                // 强一致性场景(如支付):读Master
                return getUserFromMaster(userId);
                
            case EVENTUAL_CONSISTENCY:
                // 最终一致性场景(如商品列表):读Slave
                return getUserFromSlave(userId);
                
            case AFTER_WRITE:
                // 写后读场景:优先读Master,1秒后可读Slave
                return getUserAfterWrite(userId);
                
            default:
                return getUserFromSlave(userId);
        }
    }
}

💡 知识点总结

主从同步核心要点

两种复制方式

  • 全量复制:首次或差距太大
  • 增量复制:正常同步或短暂断线

全量复制流程

  1. Slave发送PSYNC ? -1
  2. Master回复FULLRESYNC
  3. Master执行BGSAVE
  4. 发送RDB文件
  5. 发送缓冲区命令
  6. Slave加载RDB和执行命令

增量复制原理

  • 复制积压缓冲区(环形缓冲区)
  • 记录写命令和偏移量
  • Slave重连后从offset继续

复制偏移量

  • Master维护master_repl_offset
  • Slave维护slave_repl_offset
  • 差值表示延迟

一致性保证

  • 不保证强一致
  • 保证最终一致
  • 有主从延迟

风险与优化

  • 增大复制缓冲区
  • 避免全量复制
  • 监控主从延迟
  • WAIT命令等待同步

记忆口诀

主从同步两方式,
全量增量要分清。
首次连接全量传,
RDB文件加命令。
断线重连增量补,
复制缓冲来帮忙。
偏移量记录进度,
Master减Slave是延迟。
不保证强一致性,
最终一致是特点。
写Master读Slave,
延迟标记来优化。

🤔 常见面试题

Q1: Redis主从同步中的增量和全量同步怎么实现?

A:

全量同步:
1. Slave发送PSYNC ? -1
2. Master执行BGSAVE生成RDB
3. RDB生成期间的写命令缓存
4. 发送RDB文件给Slave
5. Slave清空数据,加载RDB
6. 发送缓冲区命令
7. Slave执行命令
8. 全量同步完成

增量同步:
1. Master有写命令
2. 写入复制积压缓冲区(环形,默认1MB)
3. 异步发送给Slave
4. Slave执行命令
5. 更新复制偏移量

判断方式:
- runid不匹配 → 全量
- offset不在缓冲区 → 全量
- 其他 → 增量

Q2: Redis主从和集群可以保证数据一致性吗?

A:

不能保证强一致性,只能保证最终一致性

原因:
1. 异步复制
   - Master写入立即返回
   - 不等Slave同步完成
   
2. 有延迟
   - 网络延迟
   - Slave处理慢
   
3. 可能丢数据
   - Master宕机,Slave还没同步
   - 部分数据丢失

如何提高一致性:
1. WAIT命令等待同步
2. 读写都走Master
3. 增大复制缓冲区
4. 监控延迟

Q3: 主从复制的复制积压缓冲区是什么?

A:

复制积压缓冲区(replication backlog):

作用:
- 记录Master最近的写命令
- 用于增量复制

特点:
- 环形缓冲区(FIFO)
- 默认1MB
- 数据满了覆盖旧数据

工作流程:
1. Master执行写命令
2. 写入缓冲区
3. 发送给Slave
4. Slave断线重连时,从offset位置继续

配置:
repl-backlog-size 10mb  # 大小
repl-backlog-ttl 3600   # 保留时间

计算:
缓冲区大小 >= 断线时长 × 写速度 × 2

💬 写在最后

从全量复制到增量复制,我们深入学习了Redis主从同步机制:

  • 🔄 理解了两种复制方式的触发时机
  • 📊 掌握了复制偏移量的作用
  • ⚠️ 学会了数据一致性问题的解决
  • 💻 完成了实战优化方案

这篇文章,希望能让你彻底搞懂Redis主从同步!

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

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

感谢阅读,期待下次再见! 👋