news 2026/10/27 20:11:32

Redis主从复制机制全解:偏移量、积压缓冲区与延迟排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis主从复制机制全解:偏移量、积压缓冲区与延迟排查

主从复制这件事,很多团队是在业务量上来之后才临时补的,结果配置一加、从库一挂,问题全冒出来了。我在不同规模的线上环境里见过太多类似的场景:主库的INFO replication里connected_slaves显示三个从库,master_link_status全是up,看起来一切正常,可应用侧读到的数据就是比主库慢半拍,某些键莫名其妙地“消失”又“复活”。这类问题几乎都不是 Redis 本身坏了,而是对主从复制的运行机制缺少完整的认识:不知道数据是以什么粒度在链路上流动的,不知道全量同步和增量同步的分界线在哪里,也不知道哪些参数决定了断线之后还能不能接得上。这篇内容围绕 Redis 主从复制机制本身展开,从偏移量、复制 ID、积压缓冲区这些底层概念,讲到实际搭建、拓扑选型、复制风暴与延迟排查,再到没有哨兵时的手工切主流程。适合已经能把 Redis 跑起来、但还没把主从彻底摸透的开发者,也适合正在为读写分离方案做技术选型的人。

1. 主从复制的真实定位:它不是备份,也不是高可用

1.1 复制解决的是“读”和“热备”,不是“丢数据”

先把定位摆正,后面所有配置决策才不会跑偏。Redis 主从复制本质上是单向的、异步的数据流复制:主库把写命令按执行顺序打包,持续推给从库,从库重放这些命令来保持数据一致。它解决的核心问题是两件事——横向扩展读能力,以及准备一份可以随时接管的热备副本。

但它不解决“数据绝对不丢”。主库执行完一条SET就返回客户端成功,此时这条命令还在主库的复制缓冲区里排队,从库收到没有、落盘没有,主库根本不关心。所以只要主库在命令发出后、从库确认前的窗口里挂掉,这条数据就是丢了,而且丢得毫无痕迹。这一点和很多人印象里的“从库就是备份”差距很大。

真正意义上的备份是 RDB 快照文件、AOF 文件被搬运到异地存储介质上,那份数据不依赖任何进程活着。从库顶多算一份“热副本”,它可能比主库慢、可能在复制中断期间缺少一段数据、也可能因为一次误操作被主库的FLUSHALL同步清空。把从库当备份用,是线上事故里最常见的一类误判。

我在做方案评审时通常只问一句:这个从库是用来扛读流量的,还是用来做数据兜底的?答案不同,后面的资源配置、replica-read-only设置、监控指标、甚至拓扑结构都不一样。如果是为了兜底,那从库不应该承接任何业务读,最好独立部署、定期把 RDB 转移到别的存储上;如果是扛读流量,那就得接受“从库数据有延迟”这个前提,并在应用层设计好降级逻辑。

1.2 三个必须先想清楚的问题:读一致性、延迟预算、切换方式

动手配replicaof之前,有三个问题必须先有答案,否则搭出来的主从迟早要返工。

第一个是读一致性要求。业务能不能接受“刚写完立刻读,可能读不到”?如果某个接口是典型的“写后立即读”,比如下单后立刻查订单状态,那这个读请求必须走主库,或者走从库但加一层“写时把 key 标记、读时校验版本”的兜底。很多团队的做法是简单粗暴地全部读从库,然后在压测时发现偶发数据不一致,回头排查半天才发现是复制延迟。这个坑不该在线上踩。

第二个是延迟预算。主从延迟不是恒定的,它随写入量、网络状况、从库负载波动。正常情况下同机房内网延迟在毫秒级,但如果主库写入突增、或者从库执行了某个KEYS类的慢命令被阻塞,延迟能瞬间冲到几秒甚至几十秒。业务侧需要明确一个可接受的上限,比如 200ms,超过这个值就应该触发告警,而不是等用户投诉。

第三个是切换方式。手工切、哨兵自动切、还是集群模式?三者的复杂度和运维成本差得很远。手工切最灵活,但对故障响应速度要求高;哨兵能自动发现主库故障并选举新主,但引入了新的组件和新的不确定性(比如网络分区时的脑裂)。这一层想在前面,后面才不会在主库挂掉的时候手忙脚乱。

提示:如果团队里没人能说清楚“主库挂了之后谁来切、切完应用怎么连”,那这套主从只是搭给监控看的,不具备真实的高可用能力。

