Redis 持久化与主从同步

RDB、AOF、AOF 重写与复制链路的工作机制、权衡与版本差异

Posted by Ekko on October 19, 2025

这篇笔记把 Redis 的数据落盘与复制链路放在一起梳理,核心是弄清楚 RDB、AOF、AOF 重写、全量复制、增量复制分别在什么时机发生,谁负责执行,哪里可能阻塞,哪里只保证最终一致。

内容以 Redis Open Source 的通用机制为主,尽量保留原始笔记结构,同时补上容易混淆的版本差异,尤其是 Redis 7.0 之后的 multi-part AOF,以及复制里 replication id、offset、backlog 的配合关系。

参考资料:

Redis persistence

Redis replication

BGREWRITEAOF

[TOC]


1.RDB快照持久化

RDB 的本质是某一时刻内存数据集的二进制快照。它适合备份、灾备、快速恢复,也常用于主从全量复制时给从节点传输初始数据。

1
2
3
4
# RDB 常见自动快照策略
save 900 1    # 900 秒内至少有 1 个键被修改
save 300 10   # 300 秒内至少有 10 个键被修改
save 60 10000 # 60 秒内至少有 10000 个键被修改

RDB文件逻辑结构

可以先按“理解版”记成下面这个顺序:

[魔数 “REDIS”] -> [4 字节 ASCII 版本号] -> [可选辅助元数据 AUX] -> [一个或多个数据库片段] -> [EOF 0xFF] -> [8 字节校验和]


魔数(Magic Number): 文件类型标识

  1. 位置:文件最开头,占 5 字节。
  2. 内容:固定为字符串 REDIS 的 ASCII 编码。
  3. 作用:Redis 加载文件时先校验魔数,确认这是合法的 RDB 文件。

版本号(RDB Version): 文件格式版本标识

  1. 位置:魔数之后,占 4 字节。
  2. 内容:不是二进制大端整数,而是 4 个 ASCII 字符,例如 00090010
  3. 作用:不同版本的 RDB 在 opcode、编码方式、模块扩展数据上可能存在差异,Redis 会按版本选择解析逻辑。

可选元数据(Optional Metadata / AUX): 文件辅助信息

  1. 位置:通常出现在版本号之后、正式数据区之前,也可能穿插少量辅助 opcode。
  2. 内容:常见包括 Redis 版本、创建时间、复制相关信息、内存使用提示等。
  3. 作用:主要用于兼容性、调试和运维观察,不是“键值数据本体”。

数据库数据区(Database Data): 核心数据存储

  1. 这是 RDB 文件中体积最大的部分。
  2. Redis 会按数据库维度写入数据,常见会看到 SELECTDBRESIZEDB、过期时间相关 opcode,再跟上具体键值对。
  3. 值对象会按类型使用不同编码方式,例如字符串、列表、哈希、集合、有序集合的序列化格式并不相同。

EOF 标记: 数据区结束标识

  1. 位置:所有数据库数据写完后,占 1 字节。
  2. 内容:固定为 0xFF
  3. 作用:告诉解析器核心数据区已经结束,后面只剩校验信息。

校验和(Checksum): 文件完整性校验

  1. 位置:文件尾部,占 8 字节。
  2. 内容:默认是对前面全部内容计算出的 64 位校验值,常见实现是 CRC64。
  3. 作用:Redis 加载 RDB 时会重新计算校验值,不一致则认为文件可能损坏。

注意:如果只是为了理解持久化机制,不必死记每个 opcode;更重要的是知道 RDB 是“面向恢复的二进制快照”,不是命令日志。


save 命令:同步阻塞,不适合在线业务

执行逻辑: SAVE 由 Redis 主线程直接遍历内存数据并写出 RDB。执行期间事件循环被占住,Redis 不能正常处理新的客户端请求。

阻塞表现: 更准确的说法不是“拒绝所有请求”,而是客户端请求通常会排队等待,超时后才表现为业务侧报错。

典型场景: 只适合调试、维护窗口或非常小的数据集,不建议在生产流量下手工执行。若要在关闭前强制落盘,更常见的表达是使用 SHUTDOWN SAVE


bgsave 命令:主线程继续服务,子进程负责落盘

