Redis 主从复制详解:全量同步与增量同步
如果把 Redis 比作一个图书馆,那么主从复制就像是在不同地方建立了分馆。主馆(Master)负责收录新书,分馆(Slave)自动同步主馆的藏书。这样即使主馆失火,分馆还能继续提供服务。本文将带你深入理解 Redis 主从复制的奥秘。
📖 目录
- 为什么需要主从复制?
- [5.1 主从复制概述](#5.1 主从复制概述 "#51-%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E6%A6%82%E8%BF%B0")
- [5.2 复制流程详解](#5.2 复制流程详解 "#52-%E5%A4%8D%E5%88%B6%E6%B5%81%E7%A8%8B%E8%AF%A6%E8%A7%A3")
- [5.3 PSYNC 命令](#5.3 PSYNC 命令 "#53-psync-%E5%91%BD%E4%BB%A4")
- [PSYNC 演进历史](#PSYNC 演进历史 "#psync-%E6%BC%94%E8%BF%9B%E5%8E%86%E5%8F%B2")
- [PSYNC2 的智慧](#PSYNC2 的智慧 "#psync2-%E7%9A%84%E6%99%BA%E6%85%A7")
- 复制偏移量:数据的身份证
- [5.4 无磁盘复制](#5.4 无磁盘复制 "#54-%E6%97%A0%E7%A3%81%E7%9B%98%E5%A4%8D%E5%88%B6")
- [5.5 部分重同步](#5.5 部分重同步 "#55-%E9%83%A8%E5%88%86%E9%87%8D%E5%90%8C%E6%AD%A5")
- [5.6 主从延迟](#5.6 主从延迟 "#56-%E4%B8%BB%E4%BB%8E%E5%BB%B6%E8%BF%9F")
- [5.7 级联复制](#5.7 级联复制 "#57-%E7%BA%A7%E8%81%94%E5%A4%8D%E5%88%B6")
- [5.8 读写分离](#5.8 读写分离 "#58-%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB")
- 生产环境配置
- 常见问题解答
为什么需要主从复制?
单机 Redis 的困境
想象一下,你开了一家餐厅,只有一个厨师。如果这个厨师生病了,餐厅就得关门;客人太多,厨师忙不过来,就得排长队。这就是单机 Redis 的问题:
markdown
┌─────────────────────────┐
│ Redis 单机 │
│ • 单点故障 💥 │
│ • 容量受限 📦 │
│ • QPS 受限 🐌 │
└─────────────────────────┘
↓ 宕机
💥 服务不可用
💥 数据全部丢失
💥 用户流失
主从复制:开分店的智慧
主从复制就像给餐厅开分店:
🏪 总店(Master):
• 研发新菜品(处理写请求)
• 也可以接待客人(处理读请求)
• 把新菜谱发给分店(数据同步)
🏪 分店(Slave):
• 接待客人(处理读请求)
• 学习总店的新菜品(数据同步)
• 总店关门时可以顶上(故障转移)
带来的好处:
- ✅ 数据冗余:总店失火,分店还有备份菜谱
- ✅ 读写分离:总店做菜,分店接客,效率翻倍
- ✅ 故障恢复:总店关门,分店立即顶上
- ✅ 负载均衡:客人分流到多个分店
- ✅ 高可用基础:哨兵和集群的基石
性能对比:
单店模式(单机):
• 做菜能力:5 万道/秒
• 接客能力:5 万人/秒
• 总服务能力:5 万/秒
1 总店 + 3 分店:
• 做菜能力:5 万道/秒(总店)
• 接客能力:15 万人/秒(3 个分店)
• 总服务能力:20 万/秒(提升 4 倍!)
5.1 主从复制概述
主从复制架构
yaml
┌────────────────────────────────────────┐
│ Redis 主从架构:总分店模式 │
└────────────────────────────────────────┘
写请求(新菜品)
↓
┌──────────┐
│ Master │ 总店(研发新菜 + 接客)
│ 6379 │
└────┬─────┘
│ 📋 菜谱同步
┌──┴──┬──────┐
↓ ↓ ↓
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Slave1 │ │ Slave2 │ │ Slave3 │
│ 东城分店 │ │ 西城分店 │ │ 南城分店 │
│ 6380 │ │ 6381 │ │ 6382 │
└──────────┘ └──────────┘ └──────────┘
↑ ↑ ↑
└────────────┴────────────┘
读请求(客人点餐)
特点:
• 总店:可以研发新菜(写),也可以接客(读)
• 分店:只能接客(读),不能研发新菜
• 菜谱流向:总店 → 分店(单向)
• 异步同步:总店不等分店确认收到
核心概念
1️⃣ 主节点(Master)- 总店店长
bash
# 总店的职责
• 处理所有"新菜研发"(写请求)
• 也可以"接待客人"(读请求)
• 把新菜谱同步给分店(数据同步)
• 记录菜谱版本号(复制偏移量)
# 查看总店信息
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:3
slave0:ip=127.0.0.1,port=6380,state=online,offset=1234,lag=0 ✅ 东城分店同步正常
slave1:ip=127.0.0.1,port=6381,state=online,offset=1234,lag=1 ⚠️ 西城分店延迟1秒
slave2:ip=127.0.0.1,port=6382,state=online,offset=1234,lag=0 ✅ 南城分店同步正常
2️⃣ 从节点(Slave/Replica)- 分店店长
bash
# 分店的职责
• 只能"接待客人"(读请求)
• 不能"研发新菜"(写请求会报错)
• 学习总店的新菜谱(数据同步)
• 也可以开自己的分店(级联复制)
# 查看分店信息
127.0.0.1:6380> INFO replication
# Replication
role:slave # 我是分店
master_host:127.0.0.1 # 总店地址
master_port:6379 # 总店电话
master_link_status:up # ✅ 和总店联系正常
master_last_io_seconds_ago:0 # 0 秒前刚通过话
master_sync_in_progress:0 # 未在同步中
slave_repl_offset:1234 # 已学到第 1234 道菜
slave_read_only:1 # 🔒 只读模式
3️⃣ 复制 ID(Replication ID)- 菜谱版本号
bash
# 每个 Redis 实例都有唯一的"菜谱版本号"
master_replid:8e3c9d4f2a1b5c6d7e8f9a0b1c2d3e4f5a6b7c8d
# 就像书籍的 ISBN 号
• 标识这是哪个版本的数据集
• 总店换老板(主从切换)会生成新版本号
• 用于判断分店的菜谱是否过时
4️⃣ 复制偏移量(Replication Offset)- 学习进度
bash
# 总店的菜谱进度
master_repl_offset:1234567 # 已研发到第 1234567 道菜
# 分店的学习进度
slave_repl_offset:1234567 # 已学到第 1234567 道菜
# 进度一致 = 菜谱同步
# 进度相差 = 还有新菜没学会(延迟)
应用场景
1️⃣ 读写分离 - 总店做菜,分店接客
java
// 写操作(研发新菜)→ 总店
@Autowired
private RedisTemplate<String, String> masterRedis;
public void createNewDish(String dishId, String recipe) {
// 新菜只能在总店研发
masterRedis.opsForValue().set("dish:" + dishId, recipe);
}
// 读操作(客人点餐)→ 分店
@Autowired
private RedisTemplate<String, String> slaveRedis;
public String getDish(String dishId) {
// 客人可以在任意分店点餐
return slaveRedis.opsForValue().get("dish:" + dishId);
}
2️⃣ 故障恢复 - 分店顶替总店
bash
# 场景:总店突然停电
Master (6379) 💥 Down
# 应急方案:东城分店临时升级为总店
Slave1 (6380) → 升级为 Master
# 其他分店改为跟随新总店
Slave2, Slave3 → 跟随新总店 (6380)
# 服务不中断!客人甚至没察觉 ✅
3️⃣ 数据备份 - 专职备份员
bash
# 安排一个分店专门做备份(不接客)
Master (6379) → Slave-Backup (6390)
↓
每天凌晨打包菜谱(BGSAVE)
↓
backup/recipes-20251026.rdb
4️⃣ 负载均衡 - 客人分流
scss
客人来了,去哪个分店?
↓
负载均衡器(前台)
├─→ 东城分店 (33% 客人)
├─→ 西城分店 (33% 客人)
└─→ 南城分店 (34% 客人)
每个分店压力均衡 ✅
5.2 复制流程详解
建立连接:从相识到相知
分店要加盟总店,需要经历一系列"认亲"过程:
sql
┌────────────────────────────────────────┐
│ 分店加盟流程:7 步握手 │
└────────────────────────────────────────┘
Slave (分店) Master (总店)
│ │
│─── ① 表明身份 ───────────→│
│ "我想加盟您!" │
│ replicaof 127.0.0.1 6379│
│ │
│─── ② 打个招呼 ───────────→│
│ PING │
│ │
│←── ③ 回应 ───────────────│
│ PONG "欢迎欢迎!" │
│ │
│─── ④ 验证身份 ───────────→│
│ AUTH password │
│ │
│←── ⑤ 身份确认 ───────────│
│ OK "身份验证通过" │
│ │
│─── ⑥ 报告地址 ───────────→│
│ REPLCONF listening-port │
│ REPLCONF ip-address │
│ │
│─── ⑦ 请求菜谱 ───────────→│
│ PSYNC replid offset │
│ │
│←── ⑧ 开始传授 ───────────│
│ +FULLRESYNC 或 +CONTINUE│
配置分店:
bash
# 方式 1:写入分店合同(永久有效)
# redis.conf
replicaof 127.0.0.1 6379 # 认 127.0.0.1:6379 为总店
masterauth <password> # 总店的暗号
# 方式 2:口头约定(临时的)
127.0.0.1:6380> REPLICAOF 127.0.0.1 6379
OK
# 分店想独立(取消加盟)
127.0.0.1:6380> REPLICAOF NO ONE
OK # 我要自己当老板!
全量同步:打包发货
第一次加盟或中断太久,总店需要把所有菜谱打包发给分店:
perl
┌────────────────────────────────────────┐
│ 全量同步:菜谱大全发货流程 │
└────────────────────────────────────────┘
Slave (分店) Master (总店)
│ │
│─── "老板,给我全套菜谱" ──→│
│ PSYNC ? -1 │
│ │
│ │ ① "好的,我来整理"
│ │ fork 子进程
│ │ 开始打包(BGSAVE)
│ │
│←── "稍等,正在打包" ──────│
│ +FULLRESYNC replid offs │
│ │
│ │ ② 打包期间
│ │ 新菜谱先记小本上
│ │ (复制缓冲区)
│ │
│←── 📦 "菜谱大全" ────────│ ③ 快递发货
│ (RDB 文件) │ (网络传输)
│ │
│ ④ "收到!先清空旧菜谱" │
│ FLUSHALL │
│ │
│ ⑤ "开始学习新菜谱" │
│ 加载 RDB │
│ │
│←── 📝 "这是期间的新菜" ──│ ⑥ 补发小本上的
│ (复制缓冲区的命令) │ 新菜谱
│ │
│ ⑦ "学会了!" │
│ 执行增量命令 │
│ │
│ ✅ 同步完成,开始营业! │
全量同步的代价(就像搬家):
bash
# 对于 10GB 的数据
总店(Master)的工作量:
• 整理菜谱:30 秒(CPU 忙)
• 打包装箱:10 秒(磁盘 IO)
• 快递发货:60 秒(网络 IO)
分店(Slave)的工作量:
• 接收快递:60 秒(网络 IO)
• 清空旧物:5 秒(FLUSHALL)
• 整理新物:30 秒(加载 RDB)
总搬家时间:约 2 分钟
性能优化技巧:
c
// 总店很忙,多个分店同时要菜谱怎么办?
void syncCommand(client *c) {
// 聪明的做法:
// 如果已经在打包了,新分店也等这一份
if (server.rdb_child_pid != -1) {
// "已经在打包了,你也等着吧"
listAddNodeTail(server.slaves_waiting_bgsave, c);
} else {
// "我现在开始打包"
startBgsaveForReplication();
}
}
// 一次打包,多个分店共享 ✅
// 节省资源,聪明!
增量同步:实时更新
菜谱同步完成后,总店每研发一道新菜,立即通知分店:
bash
┌────────────────────────────────────────┐
│ 增量同步:新菜实时推送 │
└────────────────────────────────────────┘
Master (总店) Slave (分店)
│ │
│ ① "研发新菜:宫保鸡丁" │
│ SET dish:001 "宫保鸡丁" │
│ │
│ ② 记录到菜谱本 │
│ (执行命令) │
│ │
│ ③ 记录到备忘录 │
│ (写 AOF) │
│ │
│ ④ 通知所有分店 ──────────→│
│ *3\r\n$3\r\nSET... │
│ │
│ │ ⑤ "学会了!"
│ │ (执行命令)
│ │
│ │ ⑥ 更新学习进度
│ │ (更新偏移量)
新菜同步完成!分店可以卖这道菜了 ✅
命令传播实现:
c
// 总店群发通知
void replicationFeedSlaves(list *slaves, int dictid, robj **argv, int argc) {
// "各位分店听好了,新菜来了!"
listIter li;
listNode *ln;
// 逐个通知每个分店
listRewind(slaves, &li);
while((ln = listNext(&li))) {
client *slave = ln->value;
// 把新菜谱编码成"暗语"(RESP 协议)
addReplyArrayLen(slave, argc);
for (int j = 0; j < argc; j++) {
addReplyBulk(slave, argv[j]);
}
}
}
心跳机制:互报平安
总店和分店每秒通个电话,确认对方还在:
arduino
┌────────────────────────────────────────┐
│ 心跳机制:每秒一个电话 │
└────────────────────────────────────────┘
Slave (分店) Master (总店)
│ │
│──┐ "报告!" │
│ │ 每秒一次 │
│ │ REPLCONF ACK 1234 ────→│ "我已学到第 1234 道菜"
│←─┘ │
│ │
│ │ 检查:
│ │ ✅ 分店还活着吗?
│ │ ✅ 学到哪道菜了?
│ │ ✅ 电话延迟多少?
│ │
│ 如果 60 秒没接到电话 │
│ "这个分店可能倒闭了" │
│ 断开连接 ❌ │
心跳的妙用:
- 保持联系:确认分店还在营业
- 汇报进度:分店告诉总店学到哪了
- 测量延迟:计算总分店之间的"电话延迟"
- 质量控制:检查是否有足够的合格分店
配置:
bash
# redis.conf
# 总店:多久收不到电话就认为分店倒闭
repl-timeout 60 # 默认 60 秒
# 分店:每秒打一次电话
# (这是硬性规定,不能改)
# 质量控制:至少要有几个合格分店
min-replicas-to-write 1 # 至少 1 个分店
min-replicas-max-lag 10 # 延迟不超过 10 秒
# 如果不达标,总店停止研发新菜(拒绝写入)
5.3 PSYNC 命令
PSYNC 演进历史
PSYNC 就像菜谱传授的"武林秘籍",经历了三代演进:
arduino
第一代(Redis 2.6 前):SYNC - 笨方法
分店:"老板,我想学菜"
总店:"好,我把所有菜谱从头教你"
(全量同步,即使只缺一道菜也要全部重学)
缺点:
• 分店电话断了重连,还要全部重学
• 浪费时间和资源
• 就像忘记一个单词,要把整本字典重抄一遍
第二代(Redis 2.8):PSYNC - 聪明了
分店:"老板,我学到第 1000 道菜了"
总店:"好,我看看你缺哪些,只教你缺的"
(部分重同步,只补缺的菜谱)
优点:
• 断线重连只需补缺
• 节省资源
缺点:
• 总店换老板(主从切换)还是要全部重学
第三代(Redis 4.0):PSYNC2 - 最聪明
分店:"老板,我学的是上一任老板的菜谱第 1000 道"
总店:"没事,我记得上一任老板的菜谱,我看看你缺哪些"
(即使换老板,也能部分重同步)
优点:
• 即使总店换老板也能智能补缺
• 最大化避免全量同步
PSYNC 命令格式
bash
# 格式:我学到哪了?
PSYNC <菜谱版本号> <学习进度>
# 第一次加盟(啥都不知道)
PSYNC ? -1
# 菜谱版本号:?(不知道)
# 学习进度:-1(一道菜都没学过)
# 断线重连(知道自己学到哪了)
PSYNC 8e3c9d4f2a1b5c6d 1234567
# 菜谱版本号:8e3c9d4f...(上次的版本)
# 学习进度:1234567(学到第 1234567 道菜)
总店的回应:
bash
# 回应 1:"太久没联系,全部重学吧"
+FULLRESYNC <新版本号> <起始进度>
# 分店需要清空旧菜谱,接收完整的菜谱大全
# 回应 2:"没事,我看看你缺哪些"
+CONTINUE [版本号]
# 分店继续接收缺的菜谱即可
# 回应 3:"出错了"
-ERR 菜谱版本对不上
PSYNC2 的智慧
Redis 4.0 的 PSYNC2 就像是总店有了"双重记忆":
场景重现:
ruby
初始状态:张老板当总店老板
Master (张老板, 版本:aaa, 进度:1000)
↓ 菜谱同步
Slave (学徒, 版本:aaa, 进度:1000)
突发事件:张老板退休
Master 💥 张老板退休
学徒晋升:学徒变老板
Slave → Master (李老板, 版本:bbb, 进度:1000)
↑ 换老板了,菜谱版本号变了
新学徒加盟:
NewSlave: "李老板,我之前跟张老板学到第 900 道菜"
NewSlave → PSYNC aaa 900
↑ 报的是张老板的版本号
旧方案的问题:
李老板:"我的版本号是 bbb,不是 aaa"
李老板:"对不上,重新全部学一遍吧"
→ 全量同步 ❌(浪费!明明只差 100 道菜)
PSYNC2 的智慧:
李老板:"我记得张老板(前任)的菜谱版本"
李老板:"版本 aaa 是前任的,你学到 900,我现在进度 1000"
李老板:"来,我只教你第 901-1000 道菜"
→ 部分重同步 ✅(聪明!)
实现原理:
c
// 总店记住两任老板的信息
struct redisServer {
char replid[41]; // 现任老板的版本号
char replid2[41]; // 前任老板的版本号(PSYNC2 新增)
long long master_repl_offset; // 现在的进度
long long second_replid_offset; // 前任交接时的进度
};
// 新学徒请求同步时
if (replid == replid2 && offset >= second_replid_offset) {
// "你报的是前任老板的版本,没关系,我记得!"
return CONTINUE; // 部分重同步
}
复制偏移量:数据的身份证
复制偏移量就像菜谱的页码,精确标识学到哪一页了:
scss
┌────────────────────────────────────────┐
│ 菜谱学习进度对照表 │
└────────────────────────────────────────┘
Master (总店)
• master_repl_offset: 1234567
"我的菜谱已经写到第 1234567 页了"
Slave1 (东城分店)
• slave_repl_offset: 1234567 ✅
"我也学到第 1234567 页,完全同步!"
Slave2 (西城分店)
• slave_repl_offset: 1234500 ⚠️
"我才学到第 1234500 页,落后 67 页"
Slave3 (南城分店)
• slave_repl_offset: 1233000 ❌
"我学到第 1233000 页,落后 1567 页!"
5.4 无磁盘复制
直接快递 vs 先存仓库
想象一下,总店要给分店发菜谱:
lua
传统复制(Disk-based)- 先存仓库:
总店老板:
① "我先把菜谱整理好"(遍历数据)
② "存到仓库"(写 dump.rdb 到磁盘)
③"从仓库取出来"(读 dump.rdb)
④ "快递发货"(网络传输)
耗时统计:
• 整理:30 秒
• 存仓库:10 秒 ← 多余的步骤
• 从仓库取:5 秒 ← 多余的步骤
• 快递:60 秒
总计:105 秒
无磁盘复制(Diskless)- 直接快递:
总店老板:
① "我一边整理,一边就发给你"(边遍历边发送)
(跳过存仓库的步骤)
耗时统计:
• 边整理边发:90 秒
总计:90 秒(省了 15 秒!)
流程对比图:
markdown
┌────────────────────────────────────────┐
│ 传统 vs 无磁盘:快递方式对比 │
└────────────────────────────────────────┘
传统快递(绕路):
菜谱 → 整理 → 📦 仓库 → 取出 → 🚚 快递 → 分店
↑ 多余的中转
直接快递(直达):
菜谱 → 整理 ────────────→ 🚚 快递 → 分店
↑ 一气呵成,无中转
配置与使用
bash
# redis.conf
# 开启"直达快递"模式
repl-diskless-sync yes # 默认 no(用仓库)
# 等快递员(等待更多分店一起发货)
repl-diskless-sync-delay 5 # 等 5 秒
# 分店也用"直达收货"(Redis 6.0+)
repl-diskless-load swapdb
# on-empty-db: 只有空店时用
# disabled: 不用
# swapdb: 边收边换(推荐)
"拼单发货"的智慧:
makefile
时间线:
00:00 分店1:"老板,我要菜谱"
总店:"好,我等 5 秒,看还有没有其他分店"
00:02 分店2:"老板,我也要"
总店:"好,一起等"
00:04 分店3:"老板,我也要"
总店:"好,马上时间到了"
00:05 时间到!
总店:一次整理,同时发给 3 个分店
↓
节省资源(只整理一次,像拼单快递)✅
适用场景
arduino
✅ 适合"直达快递":
• SSD 硬盘(怕磨损)
• 磁盘 IO 慢(仓库太慢)
• 网络很快(快递很快)
• 多个分店同时要(拼单划算)
❌ 不适合"直达快递":
• 机械硬盘(仓库还行)
• 网络很慢(快递太慢)
• 需要存档(要留仓库备份)
• 分店请求分散(拼不了单)
5.5 部分重同步
复制积压缓冲区:备忘录
复制积压缓冲区就像总店老板的"备忘录",记录最近研发的新菜:
yaml
┌────────────────────────────────────────┐
│ 总店老板的备忘录(环形笔记本) │
└────────────────────────────────────────┘
Master 的备忘录(默认 1MB)
┌──────────────────────────────────┐
│ 📝 最近研发的新菜(循环覆盖) │
├──────────────────────────────────┤
│ 第 1000 道:宫保鸡丁 │
│ 第 1001 道:鱼香肉丝 │
│ 第 1002 道:麻婆豆腐 │ ← 当前写到这
│ 第 1003 道:... │
│ ... │
│ (只记录最近的,超过 1MB 就覆盖) │
└──────────────────────────────────┘
↑ ↑
最老的记录 最新的记录
作用:
• 分店断线重连,只需补最近缺的菜
• 就像课堂笔记,方便复习
工作原理:
c
// 环形笔记本
void feedReplicationBacklog(void *ptr, size_t len) {
// 像记录仪一样,循环覆盖
while(len) {
// 写到笔记本末尾了?
if (server.repl_backlog_idx == server.repl_backlog_size) {
server.repl_backlog_idx = 0; // 回到开头继续写
}
// 记录新菜谱
memcpy(server.repl_backlog + server.repl_backlog_idx,
ptr, thislen);
server.repl_backlog_idx += thislen;
server.repl_backlog_histlen += thislen;
}
}
// 就像行车记录仪
// 内存满了从头覆盖
// 只保留最近的记录
触发条件
分店断线重连,什么时候能"只补缺的菜谱"?
arduino
分店:"老板,我之前学到第 900 道菜"
总店检查备忘录:
"让我看看我的备忘录..."
┌──────────────────────────────┐
│ 备忘录范围:第 800-1200 道 │
│ 分店进度:第 900 道 │
│ 判断:900 在 [800, 1200] ✅ │
└──────────────────────────────┘
总店:"在备忘录范围内,我只教你第 901-1200 道"
→ 部分重同步 ✅
如果分店说:
"我学到第 500 道菜"
总店:"我的备忘录只记录到第 800 道,500 太久远了"
总店:"你的菜谱太旧了,全部重学吧"
→ 全量同步 ❌
判断公式:
c
// 能否部分重同步?
bool can_partial_resync =
(slave_offset >= backlog_first_offset) && // 不太旧
(slave_offset <= master_offset); // 不超前
// 用白话说就是:
// "你的进度在我的备忘录记录范围内吗?"
优化策略
1️⃣ 买个大点的备忘录
bash
# redis.conf
# 默认备忘录只有 1MB(记不了多少)
repl-backlog-size 1mb
# 增大备忘录
repl-backlog-size 64mb # 扩大到 64MB
# 怎么算合适的大小?
# 备忘录大小 = 每秒新菜数量 × 预计断线时间
# 例如:
# 每秒研发:1MB 新菜
# 可能断线:60 秒
# 建议大小:1MB × 60 = 60MB
2️⃣ 多给点时间
bash
# 电话多久不通就认为分店倒闭?
repl-timeout 60 # 默认 60 秒
# 网络不稳定时,可以放宽
repl-timeout 120 # 120 秒(给分店更多时间)
3️⃣ 保留备忘录
bash
# 所有分店都不干了,备忘录保留多久?
repl-backlog-ttl 3600 # 默认保留 1 小时
# 永远保留(万一分店想回来呢)
repl-backlog-ttl 0
5.6 主从延迟
延迟产生原因
主从同步不是瞬间完成的,就像快递需要时间:
scss
┌────────────────────────────────────────┐
│ 数据同步的"快递过程" │
└────────────────────────────────────────┘
Master (总店) Slave (分店)
│ │
│ T0: "研发新菜:水煮鱼" │
│ (执行写命令) │
│ │
│ T1: "发快递" ───────────→ │
│ (网络传输) │ 🚚 在路上...
│ │
│ │ T2: "收到快递"
│ │ (接收数据)
│ │
│ │ T3: "学会了"
│ │ (执行命令)
│ │
│ 延迟 = T3 - T0 = 150ms │
延迟的"罪魁祸首":
1️⃣ 快递路上堵车(网络延迟)
• 总店在北京,分店在上海(物理距离)
• 网络拥堵(高峰期)
• 网线老化(带宽不足)
解决:换专线、升级带宽
2️⃣ 总店太忙(主节点负载高)
vbnet
• 客人太多,订单爆满(写入 QPS 高)
• 有人点了满汉全席(大 Key 操作)
• 老板在数钞票(KEYS * 慢查询)
解决:避免慢命令、控制写入速率
3️⃣ 分店太忙(从节点负载高)
• 客人爆满(读 QPS 高)
• 厨师累了(CPU 不足)
• 冰箱坏了(磁盘 IO 慢)
解决:增加服务器资源、关闭持久化
4️⃣ 快递包裹太大(大数据传输)
• 一次发 1 吨菜谱(BigKey 写入)
• 装车就要 10 分钟(网络传输慢)
解决:避免 BigKey、拆分数据
监控延迟
就像监控快递进度:
bash
# 1. 查看总店的发货记录
127.0.0.1:6379> INFO replication
slave0:ip=127.0.0.1,port=6380,state=online,offset=1234,lag=1
↑
延迟 1 秒(快递在路上)
# lag 含义(快递状态):
# • 0 秒: ✅ 已签收(实时同步)
# • 1-5 秒:📦 在路上(正常延迟)
# • >10秒: ⚠️ 堵车了(延迟较大)
# • -1: 💥 快递丢了(连接断开)
# 2. 查看具体差了多少页
master_repl_offset:1234567 # 总店写到第 1234567 页
slave_repl_offset:1234000 # 分店学到第 1234000 页
# 差值:567 页(还有 567 页没学会)
Java 监控示例(像快递追踪系统):
java
public class ReplicationMonitor {
/**
* 监控快递进度(主从延迟)
*/
public void trackDelivery() {
Jedis master = new Jedis("127.0.0.1", 6379);
String info = master.info("replication");
// 解析延迟
Pattern pattern = Pattern.compile("lag=(\\d+)");
Matcher matcher = pattern.matcher(info);
while (matcher.find()) {
int lag = Integer.parseInt(matcher.group(1));
if (lag > 10) {
// 📢 告警:快递严重延迟!
alert("🚨 分店菜谱延迟 " + lag + " 秒!客人可能点不到新菜!");
} else if (lag > 5) {
// ⚠️ 警告:延迟有点高
warn("⚠️ 分店菜谱延迟 " + lag + " 秒,需关注");
}
}
}
}
减少延迟方法
1️⃣ 升级快递方式(优化网络)
bash
# 从"普通快递"升级到"同城速递"
• 总店和分店开在同一条街(同机房)
• 使用专车配送(专用网络)
• 升级到顺丰(1Gbps → 10Gbps 带宽)
# 配置优化
repl-disable-tcp-nodelay no
# 禁用 Nagle 算法(小包裹也立即发,不等拼单)
# 延迟更低 ✅
2️⃣ 总店别太忙(优化主节点)
bash
# 让总店专心做菜
• 不要数钞票(避免 KEYS *)
• 不要清理仓库(避免 SMEMBERS bigset)
• 控制接单速度(限流)
• 不做超大订单(避免 BigKey)
# 实战建议
KEYS * → SCAN 0 MATCH pattern # 边数边做事
DEL bigkey → UNLINK bigkey # 交给保洁阿姨(异步删除)
3️⃣ 分店轻装上阵(优化从节点)
bash
# 分店可以不记账(关闭持久化)
save "" # 不做 RDB
appendonly no # 不写日志
# 理由:
# • 总店有账本就够了
# • 分店省时间,服务更多客人
# • 分店挂了大不了重新加盟
# 增加硬件
• 更快的厨师(CPU)
• 更大的厨房(内存)
• 更快的网线(网络)
4️⃣ 质量管控(监控告警)
bash
# 设置"分店合格标准"
min-replicas-to-write 1 # 至少 1 个分店正常
min-replicas-max-lag 10 # 延迟不超过 10 秒
# 不达标?停止接单!
# "分店都跟不上了,我先缓缓"
# 主节点拒绝写入,防止延迟继续扩大 ✅
5.7 级联复制
分级管理的艺术
级联复制就像连锁餐饮的"区域经理制":
scss
┌────────────────────────────────────────┐
│ 级联复制:分级管理模式 │
└────────────────────────────────────────┘
总部(Master)
"全国总店"
│
┌────────┼────────┐
↓ ↓ ↓
东区经理 西区经理 南区经理
(Slave1) (Slave2) (Slave3)
│
┌──┼──┐
↓ ↓ ↓
东城店 东郊店 东北店
(Slave4)(Slave5)(Slave6)
菜谱流向:
总部 → 东区经理 → 东城店、东郊店、东北店
总部 → 西区经理
总部 → 南区经理
好处:
• 总部只管 3 个区域经理(压力小)
• 区域经理管自己的分店(分层管理)
配置示例:
bash
# 东区经理(既是分店,也是区域总监)
redis-server --port 6380 --replicaof 127.0.0.1 6379
# 东城店(跟着东区经理学)
redis-server --port 6383 --replicaof 127.0.0.1 6380
# 东区经理的双重身份
127.0.0.1:6380> INFO replication
role:slave # 我是总部的分店
master_host:127.0.0.1 # 我的总店是总部
master_port:6379
connected_slaves:3 # 我管着 3 个分店
slave0:ip=127.0.0.1,port=6383,... ← 东城店
slave1:ip=127.0.0.1,port=6384,... ← 东郊店
slave2:ip=127.0.0.1,port=6385,... ← 东北店
优缺点分析
✅ 优点
1. 总部压力小
不分级(总部直管):
总部
├─→ 分店1 (全套菜谱)
├─→ 分店2 (全套菜谱)
├─→ 分店3 (全套菜谱)
├─→ 分店4 (全套菜谱)
├─→ 分店5 (全套菜谱)
└─→ 分店6 (全套菜谱)
总部的工作量:
• 打包 6 次(或等待多个分店一起)
• 发货 6 份
• 管理 6 个分店的联系
分级管理:
总部
├─→ 东区经理
│ ├─→ 分店4
│ ├─→ 分店5
│ └─→ 分店6
├─→ 西区经理
└─→ 南区经理
总部的工作量:
• 打包 1-3 次
• 发货 3 份
• 只管 3 个经理
轻松多了!✅
2. 节省跨城带宽
scss
跨城场景(像快递跨省):
北京总部 上海区域
Master ────长途专线─────────→ Slave1 (区域经理)
├─→ Slave4 (上海1店)
├─→ Slave5 (上海2店)
└─→ Slave6 (上海3店)
跨城带宽:只占 1 份(省钱!)
同城带宽:占 3 份(便宜!)
❌ 缺点
1. 延迟加倍(快递中转)
直达:
总部 ────→ 分店4
延迟:100ms
中转:
总部 ────→ 区域经理 ────→ 分店4
100ms +100ms
总延迟:200ms(慢了一倍)❌
2. 单点故障(区域经理是关键)
markdown
场景:区域经理生病了
总部 (正常营业)
│
└─→ 东区经理 💥 生病
├─→ 东城店 ❌ 收不到菜谱
├─→ 东郊店 ❌ 收不到菜谱
└─→ 东北店 ❌ 收不到菜谱
一人生病,全区瘫痪 ❌
3. 排查问题麻烦(多层传递)
问题:东北店的菜谱不对
可能的原因:
• 总部 → 区域经理 的快递丢了?
• 区域经理 → 东北店 的快递丢了?
• 东北店自己搞错了?
需要逐级排查,像破案 🕵️
使用建议
bash
✅ 适合分级管理:
• 跨城部署(北京、上海、广州)
• 分店超多(> 10 个)
• 总部压力大
❌ 不适合分级管理:
• 对延迟很敏感(金融交易)
• 对可靠性要求极高(核心业务)
• 分店很少(< 5 个,直管更简单)
📋 使用建议:
• 最多 2 级(总部 → 经理 → 分店)
• 区域经理配置要好(不能成为瓶颈)
• 做好监控(每一级都要盯着)
5.8 读写分离
实现方案
1️⃣ 客户端自己决定
java
// 方案 1:手动选择去哪吃饭
public class RestaurantClient {
private Jedis master; // 总店(做菜)
private List<Jedis> slaves; // 分店(接客)
// 要点新菜?去总店
public void orderNewDish(String dishId, String recipe) {
master.set("dish:" + dishId, recipe);
System.out.println("新菜在总店研发成功!");
}
// 吃饭?随便找个分店
public String getDish(String dishId) {
// 随机选一个分店
Jedis slave = slaves.get(random.nextInt(slaves.size()));
return slave.get("dish:" + dishId);
}
}
java
// 方案 2:用注解标记(更优雅)
@Service
public class UserService {
@Master // 写菜谱 → 去总店
public void updateMenu(Dish dish) {
dishMapper.update(dish);
System.out.println("✍️ 菜谱已在总店更新");
}
@Slave // 看菜单 → 去分店
public Dish getMenu(String dishId) {
return dishMapper.selectById(dishId);
}
}
2️⃣ 前台指引(代理)
javascript
客户来了:
"您好,请问要做菜还是吃饭?"
客户:"我要点新菜"
前台:"请去总店(Master)"
客户:"我要吃饭"
前台:"请去分店(Slave1/2/3)"
┌────────────────────────────────────────┐
│ Proxy(前台接待员) │
└────────────────────────────────────────┘
客户
↓
Proxy(判断请求类型)
├─ 写请求(点新菜)→ Master
└─ 读请求(吃饭)→ Slave1, Slave2, Slave3
优点:
• 客户不用想(对客户透明)
• 统一管理
缺点:
• 多了个前台(增加延迟)
• 前台也可能下班(单点故障)
数据一致性的陷阱
读写分离有个"时间差陷阱",就像这样:
vbnet
┌────────────────────────────────────────┐
│ 时间差陷阱:客人的困惑 │
└────────────────────────────────────────┘
场景:用户修改密码
时间线:
T0: 客人:"我要改密码为 new_pass"
→ 总店执行修改 ✅
T1: 总店:"好了,已改"
→ 立即回复客户
T2: 总店:"通知各分店"
→ 开始同步给分店(需要 100ms)
T3: 客人:"我用新密码登录"
→ 请求被路由到分店
T4: 分店:"密码是 old_pass"
→ 返回旧密码 ❌
客人:"???我刚改的密码怎么不对?"
→ 数据不一致问题!
解决方案:
方案 1️⃣ 刚写完的去总店看(写后读主)
java
public class SmartService {
// 记录最后一次写的时间
private ThreadLocal<Long> lastWriteTime = new ThreadLocal<>();
public void changePassword(String userId, String newPass) {
// 在总店改密码
masterRedis.set("password:" + userId, newPass);
// 记个时间
lastWriteTime.set(System.currentTimeMillis());
}
public String getPassword(String userId) {
Long writeTime = lastWriteTime.get();
// 刚改完密码(1 秒内)?去总店确认
if (writeTime != null &&
System.currentTimeMillis() - writeTime < 1000) {
// "刚改的,我去总店看看"
return masterRedis.get("password:" + userId);
}
// 时间够久了,分店肯定已经更新了
return slaveRedis.get("password:" + userId);
}
}
方案 2️⃣ 重要的事去总店(关键操作读主)
java
public class PaymentService {
public void pay(Order order) {
// 支付这么重要,必须去总店查余额!
Balance balance = masterRedis.get("balance:" + order.getUserId());
if (balance.amount >= order.amount) {
// 扣款(总店操作)
balance.amount -= order.amount;
masterRedis.set("balance:" + order.getUserId(), balance);
System.out.println("💰 支付成功!在总店确认余额,安全!");
}
}
}
方案 3️⃣ 强制指定店(用户选择)
java
// 让用户自己选
public String getValue(String key, boolean mustGoMaster) {
if (mustGoMaster) {
// "我必须去总店"
return masterRedis.get(key);
} else {
// "随便哪个店都行"
return slaveRedis.get(key);
}
}
方案 4️⃣ 检查快递进度(延迟检查)
java
public String smartGet(String key) {
int lag = checkReplicationLag();
if (lag > 5) {
// "分店快递延迟太久,我还是去总店吧"
return masterRedis.get(key);
} else {
// "分店菜谱是新的,可以去"
return slaveRedis.get(key);
}
}
最佳实践
java
/**
* 读写分离的"餐饮服务"最佳实践
*/
@Service
public class SmartRedisService {
@Autowired
private RedisTemplate<String, String> master; // 总店
@Autowired
private List<RedisTemplate<String, String>> slaves; // 分店们
/**
* 1. 写操作:新菜只在总店研发
*/
public void write(String key, String value) {
master.opsForValue().set(key, value);
log.info("✍️ 新菜谱已在总店记录");
}
/**
* 2. 读操作:优先去分店,分店关门了再去总店
*/
public String read(String key) {
try {
// 选个分店
RedisTemplate<String, String> slave = pickRandomSlave();
return slave.opsForValue().get(key);
} catch (Exception e) {
// 分店关门了?没事,总店还在
log.warn("⚠️ 分店出问题了,去总店", e);
return master.opsForValue().get(key);
}
}
/**
* 3. 强一致性读:重要事务必去总店
*/
public String readFromMaster(String key) {
log.info("🎯 重要查询,直接去总店");
return master.opsForValue().get(key);
}
/**
* 4. 负载均衡:客人排队,轮流去不同分店
*/
private AtomicInteger counter = new AtomicInteger(0);
private RedisTemplate<String, String> pickRandomSlave() {
int index = counter.getAndIncrement() % slaves.size();
log.debug("🎲 选择分店 {}", index + 1);
return slaves.get(index);
}
/**
* 5. 智能选择:检查分店菜谱是否够新
*/
public String smartRead(String key) {
if (areSlavesHealthy()) {
log.debug("✅ 分店菜谱都是新的,去分店");
return read(key);
} else {
log.warn("⚠️ 分店菜谱太旧了,去总店");
return readFromMaster(key);
}
}
/**
* 健康检查:分店菜谱够新吗?
*/
private boolean areSlavesHealthy() {
int lag = checkReplicationLag();
return lag < 1; // 延迟小于 1 秒认为健康
}
}
生产环境配置
总店配置(Master)
bash
# ============ 总店运营手册 ============
# redis-master.conf
# 总店地址(接受全国加盟)
bind 0.0.0.0
# 总店电话
port 6379
# 加盟暗号(防止冒牌货)
requirepass <strong_password>
# 总店记账(持久化)
save 900 1 # 15 分钟改了 1 次就记账
save 300 10 # 5 分钟改了 10 次就记账
save 60 10000 # 1 分钟改了 1 万次就记账
appendonly yes
appendfsync everysec
# 备忘录大小(记录最近的新菜)
repl-backlog-size 64mb
repl-backlog-ttl 3600
# 分店质量管控
min-replicas-to-write 1 # 至少 1 个分店正常
min-replicas-max-lag 10 # 延迟不超过 10 秒
# 快递方式(可选)
repl-diskless-sync yes # 直达快递
repl-diskless-sync-delay 5 # 等 5 秒拼单
分店配置(Slave)
bash
# ============ 分店运营手册 ============
# redis-slave.conf
# 总店地址(认谁当老大)
replicaof 192.168.1.100 6379
masterauth <strong_password>
# 分店只接客(只读)
replica-read-only yes
# 分店密码(如果分店也要密码)
requirepass <strong_password>
# 分店不记账(节省时间接客)
save ""
appendonly no
# 分店优先级(总店倒闭时,谁有资格接班)
replica-priority 100 # 数字越小,优先级越高
# 网络优化(快递加速)
repl-disable-tcp-nodelay no
# 联系超时(多久不通电话算失联)
repl-timeout 60
监控脚本
bash
#!/bin/bash
# 📊 餐饮连锁监控系统
# redis-replication-monitor.sh
MASTER_HOST="127.0.0.1"
MASTER_PORT="6379"
MASTER_AUTH="password"
echo "🔍 正在检查各分店状态..."
# 获取总店信息
info=$(redis-cli -h $MASTER_HOST -p $MASTER_PORT -a $MASTER_AUTH INFO replication)
# 有多少个分店?
slave_count=$(echo "$info" | grep "connected_slaves" | awk -F: '{print $2}' | tr -d '\r')
echo "🏪 总共 $slave_count 个分店"
# 检查每个分店的状态
for i in $(seq 0 $(($slave_count - 1))); do
lag=$(echo "$info" | grep "slave$i" | grep -oP 'lag=\K\d+')
offset=$(echo "$info" | grep "slave$i" | grep -oP 'offset=\K\d+')
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "分店 $i 状态:"
echo " 📄 菜谱进度: $offset"
echo " ⏱️ 延迟: $lag 秒"
# 健康度评估
if [ "$lag" -eq 0 ]; then
echo " ✅ 状态:优秀!实时同步"
elif [ "$lag" -le 5 ]; then
echo " 😊 状态:良好"
elif [ "$lag" -le 10 ]; then
echo " ⚠️ 状态:注意,延迟有点高"
else
echo " 🚨 状态:警告!延迟过高!"
echo " 🔔 发送告警..."
# 发送告警通知
# curl -X POST "https://alert.example.com/api/alert" \
# -d "message=分店$i 延迟: $lag 秒,客人可能吃不到新菜!"
fi
done
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━"
# 检查总店菜谱进度
master_offset=$(echo "$info" | grep "master_repl_offset" | awk -F: '{print $2}' | tr -d '\r')
echo "📚 总店菜谱总进度: $master_offset 页"
echo ""
echo "监控完成 ✅"
常见问题解答
Q1: 主从复制是同步还是异步?
A: 异步的,就像发快递。
arduino
总店:"新菜做好了"
总店:"发快递给分店"
总店:"不等分店确认收到,继续做下一道菜"
优点:总店效率高
缺点:可能存在延迟
Q2: 分店可以研发新菜吗?
A: 可以,但不建议。
bash
# 分店默认只能接客
replica-read-only yes
# 如果改成 no,分店也能"研发新菜"
replica-read-only no
# 但是!分店的"新菜":
# ❌ 不会同步到总店
# ❌ 不会同步到其他分店
# ❌ 总店同步过来时会被覆盖
# 就像分店私自改菜谱,总部不认可
Q3: 总店倒闭了,分店会自动接班吗?
A: 不会,需要手动或用"经理"(哨兵)。
bash
# 手动接班
127.0.0.1:6380> REPLICAOF NO ONE
OK # "我不认老板了,我自己当老板!"
# 或者用 Redis Sentinel(职业经理人)
# 自动监控,总店倒了立即让分店接班
Q4: 怎么判断总店和分店的菜谱是否一致?
A: 看页码(偏移量)。
bash
# 总店菜谱
master_repl_offset:1234567 # 写到第 1234567 页
# 分店菜谱
slave_repl_offset:1234567 # 学到第 1234567 页
# 页码相同 = 菜谱一致 ✅
# 页码不同 = 分店还没学完 ⚠️
Q5: 全量同步会让总店停业吗?
A: 几乎不会,只是开业时顿一下。
bash
• fork 子进程:像"开分身",顿一下(200-500ms)
• 整理菜谱:分身干活,总店照常营业
• 发快递:异步发送,总店不等
# 大总店(10GB)fork 可能慢一点
# 但也就是眨眼的功夫(毫秒级)
总结
本文用"餐饮连锁"的比喻,深入浅出地讲解了 Redis 主从复制:
核心概念
- 🏪 主从复制:总店 + 分店模式
- 📋 菜谱同步:总店研发,分店学习
- 🎯 复制偏移量:菜谱的页码
复制流程
- 🤝 建立连接:7 步握手,从相识到相知
- 📦 全量同步:打包发货,完整菜谱
- ➕ 增量同步:实时推送,新菜通知
- 💓 心跳机制:每秒互报平安
核心技术
- 📖 PSYNC:智能判断全量还是部分
- 🎓 PSYNC2:换老板也能智能补缺
- 📝 复制积压缓冲区:备忘录机制
- 🚀 无磁盘复制:直达快递,省时间
高级应用
- 🏗️ 级联复制:区域经理制
- 📖 读写分离:总店做菜,分店接客
- ⚠️ 数据一致性:警惕时间差陷阱
- 📊 性能优化:监控、告警、降级
实战经验
- ✅ 备忘录要够大(repl-backlog-size 64mb)
- ✅ 时刻监控延迟(lag 监控)
- ✅ 分店轻装上阵(关闭持久化)
- ✅ 读写分离要注意一致性
- ✅ 重要操作去总店(安全第一)
理解主从复制,就像理解连锁经营的奥秘:
- ✅ 实现高可用(分店顶替)
- ✅ 提升性能(分流客人)
- ✅ 数据安全(多处备份)
- ✅ 灵活扩展(想开几个分店开几个)
💡 下一篇预告:《Redis 哨兵模式详解:职业经理人的自动管理艺术》
当总店倒闭,如何让分店自动接班?哨兵模式就是答案!敬请期待!