ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从全量到增量:Redis主从同步机制的底层原理与生产实践

从全量到增量:Redis主从同步机制的底层原理与生产实践 面试我总是先从这个问题开场你说说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-period10秒一次。从节点则会主动向主节点上报当前的处理进度使用的是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 AC节点配置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大小复制链路的事故往往在计数器悄悄变化时就埋下了苗头。
返回列表