难度:⭐⭐⭐⭐⋆ | 适合人群:想深入理解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);
}
}
}
💡 知识点总结
主从同步核心要点
✅ 两种复制方式
- 全量复制:首次或差距太大
- 增量复制:正常同步或短暂断线
✅ 全量复制流程
- Slave发送PSYNC ? -1
- Master回复FULLRESYNC
- Master执行BGSAVE
- 发送RDB文件
- 发送缓冲区命令
- 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主从同步!
如果这篇文章对你有帮助,请:
- 👍 点赞支持
- ⭐ 收藏备用
- 🔄 转发分享
- 💬 评论交流
感谢阅读,期待下次再见! 👋