执行逻辑: BGSAVE 由主进程调用 fork() 创建子进程,子进程负责生成 RDB 文件,主线程继续处理命令请求。

潜在阻塞点: 真正容易抖动的时刻是 fork() 本身。数据集越大、页表越大、内存碎片越明显,fork() 的暂停时间越容易被放大。

COW 机制影响: 子进程拿到的是 fork 时刻的内存视图。之后主线程若修改某个内存页,会触发写时复制(COW),让父子进程各自持有不同页副本。因此高写入流量下,快照期间的额外内存占用可能明显上升。


RDB相关并发限制

更准确的记忆方式是:同一时刻不会并发运行多个重型后台持久化子进程

  1. 已有 BGSAVE 在执行时,再发起 BGSAVE 会被拒绝。
  2. SAVE 是同步命令,执行期间主线程本身就被占住,因此不存在“两个 SAVE 并发跑”的实际意义。
  3. BGSAVEBGREWRITEAOF 都依赖 fork() 子进程,Redis 会避免让多个这类后台子进程同时重叠运行,以控制 CPU、内存和 IO 压力。

RDB文件生成过程

RDB生成_1.png

SAVE 是主线程操作,所以会阻塞新的读写请求。下面重点看 BGSAVE 的过程。

1、主进程准备:fork 子进程

2、子进程生成 RDB 数据:遍历与序列化

3、主进程并发处理:写时复制(COW)保障数据一致性

  1. 极端情况下,如果快照期间绝大多数内存页都被改写,额外内存占用可能接近原数据集规模。

4、子进程完成写入,原子替换目标文件

  1. 子进程先写临时文件,完成后再通过原子重命名替换旧 RDB 文件。
  2. 子进程退出后,操作系统会向主进程发送 SIGCHLD,主进程据此回收子进程资源并更新持久化状态。

5、主进程清理资源与更新状态


RDB补充知识点

  1. LASTSAVE 可以查看最近一次成功生成 RDB 的时间戳。
  2. stop-writes-on-bgsave-error yes 是常见保护项,避免后台快照持续失败时主节点还继续接收写入。
  3. RDB 很适合备份和快速重启,但不适合拿来承诺“几乎不丢数据”;它本质上是时间点快照,不是逐条写入日志。

2.AOF持久化

AOF 的核心思路不是“保存某一时刻结果”,而是记录会修改数据集的写命令。Redis 重启后通过重放这些命令来恢复数据。

1
2
3
appendfsync always   # 每次写命令后都执行 fsync
appendfsync everysec # 每秒 fsync 一次,默认策略
appendfsync no       # 不主动 fsync,由操作系统决定何时刷盘

可以把 AOF 的写入链路拆成四步:

  1. 执行写命令,先修改内存中的真实数据。
  2. 把对应协议内容追加到 AOF 缓冲区(aof_buf)。
  3. 主线程调用 write(2),把数据写到 AOF 文件描述符,通常先进入内核页缓存。
  4. 再由 appendfsync 策略决定何时真正 fsync 到磁盘。

问题一:Redis执行命令成功,但是在写AOF之前宕机

数据可能丢失。

更细一点说,写命令“执行成功”只代表内存状态已经更新,不等于已经持久化成功。崩溃点不同,风险窗口也不同:

  1. 如果命令刚改完内存,还没追加到 aof_buf,那么这条写入肯定无法通过 AOF 恢复。
  2. 如果已经写入 aof_buf,但还没 write 到内核缓冲区,也可能丢。
  3. 如果已经 write 到页缓存,但还没 fsync,那么是否能保住,取决于内核何时真正落盘;这也是 everysecno 会有数据丢失窗口的原因。

问题二:RDB文件是fork子进程生成,AOF刷盘是由谁操作

AOF 的 命令执行AOF 追加写入 主要由主线程负责;fsync 是否放到后台线程,要看 appendfsync 策略。

  1. appendfsync always:主线程执行完一批写命令后,会完成 AOF 写入并显式 fsync,然后再向客户端返回结果。延迟最高,但持久化窗口最小。
  2. appendfsync everysec:主线程仍负责把数据写到 AOF 文件,fsync 通常由后台线程每秒执行一次。正常情况下吞吐和延迟更平衡,通常最多丢 1 秒左右数据。
  3. appendfsync no:主线程只负责写入内核缓冲区,不主动 fsync,真正刷盘交给操作系统。它通常最快,但并不等于“绝对无阻塞”,因为内核缓冲区压力大时,write 也可能卡住主线程。

