上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:小亿 发布时间:12小时前 阅读量:218


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

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

测试妹子: “你的接口有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:正常运行中
    → 增量复制(实时同步)

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

完整时序图

SlaveMaster配置:replicaof master-ip master-port1. 发送PSYNC ? -1PSYNC replicationid offset首次复制:? -12. 回复+FULLRESYNC runid: Master的唯一IDoffset: 当前复制偏移量3. 保存Master的runid和offset4. 执行BGSAVE生成RDBfork子进程不阻塞主进程5. RDB生成期间的写命令缓存到复制缓冲区6. 发送RDB文件清空旧数据加载RDB文件7. 发送复制缓冲区的命令执行命令8. 全量复制完成进入增量复制阶段SlaveMaster

详细步骤解析

步骤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. 更新复制偏移量

增量复制时序图

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

判断全量还是增量

阿西噶阿西: “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字节

偏移量更新

ClientMasterSlaveoffset: 1000offset: 1000SET key1 "value1"执行命令offset += 25(命令字节数)offset: 1025同步命令:SET key1 "value1"执行命令offset += 25offset: 1025偏移量一致,数据同步ClientMasterSlave

偏移量检查

# 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频率
rep                                    
目录结构
全文