问题
Redis主从复制的完整过程是怎样的?
答案
1. 核心概念
Redis主从复制(Replication)是将主节点的数据复制到从节点的过程,分为 全量同步 和 增量同步 两种方式。整个过程包括连接建立、数据同步、命令传播、心跳检测等多个阶段。
2. 主从复制完整流程
[从节点] [主节点]
| |
| 阶段1: 建立连接 |
|-- (1) PSYNC replicationId offset ------->|
| |
| 阶段2: 判断同步类型 |
| |
|<-- (2) +FULLRESYNC runId offset ---------| (全量同步)
| 或 |
|<-- (2) +CONTINUE -------------------------| (增量同步)
| |
| 阶段3: 数据同步 |
|<-- (3) RDB快照文件 -----------------------|
|-- (4) 加载RDB到内存 |
|<-- (5) 发送RDB期间的写命令缓冲 -----------|
| |
| 阶段4: 命令传播 |
|<-- (6) 持续接收写命令 ---------------------|
| |
| 阶段5: 心跳检测 |
|-- (7) REPLCONF ACK <offset> ------------->|
| |
3. 各阶段详细解析
阶段1: 建立连接与身份验证
步骤1:从节点发起连接
# 从节点配置
replicaof 192.168.1.100 6379
步骤2:身份验证(如果主节点设置了密码)
# 从节点配置文件
masterauth <password>
步骤3:从节点发送监听端口信息
REPLCONF listening-port 6380
步骤4:从节点发送能力声明
REPLCONF capa eof capa psync2 # 支持PSYNC2协议
步骤5:从节点发送PSYNC命令
# 首次复制
PSYNC ? -1
# 部分复制
PSYNC <replicationId> <offset>
阶段2: 主节点判断同步类型
判断逻辑:
public class SyncTypeDecision {
public String decideSyncType(String replId, long offset) {
// 情况1: 首次复制(replId为?或offset为-1)
if ("?".equals(replId) || offset == -1) {
return "FULLRESYNC"; // 全量同步
}
// 情况2: replId不匹配(主节点重启或从节点连接了新主节点)
if (!replId.equals(currentReplId)) {
return "FULLRESYNC";
}
// 情况3: offset不在复制积压缓冲区范围内
if (offset < replBacklogFirstByte || offset > replBacklogOffset) {
return "FULLRESYNC";
}
// 情况4: offset在复制积压缓冲区范围内
return "CONTINUE"; // 增量同步
}
}
主节点响应:
# 全量同步
+FULLRESYNC <runId> <offset>
# 增量同步
+CONTINUE
阶段3: 数据同步(全量同步)
步骤1:主节点执行BGSAVE
// 主节点伪代码
public class FullSync {
public void execute() {
// 1. fork子进程生成RDB快照
pid_t pid = fork();
if (pid == 0) {
// 子进程执行RDB持久化
rdbSave("/tmp/dump.rdb");
exit(0);
}
// 2. 主进程继续处理写命令,并缓存到复制缓冲区
while (rdbSaving) {
Command cmd = receiveWriteCommand();
replicationBuffer.add(cmd); // 缓存写命令
}
}
}
步骤2:传输RDB文件
主节点 ---> 发送RDB文件 (可能数GB) ---> 从节点
步骤3:从节点清空旧数据并加载RDB
public class SlaveLoadRDB {
public void execute() {
// 1. 清空从节点现有数据
flushAll();
// 2. 加载RDB文件到内存
loadRDB("/tmp/dump.rdb");
// 3. 更新复制偏移量和复制ID
this.replOffset = masterReplOffset;
this.replId = masterReplId;
}
}
步骤4:发送BGSAVE期间的写命令
主节点 ---> 发送复制缓冲区中的命令 ---> 从节点执行
阶段3: 数据同步(增量同步)
触发条件:从节点短暂断开后重连,且复制偏移量仍在复制积压缓冲区范围内。
执行流程:
public class PartialSync {
public void execute(long slaveOffset) {
// 1. 从复制积压缓冲区中提取缺失的命令
List<Command> commands = extractCommands(slaveOffset, currentOffset);
// 2. 发送给从节点
for (Command cmd : commands) {
sendToSlave(cmd);
}
// 3. 从节点执行命令
slave.executeCommands(commands);
}
private List<Command> extractCommands(long startOffset, long endOffset) {
// 从环形缓冲区中按偏移量提取命令
return replBacklogBuffer.getRange(startOffset, endOffset);
}
}
优势:
- 无需传输RDB文件,速度快
- 对主节点性能影响小(无需BGSAVE)
阶段4: 命令传播(持续同步)
同步完成后的正常运行阶段:
主节点操作:
public class CommandPropagation {
public void propagateCommand(Command cmd) {
// 1. 主节点执行写命令
executeCommand(cmd);
// 2. 将命令发送给所有从节点
for (Slave slave : connectedSlaves) {
slave.sendCommand(cmd);
}
// 3. 写入复制积压缓冲区
replBacklogBuffer.append(cmd);
// 4. 更新复制偏移量
this.replOffset += cmd.getBytes().length;
}
}
从节点操作:
public class SlaveExecuteCommand {
public void execute(Command cmd) {
// 1. 执行接收到的命令
executeCommand(cmd);
// 2. 更新复制偏移量
this.replOffset += cmd.getBytes().length;
}
}
阶段5: 心跳检测与延迟监控
从节点定期发送心跳:
# 从节点每秒发送一次(默认1秒)
REPLCONF ACK <offset>
心跳的三大作用:
1. 检测主从网络状态
// 主节点检测从节点是否在线
public boolean isSlaveAlive(Slave slave) {
long lastAck = slave.getLastAckTime();
long timeout = System.currentTimeMillis() - lastAck;
return timeout < repl_timeout; // 默认60秒
}
2. 汇报复制偏移量
// 主节点计算主从延迟
public long calculateLag(Slave slave) {
return this.replOffset - slave.getReplOffset();
}
3. 实现min-replicas机制
# 主节点配置:至少1个从节点延迟<10秒才允许写入
min-replicas-to-write 1
min-replicas-max-lag 10
// 主节点写入前检查
public boolean canWrite() {
int validSlaves = 0;
for (Slave slave : connectedSlaves) {
long lag = (System.currentTimeMillis() - slave.getLastAckTime()) / 1000;
if (lag <= minReplicasMaxLag) {
validSlaves++;
}
}
return validSlaves >= minReplicasToWrite;
}
4. 关键机制详解
4.1 复制偏移量(Replication Offset)
主节点偏移量:
redis-cli INFO replication | grep master_repl_offset
# 输出: master_repl_offset:123456
从节点偏移量:
redis-cli INFO replication | grep slave_repl_offset
# 输出: slave0:offset=123450,lag=0
偏移量作用:
- 判断主从数据是否一致(差值为0表示同步)
- 决定是否可以进行增量同步
4.2 复制积压缓冲区(Replication Backlog)
环形缓冲区结构:
+---+---+---+---+---+---+---+---+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | (固定大小,如1MB)
+---+---+---+---+---+---+---+---+
^ ^
oldest_offset newest_offset
配置参数:
# 缓冲区大小(默认1MB)
repl-backlog-size 1mb
# 主节点无从节点连接后,保留缓冲区的时间(默认1小时)
repl-backlog-ttl 3600
大小设置建议:
repl-backlog-size = 平均写入速率 × 2 × 预计断线时间
例如:
- 平均写入速率: 100KB/s
- 预计断线时间: 60秒
- 计算: 100KB/s × 2 × 60s = 12MB
4.3 无盘复制(Diskless Replication)
传统方式:主节点 → 磁盘RDB → 网络 → 从节点
无盘复制:主节点 → 直接通过网络发送 → 从节点
配置开启:
# 开启无盘复制
repl-diskless-sync yes
# 延迟开始时间(等待更多从节点连接,批量传输)
repl-diskless-sync-delay 5
优势:
- 节省磁盘IO,适合磁盘慢但网络快的场景
- 多个从节点可同时接收数据
5. 异常场景处理
5.1 从节点断线重连
场景:从节点断开30秒后重连
处理流程:
1. 从节点发送 PSYNC <replId> <offset>
2. 主节点检查offset是否在复制积压缓冲区
3. 若在范围内 → 增量同步
4. 若超出范围 → 全量同步
5.2 主节点重启
场景:主节点重启后replId改变
处理流程:
1. 从节点发送 PSYNC <old_replId> <offset>
2. 主节点检测到replId不匹配
3. 返回 +FULLRESYNC <new_replId> <new_offset>
4. 执行全量同步
5.3 从节点延迟过大
监控命令:
redis-cli INFO replication | grep lag
# 输出: slave0:lag=15 (延迟15秒)
原因分析:
- 主节点写入压力大
- 从节点性能不足(慢查询、持久化阻塞)
- 网络带宽不足
解决方案:
- 关闭从节点的AOF持久化(
appendonly no) - 优化从节点慢查询
- 增加从节点硬件配置
6. 面试答题总结
标准回答模板:
Redis主从复制分为五个阶段:
- 建立连接:从节点发送
PSYNC <replId> <offset>命令到主节点- 判断同步类型:主节点根据replId和offset判断全量同步(+FULLRESYNC)或增量同步(+CONTINUE)
- 数据同步:
- 全量同步:主节点BGSAVE生成RDB → 发送RDB → 发送缓冲区命令
- 增量同步:主节点从复制积压缓冲区发送缺失命令
- 命令传播:主节点持续将写命令发送给从节点
- 心跳检测:从节点每秒发送
REPLCONF ACK <offset>,汇报复制进度核心机制:
- 复制偏移量:判断主从数据一致性
- 复制积压缓冲区:支持增量同步(默认1MB,建议10MB+)
- 无盘复制:适合磁盘慢网络快的场景(repl-diskless-sync yes)
优化建议:
- 合理配置
repl-backlog-size,减少全量同步频率- 从节点关闭AOF持久化,降低IO压力
- 配置
min-replicas-to-write保证数据安全
常见追问:
- BGSAVE期间主节点崩溃怎么办? → 从节点会重新发起PSYNC,主节点重新执行全量同步
- 复制积压缓冲区满了会怎样? → 旧数据被覆盖,从节点重连时offset超出范围,触发全量同步
- 主从复制是同步还是异步? → 异步复制,主节点不等待从节点确认即返回(可能丢数据,但性能高)