Skip to content

08|主从复制

单机 Redis 跑了一段时间,数据已经有一些了。现在加一台机器做从库,把主库的数据同步过去——读请求可以分担,主库宕机时从库有完整副本可以接管。

从库连上主库后,主库的变更会自动同步过来。但复制不是强一致的——主库写入成功返回后,从库可能还没来得及收到。主库突然故障时,最后一段数据可能丢失。复制提供的是"副本"和"恢复基础",不是数据库级别的强一致。

主从复制也是 Sentinel 和 Cluster 的基础——后面两篇的高可用方案都建立在复制之上。

一、建立复制

主库已经在 192.168.10.11:6379 上运行。从库装好 Redis 后,只需一行配置让它连上主库:

conf
# 从库 /etc/redis/redis.conf
bind 127.0.0.1 192.168.10.129
port 6379
replicaof 192.168.10.11 6379
appendonly yes

replicaof 告诉从库:启动后去连 192.168.10.11:6379,同步数据。如果主库有密码,从库还要配 masterauth

conf
masterauth strong_password_here

重启从库:

bash
systemctl restart redis

从库连上主库后,第一件事是确认链路状态:

bash
redis-cli -h 192.168.10.129 INFO replication

输出里能看到几个关键信息:

text
role:slave
master_host:192.168.10.11
master_port:6379
master_link_status:up
master_repl_offset:1234567
slave_repl_offset:1234567

role:slave 表示当前角色是从库;master_link_status:up 表示跟主库的链路正常;两个 offset 相等,说明从库已经追上了主库,没有延迟。

主库上写一条,从库上立刻查:

bash
# 主库
redis-cli -h 192.168.10.11 SET test:replica "hello"

# 从库
redis-cli -h 192.168.10.129 GET test:replica
# "hello"

同步在工作。从库默认只读——直接写入会报错:

bash
redis-cli -h 192.168.10.129 SET x 1
# (error) READONLY You can't write against a read only replica.

这个限制能防止应用误写从库。故障切换时新主库会自动取消这个限制。

二、复制状态怎么看

INFO replication 是排查复制问题的核心入口。主库上的输出:

bash
redis-cli -h 192.168.10.11 INFO replication
text
role:master
connected_slaves:2
master_repl_offset:2345678
repl_backlog_active:1
repl_backlog_size:1048576

connected_slaves 是当前连接的从库数量;master_repl_offset 是主库每写入一条命令就增长的"复制偏移量",可以把它理解成"主库总共写了多少字节的数据流";repl_backlog_active 是主库是否在维护一个"复制积压缓冲区"——这个缓冲区保存了最近一段写命令,从库短暂断开后重连时,如果断开期间的数据还在缓冲区里,就只补增量,不用再做全量同步。

判断延迟的核心方法是对比主从的 offset:

bash
# 主库上
redis-cli -h 192.168.10.11 INFO replication | grep master_repl_offset
# 从库上
redis-cli -h 192.168.10.129 INFO replication | grep slave_repl_offset

两者的差值就是当前未同步的字节量。差值持续扩大说明从库追不上主库——可能的原因:网络带宽不够、从库 CPU 或磁盘慢、主库写入量太大、从库正在做 BGSAVE 或 AOF 重写抢了资源。

三、全量复制与部分复制

从库初次连接主库时,主库里已经有一堆数据了——从库不可能知道之前发生了什么,只能拿一份完整快照重新开始。这个过程叫全量复制

主库 fork 子进程生成 RDB 快照,通过网络发给从库,从库加载这份快照后,再接收快照生成之后的新写入。全量复制期间主库的 fork 和 RDB 传输都会消耗资源——如果从库很多且同时发起全量复制(比如所有从库同时重启),主库的压力是叠加的。重启从库要错开时间。

但从库如果只是短暂断网几秒钟,没必要再做一次全量复制——断开期间的那几条写入完全可以补发过去。主库维护了一个复制积压缓冲区(backlog),是一个环形缓冲区,记录最近的写命令:

conf
repl-backlog-size 256mb
repl-backlog-ttl 3600

从库重连后上报自己的 offset,主库检查这个 offset 是否还在 backlog 的范围内——在的话只发缺失的那段增量,这叫部分复制;不在的话就只能走全量复制。

repl-backlog-size 设多少合适?按"写入速率 × 期望容忍的最大断开时间"估算。如果主库每秒写入 10MB,期望 60 秒内的断链都能走部分复制,backlog 至少需要 10MB × 60 = 600MB。设太小频繁全量复制,设太大浪费内存。

repl-backlog-ttl 是当所有从库都断开、且超过这个时间(秒)后,主库释放 backlog 以节省内存。

全量复制时还可以选择不走磁盘:

conf
repl-diskless-sync yes

默认情况下,主库先把 RDB 写到磁盘文件,再发给从库——磁盘慢时这个"先写盘"步骤是瓶颈。repl-diskless-sync yes 让 RDB 直接通过 socket 发送,不额外落盘。代价是 RDB 数据缓存在主库内存中再通过 socket 发送,如果 RDB 很大(比如几十 GB),主库内存压力会短暂升高。