2. 复制链路里流动的到底是什么:offset、replid 与 backlog

2.1 字节偏移量:主从状态对账的唯一硬指标

复制偏移量(replication offset)是整个主从机制里最核心也最容易被忽略的概念。它不是“第几条命令”,而是主库复制流的字节累计值。主库每向复制流写入一个字节,master_repl_offset就加一;从库每重放一个字节,slave_repl_offset也加一。

这个设计非常巧妙:偏移量天然是全序的、单调递增的,主从双方只要报出自己的偏移量,就能立刻算出差距有多大。在从库上执行INFO replication,看到:

master_repl_offset: 12345678 slave_repl_offset: 12345600

这两个值的差 78,意味着从库落后主库 78 个字节的复制流。注意是字节不是命令条数,一条SET user:1 xxx可能几十个字节,具体要看 value 长度和协议开销。

实操中我更习惯在主库上执行INFO replication,看slave0、slave1这些行的offset字段,和主库的master_repl_offset逐一做减法。同时关注lag字段,它表示从库最后一次向主库发送REPLCONF ACK距今的秒数。这两个指标要结合起来看:如果 offset 差得很大但 lag 是 0,说明从库一直在正常确认,只是追不上写入速度;如果 lag 已经几十秒了,那基本可以判定链路已经出问题了。

需要注意lag的精度是一秒级,它反映的是“最后一次确认距今多久”,不是精确的延迟值。想看更细的延迟,得靠应用层打点,比如在写主库的同时记录时间戳,读从库时对比。

2.2 replid 与 replid2:换了“血统”还能不能续上

复制 ID(replication ID,简称 replid)是主库的“身份标识”,一个 40 位的十六进制字符串,主库启动时随机生成。从库连接主库时会记住这个 ID,断线重连时带着它一起发过去,主库一看“哦,你之前跟的是我”,才愿意做增量同步。

问题在于主库重启。如果主库是正常重启,它会从 RDB 或 AOF 里恢复出原来的 replid,从库还能续上。但如果主库是通过replicaof no one提升上来的、或者配置被清掉了,它会生成一个全新的 replid,此时原先的从库带着旧 ID 来请求增量同步,主库直接拒绝,返回+FULLRESYNC,走全量。

为了让“主库重启 + 从库提升”这类场景不至于每次都全量,Redis 引入了replid2机制(PSYNC2)。简单说,从库被提升为主库时,会同时保留旧主的 replid 作为replid2,并记录一个second_repl_offset作为分界点。这样旧主的其他从库拿着老 ID 来连,只要偏移量在分界点之前,新主仍然可以接受它们做部分重同步。这个机制在“主库挂了、从库顶上、旧主恢复后重新加入”的典型流程里价值极大,能省掉一次全量传输。

在INFO replication里能看到master_replid和master_replid2两个字段。日常运维不太需要直接操作它们,但排查“为什么这次重连又全量了”时,看一眼 replid 有没有变,往往能定位到根因。

2.3 repl-backlog:那个默认只有 1MB 的环形缓冲区

积压缓冲区(replication backlog)是主库维护的一个固定大小的环形缓冲区,专门存最近一段时间的复制流。从库断线重连请求增量同步时,主库会检查从库的偏移量是否还落在 backlog 覆盖的范围内,在的话就把缺失的那段数据补给它,不在就全量。

关键点在于:默认的repl-backlog-size只有 1MB。这个值放在今天几乎是个玩笑。假设主库复制流每秒写 500KB(这个量级对于一个中等写入压力的业务很常见,一条 value 200 字节的写命令,每秒 2500 次写入就够了),1MB 的 backlog 只能覆盖大约 2 秒的断线。也就是说,只要网络抖一下超过两秒,从库重连就得全量同步。

这个参数该设多大?有个简单的估算方法:

参数计算方式示例
复制流写入速率用主库master_repl_offset的增量除以时间10 秒内增长 5MB,即 0.5MB/s
可容忍断线时长按网络抖动、运维操作窗口估60 秒
理论需要值速率 × 时长30MB
实际配置值理论值 × 1.5 到 2 倍冗余64MB

所以 64MB 到 128MB 是个比较稳妥的起点,写入特别重的实例可以给到 256MB。代价是主库会额外占用这么多内存,但这个开销比起一次全量同步带来的 fork 阻塞和带宽冲击,完全值得。

