面试我总是先从这个问题开场:你说说Redis的主从同步,全量复制和增量复制有什么区别?很多候选人能背出流程,"全量是主节点生成RDB发给从节点,增量是从节点断线后继续同步",但你再追一句"从节点重启后一定全量吗?为什么?",就明显开始慌了。
这其实暴露了一个问题:大家把同步机制当成概念在记,没有真正理解Redis复制协议的底层逻辑。Redis同步机制不只是应付面试的考点,它直接连着主从架构、哨兵故障转移、集群数据一致性、分布式锁正确性,甚至缓存治理中的脏读问题。把这套机制讲透,等于把Redis高可用体系一半的知识串起来了。这篇文章我会从复制协议的源码行为出发,结合生产环境真实踩坑记录,把全量复制、部分重同步、命令传播、心跳保活这些环节逐层拆开,再给你一套可以在本地用Docker复现的验证方法和面试应答策略。
1. 主从复制存在的意义:先弄清楚它解决什么问题再谈机制
1.1 内存数据库的单点困境
Redis把数据放在内存里,这是它能做到十万级QPS的根本原因,但与此同时带来了两个难题。第一,内存再大也有上限,单机承载的总数据量受物理内存约束。第二,进程崩溃、宿主机宕机、机房断电,内存里的数据说没就没。
很多人会反驳:Redis不是有RDB和AOF持久化吗?确实有,但持久化解决的是"重启后能恢复",而不是"故障时服务不断"。AOF重放或RDB加载期间,Redis是无法对外提供完整服务的,数据量越大恢复越慢。而且持久化本身存在丢失窗口,默认的RDB策略几秒甚至更久才落盘一次,真遇到宕机,最近一小段时间的写命令照样丢。
所以要解决的是高可用问题,而不是简单的"防止数据丢失"。高可用的核心诉求是:某个节点挂了,系统还能继续提供正确的读写服务。想要做到这一点,数据必须存在多个节点上。主从复制就是Redis用来把数据分发到多个节点的核心通道。
1.2 同步机制在高可用体系里的位置
Redis高可用的完整链路是这样的:主从复制负责数据副本,Redis Sentinel负责故障发现和主节点切换,Redis Cluster负责数据分片和分布式高可用。注意,哨兵和集群默认并不能凭空变出数据,它们能做的是在检测到主节点故障后,把某个存有完整数据的从节点提升为新的主节点。如果从节点的数据不完整,切换过去就会丢失大量数据;如果从节点之间数据不一致,切换后就会对外提供互相矛盾的数据。
所以,复制同步是所有上层高可用机制的地基。面试官问同步机制,本质上是在探你的底:你是只背了"主从复制"四个字,还是能理解它在整个架构中承担的职责。我建议所有准备面试的人都先建立这个认知,否则后面聊到哨兵的选举、Cluster的MIGRATE和replica迁移,你会越听越糊涂。
还有一个值得提前点出的观念:Redis复制是异步的,不是同步的。这意味着主节点执行完一条写命令后,不会等从节点确认就直接返回客户端。这样的设计换来了极佳的性能,但也决定了Redis的数据一致性是最终一致性,而不是强一致性。这个特性贯穿全文所有环节,你后面看全量复制、部分重同步、锁的正确性,都离不开"异步"这两个字。
2. 全量复制是怎么跑起来的:一条RDB从主节点到从节点的完整链路
2.1 同步从哪里开始:PSYNC握手
主从同步的第一步不是传数据,而是握手。从节点通过配置replicaof <master-ip> <master-port>或在客户端执行REPLICAOF命令,向主节点发起复制请求。这里有个容易忽略的细节:从节点发起的不只是一个简单的"给我把数据传过来"请求,而是一个带有身份信息的PSYNC协议。
第一次建立主从关系时,从节点还没有任何复制的身份信息,所以它发送的是:
PSYNC ? -1主节点收到这个请求后,发现从节点处于"完全未知"状态,就会走全量复制流程,返回:
+FULLRESYNC <replid> <offset>其中replid是主节点的复制ID,可以理解为主节点在复制体系里的身份证号码;offset是主节点当前的复制偏移量,表示主节点到目前为止累计记录了多长的复制数据流。从节点拿到这两个值后,会保存在内存里,后续的增量复制都依赖这个身份。
再往后,每当主节点执行了一条写命令,就会把这条命令包装成统一的复制数据流,追加到自己的复制偏移量上。从节点每处理一部分数据,也会同步推进自己的slave_repl_offset。我在排查问题的时候,经常通过对比两个offset来判断主从落后的距离。
2.2 RDB生成与传输期间的新写命令去了哪里
既然要走全量,主节点就必须把当前完整的快照发给从节点。这一步很多人以为就是执行一次SAVE然后传文件,实际没这么简单。
主节点收到FULLRESYNC请求后,会触发一个bgsave子进程,在后台生成当前数据集的内存快照RDB文件。这里有两个明显的问题:
- 子进程在生成RDB期间,Redis主进程还在正常接收读写请求,这些新写入的命令怎么处理?
- 主节点生成的RDB如果太大,传输过程比较久,这期间的新写命令怎么保证不丢?
答案在两部分内存结构里:一是主节点为每个从节点单独维护的复制输出缓冲区(replication buffer),二是整个实例级别的复制积压缓冲区(repl_backlog)。RDB传输是异步的,主节点会把RDB生成期间新产生的写命令同时写进这两个缓冲区。等RDB文件传完,从节点加载完毕,主节点再把缓冲区中积压的新命令继续发过去,让两条数据流最终对齐。
这期间最典型的故障,我在生产里遇到过不止一次:主节点数据量很大,RDB生成加传输耗时接近甚至超过client-output-buffer-limit中的限制值,主节点为了自保直接断开和从节点的连接。从节点重新发起同步,又触发一轮新的全量,结果形成"全量复制风暴"。所以遇到这类问题,思路不是调大缓冲区就完事,而是要去看主节点是否存在大key、慢查询导致bgsave耗时过长,以及从节点的网络带宽瓶颈。
2.3 从节点加载RDB时的服务表现
从节点拿到RDB文件不是直接就能用的。它首先会把RDB写入磁盘,然后清空自己当前的内存数据,再执行加载。整个过程在从节点上是阻塞性的,数据量越大阻塞越久。
有个配置叫replica-serve-stale-data,默认是yes。什么意思?在从节点和主节点完成同步之前——比如正在加载RDB或长时间未完成同步——从节点对外仍可以响应请求,但返回的全部是旧数据。如果设置成no,从节点在这个窗口期会直接拒绝所有读写请求。
我在面试时问过很多人这个配置,答上来的不多。这个点很能体现候选人有没有真正跑过主从环境,建议你留意一下。实际业务中如果从节点读多写少,而且对数据新鲜度有要求,我会把这个参数调成no,同时配合哨兵的健康检查让流量先切走,避免读到离谱的旧数据。
2.4 无盘复制与全量复制的隐患
对GB级别以上的Redis实例,磁盘IO往往是全量复制的瓶颈。Redis从2.8.18开始支持无盘复制,也就是主节点在生成RDB时,不落盘,直接把子进程生成的快照数据通过网络套接字发送给从节点,配置项是repl-diskless-sync。这个模式非常适合机器磁盘性能一般但内网带宽充裕的场景。
但无盘复制也有代价:一个从节点发起全量复制时,主节点可能要稍微等一会儿,以便多个同时请求全量的从节点可以共享同一份快照流,这也就是repl-diskless-sync-delay存在的意义——默认延迟5秒。如果只有一个从节点,这5秒就显得浪费。生产里要根据主节点承载的读流量和从节点数量权衡,不要在没搞清楚代价前乱开。
全量复制本身是个高成本操作:fork子进程会短暂阻塞主线程,RDB生成期间Redis内存占用可能临时增加一倍以上,网络带宽也可能被瞬间打满。所以任何一个成熟的生产环境,都会想尽办法减少全量复制的发生。这就引出了下半部分的核心机制:部分重同步。
3. 断线续传的实现:复制积压缓冲区与PSYNC协议的判定逻辑
3.1 环形缓冲区:复制积压缓冲区的工作方式
全量复制解决的是"从零开始"的问题,但生产里更常见的是"复制链路抖动断开了,数据衔接不上"。如果每次抖动都要全量重传,大实例根本扛不住。所以Redis设计了一个专门服务断线续传的结构,叫做复制积压缓冲区。
这个缓冲区是主节点维护的一段环形内存空间,默认大小只有1MB,配置项是repl-backlog-size。主节点每执行一条写命令,在发给从节点的同时,也会把这条命令追加到这个缓冲区里。因为是环形结构,新数据会覆盖最旧的数据,所以这个缓冲区能保留的,其实是最近一段时间内主节点产生的复制数据流。
注意这里的"数据流大小"和"内存实际写入量"不是一回事。一条DEL hugekey命令本身只有十几个字节,但它可能让主节点内存减少几个GB,复制数据流却只增加了十几字节。很多人估算backlog时按业务写入量算,这是对的,但容易忽略了批量删除这类特殊操作的影响。
3.2 部分重同步的判定条件:replid和offset的配合
当从节点因为超时、网络闪断等原因断开后重新连上主节点时,它不再发送PSYNC ? -1,而是发送自己还记得的身份信息:
PSYNC <replid> <offset>这个replid是它上一次全量复制时从主节点拿到的,offset是它断开前已经成功处理到的复制偏移量。主节点收到后要做三个判断:
- 从节点带来的replid和当前主节点replid是否一致?
- 从节点断线前的offset,是否仍然落在复制积压缓冲区尚未被覆盖的范围内?
- 断线期间产生的数据流,是否足够从offset完整补充到最新?
如果三个判断全部通过,主节点回复+CONTINUE,然后从复制积压缓冲区里,把从节点缺失的那一段数据流直接发给它。从节点不需要清空数据,不需要重新全量加载,只需要继续执行补发的命令,这就是部分重同步,也叫增量复制。
3.3 为什么主节点重启不一定会全量,从节点重启却很容易全量
这是面试和实际加固环节里最容易混淆的一点,我单独拿出来说。
主节点如果配置了持久化,重启后replid会尽可能从持久化数据中恢复保留,复制积压缓冲区也会同步恢复。这意味着主节点重启后,从节点重新发起PSYNC时,只要它的offset还在backlog覆盖范围内,甚至可以不用全量同步。这是Redis 4.0引入PSYNC 2.0之后的一个重要能力,它直接优化了主节点短期重启场景下的恢复效率。
但从节点重启就不一样。从节点的replid和复制偏移量主要保存在内存里,进程重启后这些信息就丢了。它重新连上主节点时只能发送PSYNC ? -1,主节点一看这是"新人",只能全量复制。所以"重启从节点大概率触发全量同步"这句话,在多数配置下是成立的。
实际操作中的一个建议是,尽量避免在生产环境随意重启从节点。如果需要做内核参数调整、配置更新,优先选在业务低峰期操作,同时把repl-backlog-size调大,给这种误操作留足缓存余量。
3.4 复制积压缓冲区到底该配多大
这个问题基本每个面试官都会顺着问:你说backlog可以避免全量,那配多大合适?
正确思路不是拍脑袋,而是先评估两个数:单位时间内的复制数据流速率,和你能容忍的断线时间。比如高峰期主节点每秒钟产生200KB的复制数据流,你想要让从节点断线10分钟后重连还能部分重同步,那么backlog至少要有:
200KB/s × 600s = 120MB也就是说至少要设置repl-backlog-size 128mb。但实际生产还要考虑峰值流量和网络抖动时长,所以通常我会再乘2到3倍。比如200KB/s的业务,真正落到128MB到256MB之间才算安心。
这个公式反映出来的能力,比直接背"默认1MB太小"要有说服力得多。另外,还有一个观测指标可以直接验证backlog够不够:主节点INFO replication下的sync_partial_errs,这个值表示部分重同步失败的次数。如果它持续增长,基本可以断定backlog太小或从节点长时间落后太多。
4. 复制不是只能靠全量和增量:命令传播、心跳与级联拓扑
4.1 主节点怎么把一条写命令变成复制数据流
从节点完成全量复制后,主从关系就进入稳态。这个阶段,主节点不会专门去扫描差异,而是把每一条写命令边执行边广播给从节点。复制数据流使用的协议格式,本质上就是RESP协议的命令序列。比如执行一条:
SET user:name "zhang"主节点复制流里至少包含*3开头的命令头、$3长度的SET、key和value的各部分。从节点收到后直接重放这条命令,就能得到与主节点一致的数据。
这种设计有一个隐藏优势:从库上执行的不只是数据变更,还包括带有副作用的命令,比如EXPIRE、LPUSH、SINTERSTORE,重放时都会得到相同结果。但有一个著名的坑——EXPIRE这类相对过期机制,主节点记录的是"截止时间点",从节点重放时也是按绝对时间执行,所以只要主从时钟差了太多,就会出现key提前或延后过期的情况。生产里强烈建议所有Redis节点都配置NTP时间同步,这不只是运维洁癖,是复制一致性的真实需求。
4.2 心跳为什么是同步机制的保命符
复制链路不是一次建成就永远有效的,连接的存活需要双方不断"刷存在感"。主节点会周期性向从节点发送PING,默认间隔时间是repl-ping-replica-period,10秒一次。从节点则会主动向主节点上报当前的处理进度,使用的是REPLCONF ACK <offset>命令,默认每秒一次。
如果主节点连续超过repl-timeout(默认60秒)没有收到从节点的有效响应,就会判定复制连接超时,主动断开。从节点侧也会基于同一个超时阈值,判断主节点是不是失联了,进而触发重连。
这里有个容易被忽略的细节:Redis的复制超时判定是基于"是否有任何有效数据流动",而不是固定频率。所以如果主节点长时间没有写操作,复制数据流是静止的,心跳PING就是唯一维持连接的信号。有些人为了省事把repl-ping-replica-period调大,结果一遇到网络抖动,连接就莫名断掉,都是没搞清楚超时机制。
从节点上报的REPLCONF ACK offset还有个重要作用:主节点可以根据ACK,准确知道每个从节点落后了多少。配合min-replicas-to-write和min-replicas-max-lag这两个配置,可以让主节点在从节点数量不足或严重延迟时拒绝写入,这是Redis里极少数能主动限制数据丢失风险的开关。追求数据安全性的场景,我建议开启。
4.3 从节点还可以有自己的从节点:级联复制
如果从节点数量特别多,全部直接挂在主节点下,主节点要为每个从节点维护一份输出缓冲,还要各自处理全量复制请求,压力不小。级联复制就是为了缓解这个问题:某个从节点同时扮演下一层从节点的主节点角色。
配置方式很简单,B节点同时配置replicaof A,C节点配置replicaof B,形成A→B→C的链。这样全量数据从A流向B,再由B转发给C,主节点A只需要面对少数直接从节点。
级联拓扑带来的复杂度是,B节点一旦故障或重启,C节点会尝试重新同步。这里PSYNC 2.0的另一个关键能力登场了:中间层从节点拥有一个secondary_replid,保留着上一层主节点的复制身份。当故障转移发生后,新主节点会继承旧主的replid信息,从节点凭借自己记录的旧主身份,依然有可能向新主发起部分重同步,而不需要全量。这就是为什么Redis 4.0之后的故障切换,很少再出现所有从节点同时全量复制的"雪崩"。
4.4 主从延迟的观测方法
面试里聊到数据一致性时,面试官会问你怎么量化主从延迟。比较直接的手段是在从节点上执行INFO replication,查看master_last_io_seconds_ago字段,它表示距离最近一次与主节点交互过去了多久。这个值接近0说明链路通畅,如果持续增大,就要怀疑网络拥塞或从节点自身阻塞了。
更精细的延迟分析,可以对比主从节点上的master_repl_offset和slave_repl_offset差值。差值越大,说明从节点落后越远。不过要注意,offset差异衡量的是复制数据流字节数,不是时间,真正的时间延迟还要结合业务写入速率推算。生产监控里我用得最多的还是master_last_io_seconds_ago,简单直观,告警灵敏度也够。
5. 亲手验证:用Docker搭一套主从并模拟断线重连
5.1 快速启动一组主从实例
理论讲再多,不如自己动手跑一遍。我推荐用Docker,几分钟就能搭好环境,做全量复制观察、断线重连、参数调整都方便。
先建一个自定义网络,方便容器用服务名互相访问:
docker network create redis-lab启动主节点,监听宿主机6379端口:
docker run -d --name redis-master \ -p 6379:6379 \ --network redis-lab \ redis:7启动从节点,监听宿主机6380端口,并通过启动参数直接声明主从关系:
docker run -d --name redis-replica \ -p 6380:6379 \ --network redis-lab \ redis:7 \ redis-server --replicaof redis-master 6379从节点起来后,往主节点写几条数据,然后在从节点查一下,能看到数据说明最基本的主从复制已经通了:
docker exec -it redis-master redis-cli set lab:key hello docker exec -it redis-replica redis-cli get lab:key5.2 用INFO和日志确认全量复制的发生
登录从节点,执行INFO replication,你会看到类似下面的输出:
role:slave master_host:redis-master master_port:6379 master_link_status:up master_last_io_seconds_ago:0 master_sync_in_progress:0 slave_read_repl_offset:12345 slave_repl_offset:12345 slave_priority:100注意,新版Redis配置命令统一用replicaof,但INFO replication里的角色字段在不少版本里仍叫slave,这个是历史兼容问题,不用被吓到。核心看两个地方:master_link_status:up表示链路正常,slave_repl_offset表示从节点已经处理到的复制位置。
在主节点上执行INFO stats,关注两个计数值:
sync_full:1 sync_partial:0sync_full为1,说明刚才建立主从关系时确实走了一次全量复制。如果后续发生断线重连但被部分重同步接住了,sync_partial就会增长,而sync_full保持不变。这两个计数器是你判断同步类型最直接的依据。
5.3 模拟网络闪断,观察增量重连
重启从节点比较简单,直接触发全量的情况大家自己跑一遍就清楚了,sync_full会从1变成2。但更贴近生产的是模拟网络闪断,而不是进程重启。Docker提供了现成的网络隔离操作:
docker network disconnect redis-lab redis-replica sleep 10 docker network connect redis-lab redis-replica断开网络后,从节点只是失去连接,进程还在,内存里保存的replid和offset都完好。等网络恢复,它会向主节点发送PSYNC <replid> <offset>重连。如果主节点的backlog大小足够容纳这10秒内的数据变化,就会触发部分重同步。
回到主节点再看INFO stats,如果sync_partial从0变成1,恭喜你,断线续传机制在你的环境里真实跑通了。这个实验强烈建议做一遍,它比背十遍"PSYNC是增量同步"都管用。
5.4 把backlog调大,观察offset落后多少才会触发全量
你还可以做一个更有意思的对比实验:把主节点repl-backlog-size调成很小的值,比如1kb,然后断开从节点网络,在主节点上持续写入大量数据,超过backlog容量后再恢复从节点连接。这时由于offset已经落在backlog覆盖范围之外,主节点只能接受新一轮全量复制,sync_full会再次增加。
源码行为就是这么直观地暴露在计数器面前。很多人问我调参的依据,我常说:别光听我讲,先把Docker实验跑一遍,参数的影响你自己就感知到了。
6. 面试官连环追问:同步机制延伸出的高频考点怎么答
6.1 分布式锁为什么和异步复制存在天然矛盾
Redis分布式锁是最常见的Redis面试延伸题,它和同步机制有非常深的纠葛。经典做法是:
SET lock:order:1 随机值 NX EX 30这个命令写在主节点。如果主节点加锁成功后,还没来得及把这条命令同步给从节点,主节点就宕机了,哨兵把某个从节点提升为新主节点。此时新主节点上根本没有这把锁,另一个客户端就也可以成功加锁,两个客户端同时持有"互斥锁",互斥语义被打破。
这个问题的本质,不是锁的实现哪里写错了,而是分布式锁的正确性依赖"写入操作的可见性",而Redis主从复制是异步的,从节点看到数据必然存在时间差。面试里回答这个问题,你可以分两层展开:一是Redis锁在单主节点、无故障场景下没有问题;二是一旦出现主节点故障,异步复制会让锁存在丢失窗口。针对这个窗口,Redis官方提出了Redlock算法,但Redlock自身在极端网络分区下也有争议,所以工程上有人转向ZooKeeper这类带强一致语义的协调服务。能讲到这一层,面试官基本能确认你不是背题库,而是真的理解架构权衡。
6.2 缓存治理中的主从延迟如何影响一致性
面试里另一个高频话题是缓存治理,包括缓存穿透、缓存击穿、缓存雪崩。这些和同步机制的关联点在于:一旦缓存架构引入从节点分担读流量,主从延迟就直接变成了脏读延迟。
举个例子,业务先更新数据库再删除缓存,删除操作写入Redis主节点。如果删除请求在从节点还没生效,下一个读请求刚好落到从节点,就会读到旧缓存,回源时甚至可能把旧值再次写入缓存,造成长时间不一致。
常见的应对手段包括:写入后短期内强制读主节点、给缓存过期时间增加随机扰动、在极端场景下使用延迟双删——先删缓存、更新数据库、等待一哨兵个延迟窗口后再删一次。延迟双删能缓解从库延迟造成的旧值回填问题,但延迟窗口到底设多少没有定论,本质上是个经验性的兜底方案,生产里不要把它当银弹。
面试时提到这些方案,最好能主动说出它们的适用边界。一个成熟的工程师和一个背八股的人,差别就在"知道什么情况下方案会失效"。
6.3 主动抛出的进阶理解:CAP视角下的主从复制
如果你想在面试最后给面试官留下更深的印象,可以在聊到同步机制时主动把视角拉到分布式理论层面:Redis主从复制本质上是CAP里AP路线的实践,优先保证可用性和分区容错性,牺牲了强一致性,换取的是极致的写入性能。
但这不代表Redis完全放弃一致性。复制积压缓冲区、部分重同步、心跳检测、min-replicas-to-write、WAIT命令,这些机制都是在尽量压缩不一致窗口,做到"多数情况下最终一致"的工程化努力。Redis 6.0之后提供的WAIT命令可以等待指定数量的从节点确认写入,但它的语义是"等待数据复制到N个节点",依然没有完全消除主从差异,更不等于强一致。能把这套权衡讲清楚,比单纯罗列机制更能说明你理解能力的深度。
还有一个我常看到候选人掉进去的误区:把客户端缓存和Redis主从同步混为一谈。客户端缓存、Redis从节点、多级缓存之间各自有独立的过期和同步策略,三者不一致的根源不同,应对方式也不同。面试里如果被问"主从延迟导致缓存不一致",一定要先拆清楚你到底在说哪一层的不一致,否则答案很容易跑偏。
写在最后的一些个人经验
同步机制这块内容,我前前后后在不同项目里被反复折磨过。早期以为配好replicaof就万事大吉,直到有一次凌晨被告警吵醒,发现主从复制链路反复闪断,日志里全是MASTER aborted replication with an error: NOAUTH,才意识到主节点开了requirepass之后,从节点漏配了masterauth。这种小问题,没跑过生产环境光看书根本遇不到。
如果你正准备面试,我的建议是把这篇文章里提到的关键命令和计数器都亲自在Docker环境里跑一遍,尤其是全量复制和部分重同步的触发差异。面试时能把"从节点重启触发全量,网络闪断触发部分重同步"讲出底层原因,就已经领先大多数候选人了。如果你已经在维护生产Redis,也别忘了定期检查sync_partial_errs和backlog大小,复制链路的事故往往在计数器悄悄变化时就埋下了苗头。