一个容易记错的点:everysec 不是“主线程完全不碰磁盘”,而是主线程仍然要做 AOF write,只是把最重的 fsync 尽量移给后台线程。


appendfsync 三种策略的权衡

  1. always
    1. 优点:持久化窗口最小,客户端收到成功响应时,这批写入通常已经过 fsync
    2. 缺点:延迟最高,不适合高吞吐写场景。
  2. everysec
    1. 优点:默认策略,性能与可靠性平衡较好。
    2. 缺点:遇到进程崩溃、机器掉电时,通常仍可能丢失最近 1 秒左右数据;如果底层 IO 很忙,也可能出现短暂抖动。
  3. no
    1. 优点:省掉主动 fsync,写入开销最低。
    2. 缺点:数据安全最弱,丢失窗口取决于内核刷盘时机,实践里可能远大于 1 秒。

AOF补充知识点

  1. AOF 比 RDB 更耐久,但通常文件更大、恢复速度也可能更慢。
  2. Redis 4.0 之后,AOF 重写结果可以包含 RDB preamble,这样恢复更快;因此“重写后的 AOF 文件一定全是可读命令文本”并不总成立。
  3. AOF 不是备份系统。它解决的是“实例重启后的恢复”,真正的灾备仍然需要离机备份和多副本。

3.AOF重写

AOF 会随着写命令不断增长,而很多历史命令对“恢复当前状态”其实已经没有意义。

例如一个 key 被反复 SETINCRDEL 很多次,最终只需要保留能构造出“当前最终状态”的那组最小命令或快照。AOF 重写的目标就是把这份“当前状态表达”重新生成出来。

1
2
3
4
5
# 当前 AOF 文件体积比上次重写后体积增长了 100% 时,满足增长条件
auto-aof-rewrite-percentage 100

# 当前 AOF 文件至少达到 64MB 时,满足最小体积条件
auto-aof-rewrite-min-size 64mb

Redis 会周期性检查是否满足自动重写条件:

  1. 当前 AOF 文件已经达到最小体积门槛。
  2. 相比上次重写后的基线体积,增长比例已经达到阈值。

若同时满足,就会自动触发 BGREWRITEAOF

首次重写更适合理解为:主要先看 auto-aof-rewrite-min-size,因为还没有稳定的历史基线,增长比例条件不会成为真正门槛。

AOF 重写并不是去“裁剪旧 AOF 文件”,而是遍历当前内存数据集,重新生成一份能够恢复当前状态的新持久化结果


AOF重写过程:Redis < 7.0 的经典模型

下面这套“双缓冲区 + 临时文件 + rename”描述,更贴近 Redis 7.0 之前的单文件 AOF 机制。

1、主进程 fork 子进程

  1. 子进程继承 fork 时刻的内存快照,依赖 COW 共享页面。

2、子进程生成新的 AOF 临时文件

  1. 子进程遍历当前内存数据。
  2. 为每个 key 生成更精简的恢复命令。
  3. 将这些内容写入临时 AOF 文件。

3、主进程处理新命令(双缓冲区机制)

重写期间主线程仍然要处理新的客户端写命令。为了保证新命令不丢失,主线程会同时做两件事:

  1. 把新命令继续追加到旧 AOF 文件,保证旧文件始终可用。
  2. 把新命令写入 AOF 重写缓冲区,等子进程完成后再补到新文件末尾。

4、主进程合并新命令并原子替换旧文件

  1. 子进程写完临时文件后退出。
  2. 主进程收到 SIGCHLD 后,把重写缓冲区中的增量命令追加到临时新文件。
  3. 主进程调用 rename() 原子替换旧 AOF 文件。
  4. 旧文件被新文件顶替,重写完成。

Redis 7.0+ 的变化:multi-part AOF