还有一个参数repl-backlog-ttl,默认 3600 秒。它控制的是“主库上没有任何从库连接之后,backlog 保留多久再释放”。如果你的场景是主库重启后从库会延迟一段时间才连上来,可以考虑调大这个值,让 backlog 多留一会儿。

注意:repl-backlog-size可以在运行时用CONFIG SET调整,但调整后 backlog 会被重建,之前的内容丢失。所以要在业务低峰期做,且调整后立即用CONFIG REWRITE写回配置文件,避免重启丢失。

3. 一次 PSYNC 的完整旅程:从 PING 到命令流

3.1 握手阶段的四步与常见卡点

从库配置了replicaof之后,它并不会立刻开始收数据,中间要走一套完整的握手。这套流程理解了,很多“从库一直是down状态”的问题能自己定位。

第一步,从库向主库建立 TCP 连接,发送PING,主库回+PONG。如果这一步就失败,说明网络不通、端口不对、或者主库根本没起来。这是最常见的一类问题,用telnet或nc从从库机器上试一下就知道了。

第二步,如果主库配置了requirepass,从库需要用masterauth里的密码做AUTH。这里有个非常经典的坑:主库有密码但从库没配masterauth,从库连上之后一直报NOAUTH,状态卡在down。另一个反向的坑是主从密码不一致,日志里会有Unable to AUTH to MASTER。

第三步,从库发送REPLCONF listening-port <port>,告诉主库自己监听的端口,主库会把这个信息记进INFO replication的slaveN行里。同时发送REPLCONF capa eof capa psync2,声明自己支持无盘传输和 PSYNC2 协议。如果这里能力协商失败,就会退化成老版本的SYNC,每次都是全量。

第四步,从库发送PSYNC <replid> <offset>。首次连接时是PSYNC ? -1,意思是“我没有历史,给我全量”。主库收到后,如果判断无法做部分重同步,就返回+FULLRESYNC <replid> <offset>,把当前主库的 replid 和最新偏移量告诉从库,随后开始全量流程。

这四步里任何一步失败,从库的master_link_status都会是down,master_last_io_seconds_ago会持续增长。排查顺序就按这四步来,基本能覆盖 90% 的连接类故障。

3.2 FULLRESYNC 期间的三份数据,缺一不可

全量同步是整个主从机制里最“重”的操作,也是故障高发区。理解它的关键,是知道在 fork 出去生成 RDB 的那段时间里,主库同时在处理三份数据。

第一份是基准快照。主库调用fork()创建子进程,子进程把当前内存里的数据写成一个 RDB 文件(或者直接通过 socket 写给从库,这就是无盘复制)。fork 本身是写时复制(COW),阻塞时间通常很短,但内存页表大的时候可能达到几十毫秒甚至上百毫秒,这段时间主库是完全停止响应的。

第二份是增量缓冲。子进程在写 RDB 的整个过程中,主库还在接收新的写命令。这些命令不能丢,会被追加到该从库的复制输出缓冲区里,等 RDB 传完之后再补发。这部分数据量取决于 RDB 生成耗时和写入压力,如果 RDB 写了 30 秒、期间写入很猛,缓冲区可能堆到几百 MB,直接触发client-output-buffer-limit把从库踢掉。

第三份是积压缓冲区。前面提到的repl-backlog,在主库有从库连接期间一直存在,全量同步结束后,从库的偏移量会和它对齐,后续的增量同步就靠它兜底。

从库这边的动作是:收到 RDB 后,先清空自己现有的全部数据(FLUSHALL级别的效果),再加载 RDB。这就是为什么从库在做全量同步期间,内存可能短暂翻倍——旧数据还没释放干净,新数据已经在加载。如果从库内存吃紧,很可能在这个阶段被系统 OOM 杀掉。

无盘复制(repl-diskless-sync)可以省掉磁盘 IO,但需要网络带宽足够,且repl-diskless-sync-delay默认是 5 秒,意思是主库会等 5 秒,看有没有更多从库来连,凑一批一起做全量,减少 fork 次数。这个设计很聪明,但首次同步的从库会多等 5 秒才拿到数据。追求快速同步的场景可以调小这个值,代价是 fork 更频繁。

3.3 增量同步的判定条件与 +CONTINUE

全量同步是最坏情况,日常更关心的是断线重连能不能走增量。主库收到PSYNC <replid> <offset>后,要同时满足两个条件才会回+CONTINUE:

一是从库带来的replid必须等于主库当前的master_replid,或者等于master_replid2。前者是直接续上,后者是跨了一次提升的续上。

