外观
主从复制:从一条命令到复制断裂排查
主从是哨兵和 Cluster 的地基。这一节先把单 master 多 replica 跑通,并把"为什么从库读不到刚写的数据"这类问题讲透。
场景:什么时候要主从
- 读多写少,想把读流量分出去。
- 需要一份实时热备,master 挂了能顶上(这就要接着上哨兵)。
- 做备份时不想在 master 上
BGSAVE。 - 为 Cluster 做前置实验。
一、最小拓扑
| 角色 | IP | 端口 |
|---|---|---|
| master | 10.0.0.11 | 6379 |
| replica-1 | 10.0.0.12 | 6379 |
| replica-2 | 10.0.0.13 | 6379 |
三台都按 源码安装 装好,master 设 requirepass。
二、master 侧配置
ini
# /etc/redis/6379.conf (master)
bind 0.0.0.0
protected-mode no
requirepass YourStrongPass
# 复制相关
repl-backlog-size 64mb
repl-diskless-sync yes
repl-diskless-sync-delay 5
# 防止脑裂:至少 1 个从库在线且延迟 < 10s 才允许写
min-replicas-to-write 1
min-replicas-max-lag 10解释:
repl-backlog-size是复制积压缓冲区。从库短暂断线重连时,靠它做"部分重同步",不用全量传 RDB。默认 1mb,写 QPS 高时几秒就填满,一断线就退化成全量复制。生产给 64~256mb。repl-diskless-sync yes让 master 直接把 RDB 通过 socket 推给从库,不落磁盘。容器/云盘环境更合适。repl-diskless-sync-delay 5是等 5 秒攒一批从库一起传,减少多次 fork。只连一台从库时改成 0。min-replicas-to-write是防脑裂的:网络分区后旧 master 收不到从库确认,就拒绝写入,避免出现两份可写数据。
三、replica 侧配置
ini
# /etc/redis/6379.conf (replica)
bind 0.0.0.0
protected-mode no
requirepass YourStrongPass # 从库自身的密码,客户端连它时用
masterauth YourStrongPass # 连 master 时用的密码,别漏
replicaof 10.0.0.11 6379
replica-read-only yes
replica-serve-stale-data yes
replica-priority 100
# 从库建议不落 AOF,避免重启加载本地旧数据
appendonly no两处密码别写反:
| 配置 | 谁校验 | 写错的表现 |
|---|---|---|
requirepass | 客户端连本实例时 | 客户端 NOAUTH |
masterauth | 本实例连 master 时 | info replication 里 master_link_status:down,日志 Unable to AUTH to MASTER |
bash
systemctl restart redis-6379
tail -20 /var/log/redis/6379.log从库日志(首次全量同步):
text
* Connecting to MASTER 10.0.0.11:6379
* MASTER <-> REPLICA sync started
* Non blocking connect for SYNC fired the event.
* Master replied to PING, replication can continue...
* Trying a partial resynchronization (request 8f2c1e9a...:1).
* Full resync from master: 3d41b7c0e5a29f6b8c1d0e2f4a6b8c9d0e1f2a3b:0
* MASTER <-> REPLICA sync: receiving 21MB from master
* MASTER <-> REPLICA sync: Flushing old data
* MASTER <-> REPLICA sync: Loading DB in memory
* MASTER <-> REPLICA sync: Finished with success解释:
- 第一次连一定走
Full resync:master fork 出 RDB 传过来。 - 之后再断线,会先尝试
partial resynchronization(PSYNC <replid> <offset>),成功就是增量。
四、验证复制状态
master 上:
bash
redis-cli -a '密码' info replicationtext
# Replication
role:master
connected_slaves:2
slave0:ip=10.0.0.12,port=6379,state=online,offset=184320,lag=0
slave1:ip=10.0.0.13,port=6379,state=online,offset=184320,lag=0
master_failover_state:no-failover
master_replid:3d41b7c0e5a29f6b8c1d0e2f4a6b8c9d0e1f2a3b
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:184320
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:67108864
repl_backlog_first_byte_offset:1
repl_backlog_histlen:184320replica 上:
bash
redis-cli -a '密码' info replicationtext
# Replication
role:slave
master_host:10.0.0.11
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_read_repl_offset:184320
slave_repl_offset:184320
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:3d41b7c0e5a29f6b8c1d0e2f4a6b8c9d0e1f2a3b
master_repl_offset:184320
repl_backlog_active:0字段含义(只需要盯这四个):
| 字段 | 正常值 | 异常说明 |
|---|---|---|
master_link_status | up | down = 复制断了,看日志 |
master_last_io_seconds_ago | 0~1 | 持续 >10 = 网络或 master 卡住 |
master_sync_in_progress | 0 | 长期 1 = 正在全量同步,传输太慢 |
slave_repl_offset | 与 master 的 master_repl_offset 接近 | 差值就是积压的写命令数 |
写入验证:
bash
redis-cli -a '密码' -h 10.0.0.11 set repl:test 1
redis-cli -a '密码' -h 10.0.0.12 get repl:test
redis-cli -a '密码' -h 10.0.0.12 set repl:test 2输出:
text
OK
"1"
(error) READONLY You can't write against a read only replica.五、复制是异步的
这是最常被误解的一点:set 返回 OK 只代表 master 写进内存了,从库可能还没收到。
bash
redis-cli -a '密码' -h 10.0.0.11 set order:1 paid
redis-cli -a '密码' -h 10.0.0.12 get order:1 # 可能返回 (nil)如果业务真的需要"至少 N 个从库确认",用 WAIT:
bash
redis-cli -a '密码' set order:1 paid
redis-cli -a '密码' wait 1 100 # 等 1 个从库确认,最多 100ms输出:
text
OK
(integer) 1返回 1 表示有 1 个从库确认了;返回 0 表示超时(不代表写失败,只是没等到确认)。
WAIT 不是强一致
WAIT 只保证"命令已到达从库内存",不保证从库持久化,也不保证切换后数据还在。要真正的强一致得用 WAIT + AOF always + 哨兵/Cluster 的正确选主策略,代价很大。绝大多数业务应该接受异步复制,靠"读自己的写"(会话粘性)来规避。
六、只读从库读不到刚写的数据
这是异步复制的直接后果,现象是"用户刚保存,刷新页面还是旧值"。三种解法:
- 读写都走 master(最简单,读扩展就没了)。
- 写后短时间内读 master:在会话里记一个时间戳,N 秒内的读强制走主库。
- 按 key 路由:刚写过的 key 加白名单,命中就读主库。
别做的事:给从库加 replica-read-only no 让业务随便写。多写节点一定会分叉,之后没有任何自动化手段能合并。
七、复制断裂排查顺序
按这个顺序走,基本三步内定位:
1. 看日志
bash
grep -Ei 'master|sync|repl' /var/log/redis/6379.log | tail -30典型报错与含义:
| 日志 | 含义 |
|---|---|
Unable to AUTH to MASTER | masterauth 错或 master 改了密码 |
MASTER timeout: no data nor PING received | 网络不通 / 防火墙 / master 假死 |
Connection with master lost | 连接被断,随后会尝试重连 |
Unable to partial resync with the master | backlog 不够,退化全量 |
Diskless rdb transfer failed | master 的磁盘或 socket 发送异常 |
Replica is in an invalid state | 手工改过复制关系 |
2. 看网络与端口
bash
# 在从库上
timeout 3 bash -c 'cat < /dev/null > /dev/tcp/10.0.0.11/6379' && echo tcp-ok
redis-cli -h 10.0.0.11 -p 6379 -a '密码' ping
ss -lntp | grep 6379如果 redis-cli 能 PING 通但复制仍 down,多半是密码或 replicaof 配错,不是网络。
3. 看是否在反复全量同步
bash
# 从库
redis-cli -a '密码' info stats | grep -E 'sync_full|sync_partial'text
sync_full:17
sync_partial_ok:0
sync_partial_err:0sync_full 一直在涨、sync_partial_ok 是 0,说明每次断线都在全量重传。两个原因:
repl-backlog-size太小,断线时间一长 offset 就滑出缓冲区。- master 重启了:
master_replid变了,旧 replid 保留在master_replid2里,只有 offset 还在范围内才能续上。
bash
redis-cli -a '密码' config set repl-backlog-size 128mb
redis-cli -a '密码' config rewrite改 backlog 不用重启
repl-backlog-size 支持 config set 热改,改完对后续的复制生效,已在复制的从库不会立刻扩容。
八、手工切换与重建
把某台从库提升为主
bash
redis-cli -a '密码' -h 10.0.0.12 replicaof no one
redis-cli -a '密码' -h 10.0.0.12 info replication | head -3text
OK
role:master
connected_slaves:0其他从库挂到新主上:
bash
redis-cli -a '密码' -h 10.0.0.13 replicaof 10.0.0.12 6379
redis-cli -a '密码' -h 10.0.0.11 replicaof 10.0.0.12 6379旧 master 恢复后必须先转从
旧 master 重新上线时它仍然认为自己是 master,如果直接放回流量就是双写。动手前先停机、改配置成 replicaof 新主 6379,再启动。
另一个风险:旧 master 上有一批新主没有的写(脑裂期间),转从时全量同步会清空这些数据。要么接受丢失,要么手工导出比对。
重建一个坏掉的从库
bash
systemctl stop redis-6379
rm -rf /var/lib/redis/6379/dump.rdb /var/lib/redis/6379/appendonlydir
systemctl start redis-6379
redis-cli -a '密码' info replication | grep -E 'role|link_status'全量同步会重新灌一遍。数据量大时优先用"拷贝 RDB + 开复制"的方式减少 master 压力:
bash
# 在 master 上
redis-cli -a '密码' bgsave
scp /var/lib/redis/6379/dump.rdb 10.0.0.14:/var/lib/redis/6379/
# 从库启动后再 replicaof九、踩过的坑
| 现象 | 根因 | 处置 |
|---|---|---|
master_link_status:down,日志 Unable to AUTH | masterauth 密码错 | 对齐 master 的 requirepass |
| 从库可写 | replica-read-only no | 改回 yes |
| 每次重启都全量同步 | 从库开了 AOF 且加载了旧数据,offset 对不上 | 从库 appendonly no,或重建 |
| 复制延迟持续增长 | 从库单线程执行慢命令(大 key DEL、KEYS) | 用 UNLINK,禁 KEYS |
| master 内存翻倍 | 从库断线重连触发全量,fork + COW | 加大 backlog,减少断线 |
| 主从数据不一致但复制正常 | 有人在从库上直接写了 | 查 replica-read-only,重建从库 |
切换后业务报 READONLY | 客户端还连着旧 master | 用哨兵/Cluster 客户端,或更新配置 |
从库 master_link_status:up 但数据旧 | 只同步到一半(master_sync_in_progress:1) | 等,或查网络带宽 |