Redis 7.0 开始,AOF 不再一定只有一个 appendonly.aof 文件,而是拆成了:

  1. base AOF:重写时生成的基线文件,可能是 AOF 格式,也可能带 RDB preamble。
  2. incremental AOF:重写过程中以及之后产生的增量文件。
  3. manifest:记录当前哪些 base / incremental 文件有效的清单文件。

这一版机制的核心变化是:

  1. 父进程在重写开始后会打开新的增量 AOF 文件继续接收写入。
  2. 子进程负责生成新的 base AOF。
  3. 完成后父进程原子切换 manifest,而不是只做“单个 appendonly.aof 文件的 rename 覆盖”。

所以如果面试或复习时提到“AOF 重写最后一定是 rename 覆盖旧 appendonly.aof”,最好补一句:这是 Redis 7.0 之前更典型的描述;Redis 7.0+ 需要结合 multi-part AOF 来理解。


问题一:AOF重写时,怎么保证新来的指令最终一致

核心靠两层保障:

  1. 主线程不会停止处理新写命令。
  2. 这些新命令会被额外保存下来,并在切换新 AOF 生效前补进去。

在 Redis < 7.0 中,这通常体现为 rewrite buffer;在 Redis 7.0+ 中,则体现为新的 incremental AOF 与 manifest 切换。


问题二:AOF重写失败怎么办

重写失败不会直接破坏当前可用数据,原因是:

  1. 重写期间旧 AOF 仍持续接收新的写命令。
  2. 新文件或新 manifest 未切换成功前,旧的持久化链路仍然有效。
  3. 因此失败后通常只是“本次重写没完成”,而不是“实例马上失去恢复能力”。

4.主从同步

从 Redis 2.8 开始,复制协议的重要变化之一是 PSYNC 与部分重同步(partial resynchronization)。

重要概念:replication id、复制偏移量、复制积压缓冲区、全量复制、增量复制


先把三件关键状态记住

  1. replication id(replid):标识一条复制历史分支。主节点发生故障转移后,新的主节点通常会产生新的 replid。
  2. 复制偏移量(offset):表示复制流已经处理到哪里,本质上是字节流位置。
  3. 复制积压缓冲区(replication backlog):主节点保留最近一段复制流内容的环形缓冲区,用于断连后的部分重同步。

只有把这三者放在一起,才能真正解释“为什么有时能增量同步,有时必须全量同步”。


主从关系建立

  1. 从节点启动后,或执行 REPLICAOF(旧命令名是 SLAVEOF)后,会主动与主节点建立 TCP 连接。
  2. 如果主节点开启了认证,从节点需要通过 masterauth,或者基于 ACL 的 masteruser / masterauth 完成认证。
  3. 连接建立后,双方进入复制握手阶段,后续才判断走全量同步还是部分重同步。

首次同步(全量复制)

当从节点第一次接入主节点,或者断连时间过长导致 backlog 已经覆盖掉缺失数据时,就会触发全量复制。


1、从节点发送同步请求

  1. 首次连接时,可以把它理解为从节点发送 PSYNC ? -1,表示“我没有可复用的复制历史,请给我一份完整数据”。

2、主节点准备全量数据

  1. 主节点执行一次 BGSAVE,由子进程生成 RDB 快照。
  2. 生成 RDB 的同时,主节点继续处理新的写命令,并把这些增量变更写入复制积压缓冲区。
  3. 主节点在全量复制期间通常仍可继续对外服务,这也是 Redis 复制在主节点侧基本非阻塞的原因。

3、主节点发送 RDB 文件

  1. 子进程生成 RDB 后,主节点把 RDB 内容通过复制连接发送给从节点。
  2. 发送期间若还有新的写命令到来,这些命令仍继续进入 backlog,等待稍后补发。

4、从节点加载 RDB 文件

这里要比“先清空内存再加载”说得更细一点:

  1. 从节点在接收全量数据期间,在很多配置下仍可以继续对外提供旧数据读服务。
  2. 但当真正切换到新数据集时,旧数据需要被丢弃,新 RDB 需要被加载到内存。
  3. 这个切换窗口会阻塞从节点请求;数据越大,阻塞越明显。

也就是说,从节点不是从一开始就完全不可用,而是在最终替换旧数据并加载新快照的窗口里会明显阻塞。