二是从库的offset必须落在 backlog 的覆盖范围内。backlog 有一个repl_backlog_first_byte_offset表示最老的有效位置,从库的 offset 如果小于这个值,说明那段数据已经被覆盖了,只能全量。

这两个条件缺一个,主库就回+FULLRESYNC。所以你会看到一个很有意思的现象:网络只抖了两秒,从库重连却触发了全量。原因往往就是 backlog 太小,两秒的写入量已经把 1MB 的缓冲区冲掉了。

+CONTINUE之后,主库会把 backlog 里从该 offset 开始的数据一次性推过去,从库重放完之后就恢复正常的命令流传播模式。之后主库每执行一条写命令,都会异步地写进每个从库的输出缓冲区;从库每秒发一次REPLCONF ACK <offset>告诉主库自己处理到哪了,主库根据这个值更新slaveN的offset字段。

有一条经验值得记住:不要把REPLCONF ACK的间隔当延迟指标。它固定一秒一次,反映的是从库“还活着且处理到了某位置”,而不是“实时延迟”。真正的延迟要靠 offset 差值来算。

4. 动手搭一套:配置项、运行时命令与拓扑选型

4.1 配置文件与运行时命令的等价写法

配置从库有两种方式,效果一样,但适用场景不同。配置文件方式适合固化的生产环境,运行时命令方式适合临时验证和动态调整。

配置文件写法(从库的redis.conf):

replicaof 10.0.0.11 6379 masterauth your_password_here replica-read-only yes replica-serve-stale-data yes repl-timeout 60

改完之后需要重启从库生效。如果主库没有设密码,masterauth这行可以省略;但建议即使当前没密码也先在配置里预留注释,方便后续启用。

运行时命令方式:

# 在从库上执行 config set masterauth your_password_here # 先设置,否则后续连不上 replicaof 10.0.0.11 6379 config rewrite # 写回配置文件,避免重启丢失

顺序很重要。如果先执行replicaof再设密码,从库会先尝试连接、认证失败、状态变成down,然后你设完密码它还要等下一轮重试才恢复。虽然最终也能成,但会多绕一会儿。

反过来,把一个从库提升成主库:

replicaof no one

这条命令会让从库断开与主库的复制关系,保留现有数据,开始接受写请求。注意它不会自动清空masterauth之类的配置,后续如果要把这个实例重新变成从库,直接再replicaof指过去就行。

Redis 5.0 之后推荐用replicaof而不是老的slaveof。两者功能等价,但后者在新版本里已经被标记为兼容保留,新项目没必要再用旧名字。同理,replica-read-only对应老的slave-read-only,INFO replication里的字段也逐步从slave前缀改成了replica,不过为了兼容,很多字段名至今没变,看输出时不用困惑。

4.2 三种拓扑的代价对比

主从的拓扑结构不是随便连的,不同结构对应的资源消耗和故障影响面差别很大。

拓扑结构优点代价与风险
一主多从(星型)所有从库直连主库结构简单,延迟最低主库 fork 一次要服务 N 个从库;从库多时主库出口带宽吃紧;全量同步风暴
级联(树状)主库 → 中间层从库 → 叶子从库分摊主库复制压力,叶子节点多时优势明显层级越深延迟越大;中间层故障会连带影响整棵子树
主从从(链式)主库 → 从库A → 从库B部署简单,适合跨机房延迟累积严重,A 挂了 B 也断;不推荐做业务读

星型结构适合从库数量不多(比如 3 到 5 个)、且都在同一机房的情况。这时候主库的压力可控,延迟也最容易保证。

一旦从库数量上到 10 个以上,或者有跨机房读的需求,就该考虑级联。让每个机房的第一个从库去连主库,同机房的其他从库连这个“代理从库”,主库的复制连接数立刻从 10 降到 3。代价是叶子节点比主库慢两跳,延迟会显著增加。

链式结构我一般不建议用于承载业务读,它的价值更多在于“临时扩一个只读副本做数据分析”,用完就拆。长期挂着容易出现“A 重启了,B 不知道”这类连锁问题。

4.3 搭建完成后的验证清单

搭完之后不能只看master_link_status是不是up,那只能说明连接建立了。我通常按下面这份清单逐项验证:

