💥 开场:一次"诡异"的数据不一致
时间: 周四晚上
地点: 公司(加班中)
事件: 数据异常
测试妹子: “你的接口有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:正常运行中
→ 增量复制(实时同步)
📦 第二问:全量复制详细流程
完整时序图
详细步骤解析
步骤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. 更新复制偏移量
增量复制时序图
判断全量还是增量
阿西噶阿西: “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字节
偏移量更新
偏移量检查
# 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