5、主节点补发 backlog 中的增量命令

  1. 从节点加载完 RDB 后,主节点再把 RDB 生成和传输期间积压在 backlog 中的写命令继续发给从节点。
  2. 从节点执行这些命令后,就追平到了主节点当前状态。

6、同步完成:进入稳定复制状态

  1. 从节点完成全量同步后,后续进入持续命令流复制阶段。
  2. 从节点默认通常处于只读模式,即 replica-read-only yes

增量复制

全量复制完成后,主从进入持续的命令传播阶段。主节点把后续写命令不断推给从节点,从节点顺序执行这些命令。


1、主节点实时传播写命令

  1. 主节点执行写命令后,除了更新自身内存,也会把这条命令对应的复制流发送给所有从节点。
  2. 这个过程默认是异步复制,主节点不会为了等待所有从节点确认而阻塞当前写请求。

2、从节点执行命令并更新偏移量

  1. 从节点接收复制流后顺序执行。
  2. 主从两端都会推进自己的复制偏移量,用于判断已经同步到什么位置。

3、心跳机制维持连接

  1. 从节点会周期性向主节点发送 REPLCONF ACK <offset>,告诉主节点自己已经处理到哪个偏移量。
  2. 主节点可以根据 ACK 判断从节点落后程度,并结合 backlog 决定是否还能做部分重同步。
  3. 如果长时间收不到对端心跳,连接会被判定超时并重连,这个阈值通常和 repl-timeout 等配置有关。

断连后同步

如果主从链路暂时断开,重连后优先尝试部分重同步,而不是一上来就全量复制。


1、从节点发起重连请求

  1. 从节点重连时,会带上自己记住的 replid 与 offset,请求继续同步缺失的那一段复制流。

2、主节点判断同步方式

  1. 可以增量同步: 主节点发现从节点带来的 replid 仍然可识别,且缺失的那段数据还在 replication backlog 中,那么只补发缺失命令即可。
  2. 必须全量同步: 如果 replid 已经不匹配,或者缺失数据已经被 backlog 覆盖掉,就只能重新做一次全量复制。

这也是为什么只记 offset 不够,还必须同时理解 replid 和 backlog


核心注意事项

  1. Redis 复制默认是异步的,所以“主从同步完成”并不等于强一致,只能保证最终一致。
  2. WAIT 命令可以降低主节点刚写入就故障时的数据丢失概率,但它也不等于把 Redis 变成强一致数据库。
  3. backlog 大小非常关键。写流量大、网络抖动频繁时,backlog 太小会让部分重同步频繁退化成全量复制。
  4. 全量复制对主节点、从节点、网络都很重,尤其是大内存实例,要重点关注 fork 延迟、网络带宽和从节点加载窗口。

问题:增量同步涉及RDB吗,从节点会不会阻塞读操作

增量同步本身不涉及重新传输和加载 RDB。

  1. 部分重同步 的核心是补发缺失的复制命令流,而不是重新发送快照文件。
  2. 因此不会出现“像全量复制那样重新加载 RDB”的大阻塞窗口。
  3. 但从节点执行复制命令本身仍然在主线程完成,高写流量下读请求依然可能感知到轻微延迟,只是通常比全量同步小得多。

5.选型建议与常见误区

  1. RDB 适合备份、灾备、快速重启。 如果更关心恢复速度和备份体积,RDB 很合适。
  2. AOF 适合更高的数据耐久要求。 如果不能接受“丢最近几分钟数据”,通常至少会考虑 AOF everysec
  3. 线上常见做法是 RDB + AOF + 副本。 RDB 提供快照与备份便利,AOF 提供更小的数据丢失窗口,副本提供高可用,但三者职责并不相同。
  4. 复制不等于持久化。 只有主从复制而没有落盘,机器整体掉电时仍可能一起丢数据。
  5. 持久化也不等于备份。 无论是 RDB 还是 AOF,本质上都更偏“实例恢复”,真正的灾备仍需要把文件异地保存。
  6. 版本差异必须带着记。 讨论 AOF 重写时,如果不说明 Redis 7.0+ 的 multi-part AOF,很容易把旧版本经验当成通用结论。