检查项命令期望结果
角色是否正确主库INFO replicationrole:master,connected_slaves大于 0
从库链路状态从库INFO replicationmaster_link_status:up,master_sync_in_progress:0
偏移量是否追平对比主从master_repl_offset差值在可接受范围内且不持续扩大
数据是否同步主库SET test:key 1,从库GET test:key从库能读到
从库是否只读从库执行SET x 1返回READONLY You can't write against a read only replica
全量后内存正常从库INFO memoryused_memory与主库接近,无异常膨胀

这份清单我在每次扩容从库时都会走一遍,尤其是偏移量那一项。有些环境里从库显示up,但 offset 差值一直在缓慢扩大,说明从库处理能力跟不上主库写入速度,这种“假健康”状态最危险,因为监控看起来是绿的。

5. 复制风暴与延迟雪崩:压测阶段才暴露的五个问题

5.1 全量同步风暴:fork、磁盘 IO 与带宽的三重叠加

最典型的场景是:一次机房网络抖动,五个从库同时掉线,恢复后同时重连、同时请求全量。主库在几秒内连续 fork 五次,每次都要生成完整的 RDB 文件,磁盘写满、CPU 打满、网络出口带宽占满,主库自己的读写请求全部超时,业务侧直接雪崩。

这个问题的根因在于,全量同步对主库来说是重操作,而且是不可控的。缓解手段有三层:

第一层是配置层面。repl-diskless-sync-delay默认 5 秒的作用就是攒批,主库会等一小段时间,把同时来请求的从库合并成一次 fork、一份 RDB、多路分发。这个机制能把五次 fork 压缩成一次,效果非常明显。前提是repl-diskless-sync打开着,这个配置在较新版本里默认开启,老版本默认关闭,动手前用CONFIG GET repl-diskless-sync确认一下。

第二层是结构层面。把从库改成级联,让主库只面对一两个中间层,全量同步的冲击被分散到中间层去扛。中间层自己做全量虽然也会慢,但它不承接业务写,影响面小得多。

第三层是容量层面。主库的内存不要超过物理内存的 50%,给 fork 的写时复制留足空间。同时确保vm.overcommit_memory设为 1,否则 fork 在某些内核配置下会直接失败,日志里报Can't save in background: fork: Cannot allocate memory。

提示:vm.overcommit_memory=1这个参数必须在操作系统层面设置,写进/etc/sysctl.conf,Redis 自身的配置改不了它。这个坑我在至少三个环境里见过,都是主库内存涨上来之后全量同步突然失败。

5.2 输出缓冲区溢出:从库被主库“踢下线”的现场

主库给每个从库维护一个输出缓冲区,用来暂存还没发出去的命令。如果从库消费太慢,缓冲区会越堆越大,超过client-output-buffer-limit replica设置的上限,主库就会主动断开这个从库。

默认值是256mb 64mb 60,三个数字分别是硬上限、软上限、软上限持续的秒数。含义是:缓冲区超过 256MB 立即断开;或者超过 64MB 且持续 60 秒也断开。

这个机制本身是保护主库的,防止慢从库把主库内存吃光。但它带来的现象很容易误判:从库日志里会看到连接被重置,重连之后又触发全量,全量期间又慢,又被断开,形成循环。

排查这类问题的思路是:先在主库上观察INFO clients里的client_recent_max_output_buffer,看缓冲区峰值。如果确实接近上限,再分析从库为什么慢。常见的慢因包括:从库上有大 key 操作、从库执行了KEYS或大范围SCAN、从库磁盘 IO 打满(开了 AOF 且appendfsync always)、从库网络带宽不足。

调整上限要谨慎。直接调大能缓解断开,但只是把问题延后,真正的解法是让从库消费能力跟上。如果确实是因为业务读把从库拖慢了,那应该考虑再加一个从库分流,而不是一味调大缓冲区。

5.3 延迟怎么量:从 offset 差值到链路耗时

延迟的量化有两层,一层是复制链路的延迟,一层是业务感知的延迟,两者不是一回事。

复制链路延迟用 offset 差值算最直接。在主库上连续两次INFO replication,间隔 10 秒,记录master_repl_offset的增量和某个从库offset的增量,两者的差就是这 10 秒内从库落后的量。除以写入速率,大致能换算出落后多少秒。

但业务感知的延迟还受另外两个因素影响:一是从库上是否有慢查询阻塞了事件循环,二是客户端到从库的网络路径。曾经遇到过一个案例,主从 offset 差只有几十 KB,理论上延迟不到 10ms,但业务侧读到的数据延迟了将近 1 秒。最后定位到是从库上有个定时任务每分钟跑一次全量SCAN,把事件循环卡住了。这种问题光看复制指标永远发现不了。