四、运行时动态管理复制

复制关系也可以在运行时建立或取消,不用改配置文件重启:

bash
# 让当前实例成为从库
redis-cli REPLICAOF 192.168.10.11 6379

# 让从库脱离复制,变成独立主库
redis-cli REPLICAOF NO ONE

运行时命令适合临时操作,但持久化配置要写进配置文件——不然重启后会恢复到配置文件里定义的拓扑。

REPLICAOF NO ONE 是手工切换的核心步骤。Sentinel 自动故障转移时,本质上就是选一台从库执行这个命令,把它提升为新主库,再通知其他从库改挂新主。

五、读写分离

读写分离的典型路径:写请求进主库,读请求走从库,主库把变更异步推给从库。

核心问题是复制延迟——刚写到主库的数据,去从库读可能还没到。处理方式分场景:

场景适合读从库不适合
首页配置、排行榜、统计延迟几秒不影响展示
备份导出避免主库直接承受备份读取
大量读请求分摊读多写少,能容忍短暂延迟
刚写完马上要读到复制延迟导致读到旧值
库存扣减、余额判断一致性要求高
分布式锁状态延迟会破坏互斥判断

业务如果要求"读己之写"(刚写入的数据立即能读到),写之后的读请求要发给主库,或者在应用层做短时间的主库读标记。

六、复制安全参数

主库可以配置:在从库数量不足或延迟过高时拒绝写入,降低"孤岛写入"的风险:

conf
min-replicas-to-write 1      # 至少 1 个从库满足条件才允许写
min-replicas-max-lag 10      # 从库延迟超过 10 秒就不算"满足条件"

从库全部异常时,主库会拒绝写入——宁愿短时间写失败,也不让主库在没有副本的情况下持续写入。一旦主库在这期间宕机,数据就永久丢失了。对数据安全性要求高的核心系统会开这个参数;纯缓存场景通常不开,因为这等于让从库的可用性绑架了主库的可用性。

单次写入后也可以用 WAIT 命令确认副本接收情况:

bash
redis-cli SET order:1001 status:paid
redis-cli WAIT 1 1000   # 等至少 1 个从库确认收到,最多等 1000ms

WAIT 返回实际确认的从库数量。返回 0 说明超时前没有从库确认——但写入在主库上已经成功了。这不是事务回滚,业务代码要处理"主库写成功但副本未确认"的中间状态。

七、复制异常排查

现象排查方向
master_link_status:down网络不通、主库密码错、防火墙拦截、主库 bind 没绑从库能连到的地址
反复全量复制backlog 太小、网络频繁抖动、从库频繁重启
从库延迟持续扩大主库写入量超出从库处理能力、网络带宽瓶颈、从库在做 BGSAVE/AOF rewrite
从库连不上主库ss -lntp 确认主库监听地址、安全组/防火墙规则、masterauth 密码、protected-mode

排查步骤:

bash
# 从库侧测试主库端口是否可达
nc -vz 192.168.10.11 6379

# 从库侧测试密码是否正确
redis-cli -h 192.168.10.11 -a 'strong_password_here' PING

# 看主从双方日志
journalctl -u redis-6379 -n 100 --no-pager
tail -n 100 /var/log/redis/redis-6379.log

复制异常时把 INFO replication 的完整输出保存下来——它记录了当时的角色、offset、链路状态和从库数量,复盘时不用靠猜。

八、从库做备份

从库常被用来承担备份任务,避免主库执行 BGSAVE 时的 fork 影响主库性能:

bash
redis-cli -h 192.168.10.129 BGSAVE
redis-cli -h 192.168.10.129 INFO persistence | grep rdb_last_bgsave_status

但从库备份的前提是延迟不大——如果从库已经落后很多,备份出来的 RDB 也落后同样多。备份前检查:

bash
redis-cli -h 192.168.10.129 INFO replication | grep -E 'master_link_status|master_last_io_seconds_ago'

master_link_status 为 up 且 master_last_io_seconds_ago 在几秒以内,备份的数据才算"够新"。备份成功和数据够新是两件事。

九、手工切换

Sentinel 或 Cluster 自动切换之前,手工切换是排查和演练的基础:

bash
# 1. 在选定的从库上提升为主库
redis-cli -h 192.168.10.129 REPLICAOF NO ONE

# 2. 确认新主库角色
redis-cli -h 192.168.10.129 INFO replication | grep role

# 3. 旧主库恢复后,作为新主库的从库加入,避免双主写入
redis-cli -h 192.168.10.11 REPLICAOF 192.168.10.129 6379

手工切换最麻烦的不是这几条命令,而是应用连接怎么切到新主库、旧主库残留的写入怎么处理、其他从库要不要改挂新主。切完之后除了 PING,还要做一次写入验证确认新主库确实可写。