所以监控要分层:复制层看 offset 差值和master_last_io_seconds_ago,执行层看SLOWLOG和latency相关指标,业务层做端到端的写后读校验打点。三层都有数据,定位问题才快。

5.4 树状拓扑的延迟累积与读一致性代价

级联拓扑解决了主库压力,但引入了延迟累积。主库到中间层是一跳,中间层到叶子是第二跳,叶子的数据至少要经过两次传播。如果每一跳平均 20ms,叶子节点就稳定落后 40ms 以上,而且一旦中间层出现瞬时阻塞,叶子的延迟会被放大。

这个代价在读写分离场景里必须算进去。假设业务要求“写后 100ms 内读到新值”,那叶子节点就不适合承接这类读请求,应该由中间层或者主库来承担。实际做法通常是给叶子节点打标签,让业务按一致性要求路由到不同层级的副本。

还有一个容易被忽略的点:级联结构下,中间层从库如果开启了replica-read-only yes(默认就是),它既是主库的从库,又是叶子节点的主库,这个角色是自动的,不需要额外配置。但它同时也承接了业务读,一旦业务读把它的内存或者 CPU 打满,叶子节点的复制就会受影响。所以中间层最好独立部署,不要和业务读的从库混用。

6. 几个反直觉的复制语义:只读、过期键与内存策略

6.1 replica-read-only 之后的写请求发生了什么

从库默认replica-read-only yes,此时任何写命令都会被拒绝,返回READONLY You can't write against a read only replica。这个错误信息很明确,但在实际项目里经常被应用框架吞掉,表现成“偶发的写入失败”,排查起来很费时。

有几种情况会导致写请求打到从库上:客户端连接池配置错误、读写分离中间件路由规则写反、主库故障切换后应用配置没更新、某些 SDK 在重连时随机选节点。这些都不是 Redis 的问题,但表现都是从库报READONLY。

从库能不能设成可写?可以,replica-read-only no。但我强烈不建议在业务环境这么做。一旦从库可写,本地写入的数据和主库同步过来的数据会混在一起,主库执行FLUSHALL或者做全量同步时,这些本地数据会被直接清掉,而且不会有任何提示。这种“看起来能写、实际上随时会丢”的状态是最坑的。

真要往从库写数据,正确做法是单独起一个实例,不要和主从链路混在一起。如果是因为某些临时脚本需要写,那也应该直连主库,或者用独立实例。

6.2 从库上的过期键:逻辑过期与物理残留

这个问题非常反直觉,但理解了之后对排查“数据不一致”类问题帮助很大。

Redis 的过期键删除有两种触发方式:被动删除(访问时发现过期就删)和主动删除(后台定时抽样删除)。在从库上,主动删除是被禁用的,从库不会自己删过期键,它只等主库发来的DEL命令。这个设计是为了保证主从数据一致:如果从库自己删了、主库还没删,那主从就不一致了。

带来的现象是:一个键在主库上已经过期消失了,但从库的DBSIZE里可能还统计着它,INFO keyspace的expires数量也可能对不上。不过从 Redis 3.2 开始,从库在读取到逻辑上已过期的键时,会返回空值(当作不存在),所以业务侧读到的是“没有这个键”,而不是“读到了过期数据”。物理数据会一直留着,直到主库的DEL传播过来。

这个机制在内存排查时会造成困惑:从库的used_memory比主库高出一截,看起来像是内存泄漏。实际情况往往是大量已过期但未被删除的键还占着内存。等主库的过期删除跑完、DEL传播过来,从库的内存就会回落。

需要提醒的是,如果主库的过期策略是volatile-lru之类的淘汰策略,被淘汰的键会以DEL的形式传播给从库,这部分是同步的。真正会造成从库内存偏高的,主要是“过期了但还没被主库主动删除”的那批键。

6.3 maxmemory 在从库上的行为差异

从库不主动删除键,这一点也直接影响了maxmemory的行为。从库不会像主库那样在内存达到上限时触发淘汰,它只是老老实实地执行主库传过来的命令。

这意味着如果从库配置了和主库一样的maxmemory,很容易出现从库内存超限的情况。因为从库上还留着那批“逻辑过期但物理未删”的键,实际占用会比主库高。超限之后如果淘汰策略是noeviction,从库执行写命令会返回 OOM 错误,复制流会中断,日志里出现OOM command not allowed when used memory > 'maxmemory'。

实操建议很简单:从库的maxmemory要么不设,要么设得比主库大 20% 到 30%。同时监控从库的内存增长曲线,如果持续高于主库且差距在扩大,那就要去查是不是有过期键堆积、或者主库的淘汰策略导致从库内存结构不同。

还有一层是maxmemory-policy。从库上这个配置实际上不生效(因为不主动淘汰),但保持和主库一致是个好习惯,方便后续提升为主库时行为正确。切主之后如果策略不一致,新主可能会突然开始大量淘汰,把业务打懵。

7. 手工切主与回归:没有哨兵时的兜底操作

7.1 判断“该切了”的三个信号

没有哨兵的场景下,切主是人工决策,判断依据必须清晰,否则很容易出现“犹豫太久业务已经挂了”或者“切早了其实主库只是抖了一下”。

第一个信号是主库完全失联。从库上master_link_status变成down,master_last_io_seconds_ago持续增长超过repl-timeout,同时从其他途径(比如 ping、监控)确认主库进程或机器确实挂了。如果只是从库单方面认为主库挂了,但主库其实活着,那可能是网络分区,这时候切主会造成双主。

第二个信号是主库无法恢复。确认主库重启需要很长时间(比如磁盘故障需要重建),业务等不起,那就得切。这时候要接受“主库上最后一段未同步的写入会丢失”这个事实。

第三个信号是写入完全阻塞。如果主库还活着但已经无法处理写请求(比如被DEBUG SLEEP卡住、或者内存打满无法写入),业务侧已经全面报错,那切换是止损手段。

我个人的判断习惯是:先看监控确认主库是真的挂了还是只是网络抖动,再看从库的 offset 是不是最新的(差距越小,切换丢的数据越少),最后确认应用侧能不能快速改连接。三个条件都满足再动手,避免误切。

7.2 切主操作序列与旧主回归的脏数据处理

假设主库是10.0.0.11,选定的新主是从库10.0.0.12,其他从库是10.0.0.13、10.0.0.14。操作序列如下。

第一步,在新主上检查数据完整性:

INFO replication # 确认 master_link_status 曾经是 up,slave_repl_offset 尽量接近主库最后的值 DBSIZE # 记录键数量,切完后对比

第二步,提升新主:

replicaof no one CONFIG REWRITE

第三步,让其他从库改挂新主:

# 在 10.0.0.13 和 10.0.0.14 上执行 replicaof 10.0.0.12 6379 CONFIG REWRITE

第四步,更新应用侧连接配置,把写请求指向10.0.0.12。这一步越快越好,中间的空窗期业务是报错的。

第五步,处理旧主回归。这是最容易出事的一步。旧主恢复后,它本地还有一份数据,这份数据里可能有“已经写成功但没同步给从库”的记录,也可能有其他从库都没有的脏数据。绝对不能直接把它挂成新主的从库,否则全量同步会把新主的数据覆盖成旧主的状态(虽然全量同步的方向是主到从,但如果旧主自己先被当成主库对外服务过,那问题更复杂)。

正确做法是:旧主恢复后,先不要挂到主从链路里,而是单独启动、用redis-check-rdb校验数据文件、导出需要保留的差异数据,确认无误后再执行replicaof 10.0.0.12 6379,让它做一次全量同步,把本地数据彻底覆盖掉。如果旧主上有确实没同步出去的重要写入,那得单独捞出来人工合并,不能指望自动流程。

注意:旧主回归前一定要确认它的replicaof配置是清空状态,否则它可能自己认为自己还是主库,接受写请求,造成双主写入。这个检查别省。

7.3 用 WAIT 与 min-replicas-to-write 兜住写丢失窗口

异步复制天然有丢数据的窗口,Redis 提供了两个手段来收窄这个窗口,虽然都不能做到强一致,但比什么都不做强得多。

WAIT numreplicas timeout是个同步命令,执行后客户端会阻塞,直到指定数量的从库确认收到了之前的写命令,或者超时。用法:

SET order:1001 paid WAIT 1 100

含义是:等至少 1 个从库确认收到SET order:1001 paid这条命令,最多等 100 毫秒。返回值是实际确认的从库数量,如果返回 0,说明超时了,这条数据可能还在路上。

这个命令的价值在于,对于关键写操作,你可以用它来确保数据至少落到了一个从库上。但它不是强一致:超时之后命令其实已经执行了,只是没等到确认;而且它只保证“确认收到”,不保证从库已经把数据落盘。

min-replicas-to-write是另一个角度的保护,它控制的是“主库在有多少个健康从库的情况下才允许写入”:

min-replicas-to-write 1 min-replicas-max-lag 10

配置含义是:至少要有 1 个从库,且它的lag不超过 10 秒,主库才接受写请求。否则主库直接返回错误,拒绝写入。

这个配置在防止脑裂导致的数据丢失上很有用。想象一下主库和所有从库之间的网络断了,主库还活着但成了孤岛,如果没有这个配置,主库会继续接受写入,等网络恢复后这些写入全都会被新主的数据覆盖掉。有了这个配置,主库发现自己没有健康从库,直接拒绝写入,业务侧立刻感知到异常,反而能更快触发人工介入。

代价是可用性下降:从库全挂的时候主库也写不了了。所以这个值要结合业务容忍度来设,通常设 1 就够了,设 2 或者更高会让可用性变得很脆弱。

# 查看当前生效值 CONFIG GET min-replicas-to-write CONFIG GET min-replicas-max-lag

最后分享一个小技巧:做切主演练的时候,不要只演练“主库直接挂掉”这一种场景。更值得演练的是“主库挂掉、从库提升、旧主恢复、旧主重新加入”这个完整闭环,因为真正的事故往往出在最后一步。演练时把每一步的命令和时间点记下来,形成一份可执行的 checklist,真出事的时候照着敲就行,比临场思考靠谱得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 20:11:12

RAID原理、选型与故障处理:从条带化、镜像、校验到RAID 5/6/10实战

RAID 这个词&#xff0c;大多数人第一次见到是在装机店的配置单上&#xff0c;第二次见到是在机房的告警邮件里。它的中文名叫独立磁盘冗余阵列&#xff0c;核心思路说白了就一句话&#xff1a;把几块普通硬盘用条带化、镜像、校验这三种基本手法组合起来&#xff0c;对外呈现成…

作者头像 李华
网站建设 2026/10/27 20:11:12

多波束测线几何建模与最短布设:从覆盖宽度到海深分块求解

简介&#xff1a;这份面向数学建模国赛选手与海洋测量方向学习者的获奖论文PDF&#xff0c;聚焦2023年高教社杯B题“多波束测线问题”&#xff0c;可帮助读者理解赛题建模思路、论文写作结构与结果验证方式。压缩包仅含1个PDF文件&#xff0c;约1.24MB&#xff0c;篇幅完整&…

作者头像 李华
网站建设 2026/10/27 20:11:11

Codex前端组件秒级生成:破解设计-开发鸿沟的工程实践

1. 项目概述&#xff1a;Codex 不是魔法&#xff0c;而是前端工程效率的临界点“Codex 破局&#xff1a;前端组件秒级生成”——这个标题里藏着三个被严重低估的关键词&#xff1a;破局、秒级、生成。不是“辅助写代码”&#xff0c;不是“智能补全”&#xff0c;更不是又一个A…

作者头像 李华
网站建设 2026/10/27 20:10:50

48小时原始人生活实验:庇护所搭建、钻木取火与无具烹饪全攻略

“caveman”这个词在我手机备忘录里躺了整整三个月。三个月后我把它变成了一场48小时的原始生活实验&#xff1a;不带手机充电器、不带炊具、不带帐篷&#xff0c;只靠一双手和提前准备好的少量工具&#xff0c;在郊野营地里完成搭庇护所、钻木取火、无具烹饪这三件事。整个过程…

作者头像 李华
网站建设 2026/10/27 20:10:37

STR混合图谱四任务建模:人数判别、比例估计与基因型推断

简介&#xff1a;面向2025年深圳杯数学建模竞赛D题的完整分析论文&#xff0c;适合参赛队伍复盘、数学建模学习者及需要分类建模与优化求解思路的研究者参考。全文围绕赛题重述、问题分析、模型假设与符号定义展开&#xff0c;重点给出问题一至问题三的建模与求解过程&#xff…

作者头像 李华
网站建设 2026/10/27 20:10:32

Codex前端组件生成原理:语义-结构-行为三层解析

1. 项目概述&#xff1a;当“写组件”变成“说组件”&#xff0c;前端开发的临界点到了Codex 破局——这四个字不是营销口号&#xff0c;而是我上个月在团队内部技术复盘会上脱口而出的真实感受。那天我们刚用 Codex 辅助完成一个电商后台的「商品 SKU 配置弹窗」组件&#xff…

作者头像 李华