云计算百科
云计算领域专业知识百科平台

【Redis进阶系列】:从 RDB 到 AOF:Redis 持久化、刷盘与重写机制详解

🔥 本文专栏:Redis 🌸作者主页:努力努力再努力wz

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

💪 今日博客励志语录:你可以暂时没有结果,但不能长期没有积累。


思维导图

Redis 持久化

├── 为什么需要持久化
│ ├── Redis 不只是缓存
│ ├── 数据主要驻留内存
│ └── 内存掉电后数据丢失

├── RDB
│ ├── SAVE
│ ├── BGSAVE
│ │ ├── fork
│ │ └── Copy On Write
│ ├── 自动触发 save point
│ ├── 临时文件 + rename
│ └── 周期性快照 → 存在数据窗口

└── AOF

├── 记录写操作,而不是只保存最终状态

├── RESP 命令日志

├── AOF Buffer
│ ↓
│ write()
│ ↓
│ Page Cache
│ ↓
│ fsync()
│ ↓
│ Disk / SSD

├── appendfsync
│ ├── always
│ ├── everysec
│ └── no

├── AOF Rewrite
│ ├── 不是扫描旧 AOF 去重
│ └── 根据当前状态重新生成恢复基准

├── Redis < 7.0
│ ├── 子进程生成临时 AOF
│ ├── 父进程继续写旧 AOF
│ └── AOF Rewrite Buffer 补增量

└── Redis >= 7.0
├── Base AOF
│ └── 可使用 RDB 格式
├── Incremental AOF
└── Manifest


引入:Redis 为什么需要持久化

在此前的学习中,我们经常把 Redis 放到这样一个架构中理解:

Client

业务服务器

Redis → 缓存热点数据

MySQL → 保存全量数据

在这种场景下,一个很自然的问题就是:

既然 MySQL 已经保存了完整数据,那么 Redis 中的数据即使丢失,也可以重新从 MySQL 中加载,为什么 Redis 还需要持久化?

这里首先要建立一个非常重要的认识:

Redis 作为缓存,只是 Redis 的一种应用方式,而不是 Redis 唯一的应用方式。

Redis 本身就是一个 Key-Value 数据存储服务。

如果一个业务的数据组织方式比较简单,主要围绕:

key → value
key → hash
key → list
key → zset

进行访问,并不需要 MySQL 中复杂的关系模型、JOIN、GROUP BY、复杂 SQL 查询以及大量关系约束,那么 Redis 完全可能直接承担某些核心数据的存储。

当然,这里也不能走到另一个极端,认为“数据规模完全不重要”。

Redis 的数据主要驻留在内存中,因此:

数据规模
内存容量
内存成本
访问模型
数据结构
查询方式

都会共同影响 Redis 是否适合作为主要存储。

但是无论如何,只要 Redis 中保存的数据具有不可随意丢失的价值,就会出现一个根本问题:

Redis 数据主要位于内存

内存属于易失性介质

机器断电 / 进程崩溃 / Redis 重启

内存数据消失

因此 Redis 需要把内存中的数据保存到:

磁盘 / SSD

这种非易失性存储介质中。

这就是 Redis 持久化机制存在的根本动机。


一、RDB:给 Redis 当前数据库状态拍一张快照

在进入 AOF 之前,我们先把此前认识的 RDB 重新串起来,因为 AOF 很多机制实际上都是在解决 RDB 没有解决好的问题。

1. RDB 保存的是“某一时刻的数据库状态”

假设某一时刻 Redis 内存中是:

name → "wangzhe"
age → 21
rank → ZSet
user → Hash

RDB 的核心思想不是记录:

之前执行过哪些 SET
之前执行过哪些 INCR
之前执行过哪些 DEL

而是:

直接把当前数据库状态按照 RDB 二进制格式序列化下来。

因此:

Redis 当前内存状态

RDB 序列化

dump.rdb

所以 RDB 可以理解成:

状态快照。


2. SAVE:主线程同步生成 RDB

最简单的 RDB 触发方式就是:

SAVE

其心智模型非常直接:

Redis 主进程

遍历数据库中的 KV

序列化

写入 RDB

完成以后

继续处理后续请求

问题也非常明显:

整个 SAVE 过程会阻塞 Redis 主线程。

如果 Redis 当前数据量比较大,生成 RDB 需要较长时间,那么客户端请求就只能等待。

因此生产环境通常不主动使用 SAVE 完成长时间的快照操作。


3. BGSAVE:fork 子进程完成快照

为了避免 Redis 主线程在整个 RDB 生成期间被阻塞,Redis 提供:

BGSAVE

执行以后:

Redis 主进程

fork()

创建子进程

之后:

父进程

├── 继续处理客户端请求

└── 子进程

遍历数据库

生成 RDB

这里就会涉及 Linux 中非常重要的:

fork + Copy On Write


4. fork 为什么不需要立刻复制整份 Redis 内存

假设 Redis 当前占用:

10GB

内存。

执行 fork() 并不意味着立即得到:

父进程 10GB
+
子进程 10GB
=
20GB

父子进程会拥有各自的虚拟地址空间以及页表体系,但是刚刚完成 fork 时,大量页表项仍然指向相同的物理页:

父进程页表 ───┐
├──→ 同一批物理页
子进程页表 ───┘

只有后续某一方尝试修改共享页时,才触发:

Copy On Write

例如父进程继续处理:

SET name Tom

如果修改到了某个共享页:

父进程准备修改共享页

触发 COW

复制该物理页

父进程页表指向新页

父进程修改新页

而子进程仍然指向旧页。

所以对子进程来说:

它看到的是 fork 那一时刻相对稳定的数据库视图。

这就是为什么 RDB 子进程可以一边生成快照,而 Redis 父进程仍然继续对外处理写请求。

需要注意:

BGSAVE 并不是完全没有阻塞。

fork() 本身需要创建子进程、复制页表以及初始化相关进程状态,因此 Redis 内存越大,fork 的短暂停顿越可能变得明显。

所以更准确的区别是:

SAVE
→ 整个快照生成过程阻塞

BGSAVE
→ fork 阶段存在成本
→ fork 完成后由子进程生成快照
→ 父进程继续工作


5. RDB 的自动触发

除了手动执行:

BGSAVE

Redis 还可以根据配置中的 save 条件自动触发后台快照。

可以简单理解成:

Redis 周期性检查

经过了多长时间?
+
期间发生了多少次修改?

满足 save 条件

触发后台 RDB

例如:

一定时间内
+
修改次数达到阈值

即可触发。

这里本质上存在一个非常典型的权衡:

快照间隔大
→ 生成 RDB 次数少
→ 性能压力更小
→ 数据丢失窗口更大

快照间隔小
→ 生成 RDB 更频繁
→ 性能压力更大
→ 数据丢失窗口更小

如果已经存在一个后台快照子进程,Redis 不会无脑继续 fork 多个 RDB 子进程。

原因包括:

fork 本身有成本
+
多个子进程同时扫描大量内存
+
多个子进程同时写磁盘
+
磁盘带宽竞争


6. RDB 为什么要先写临时文件

生成新的 RDB 时,不能简单直接覆盖旧的:

dump.rdb

否则如果写到一半突然宕机:

旧 RDB 已经被破坏
+
新 RDB 又没有写完整

最终两边都不可用。

所以 Redis 的思路是:

保留旧 dump.rdb

子进程写临时 RDB 文件

完整生成成功

rename

原子替换旧 dump.rdb

因此正常情况下,Redis 保存的是:

最近一次成功生成的 RDB 快照。

如果人为进行历史备份,当然可以保存:

dump-2026-09-01.rdb
dump-2026-09-02.rdb

但这属于运维备份策略,不是 Redis 每一次 BGSAVE 默认自动保留多个历史 RDB。


7. RDB 恢复以及最大的缺点

Redis 重启以后,如果使用 RDB 恢复:

读取 dump.rdb

解析 RDB 二进制格式

重新构造 Redis 内部数据结构

恢复内存数据库

RDB 的优势非常明显:

文件紧凑
二进制格式
加载速度快
非常适合做快照备份

但是问题同样明显。

假设:

10:00 生成 RDB
10:01 SET …
10:02 INCR …
10:03 HSET …
10:04 DEL …
10:05 Redis 崩溃

而下一次 RDB 还没有生成,那么恢复时只能恢复到:

10:00

因此:

RDB 是周期性快照,故障时可能丢失最近一次快照之后产生的数据。

这就是 AOF 要重点解决的问题。


二、AOF:从“保存结果”过渡到“记录变化”

认识了 RDB 以后,再来看 AOF 就非常自然。

可以先建立一个非常核心的对比:

RDB
→ 保存某个时刻的“结果”

AOF
→ 记录数据库发生变化的“过程”

例如:

SET count 0
INCR count
INCR count
SET name wangzhe

最终结果是:

count = 2
name = wangzhe

RDB 更关注:

count = 2
name = wangzhe

而 AOF 更关注:

SET count 0
INCR count
INCR count
SET name wangzhe

也就是说:

AOF 会持续记录会修改数据库状态的写操作。

像:

GET
HGET
ZRANGE

这种纯读操作不会改变数据库状态,因此没有必要作为恢复日志写入 AOF。


三、AOF 中到底记录什么:RESP 格式的写命令

1. AOF 不是简单保存“最终值”

例如执行:

SET name wangzhe

AOF 并不是保存:

name = wangzhe

而是保存一条能够重新执行的 Redis 命令。

按照 RESP 表示,大致就是:

*3\\r\\n
$3\\r\\n
SET\\r\\n
$4\\r\\n
name\\r\\n
$7\\r\\n
wangzhe\\r\\n

也就是:

Array

├── "SET"
├── "name"
└── "wangzhe"

因此 AOF 可以理解成:

一份按照 Redis 协议编码的写操作日志。


2. AOF 虽然“看起来像文本”,但不要简单把它理解成普通文本文件

RESP 大量使用:

*
$
\\r\\n

以及可读字符串,因此人肉打开 AOF 时往往可以看到:

SET
INCR
HSET

这使它看起来非常像文本。

但是 RESP 中 Bulk String 是:

$<length>\\r\\n<data>\\r\\n

数据边界依赖长度,而不是依赖某一个普通文本结束符,因此其中可以承载任意二进制字节。

所以更准确的说法是:

AOF 使用 RESP 形式保存命令,整体具有很强的可读性,但 RESP 本身是二进制安全的序列化协议,并不等价于普通文本协议。


3. AOF 并不是简单把客户端原始网络请求复制一份

客户端执行:

SET name wangzhe

网络链路大致是:

客户端 Redis Command

RESP 编码

TCP 字节流

Redis Server

RESP 解析

得到 argv

Redis Server 内部最终更接近得到:

argv[0] = "SET"
argv[1] = "name"
argv[2] = "wangzhe"

然后执行命令。

如果这条命令需要进入 AOF,则 Redis 根据需要重新组织/编码相应的命令内容,再追加进入 AOF 记录流程。

因此不要理解成:

收到网络请求

保留原始网络字节副本

直接写进 AOF

更加合适的心智模型是:

客户端请求

RESP 解析

得到命令和参数

执行命令

生成需要记录的 AOF 命令表示

追加 AOF


四、AOF 恢复:解析日志并重放命令

既然 AOF 保存的是一条条写命令,那么恢复过程就很容易理解了:

AOF 文件

读取 RESP 数据

解析 / 反序列化

得到 Redis 命令

按照顺序重新执行

重新构造内存数据库

例如:

SET a 1
INCR a
HSET user name Tom

Redis 重启以后重新执行这些操作,最终自然得到对应的数据状态。

因此 AOF 的恢复思路很像数据库中的:

逻辑备份 / 日志重放

例如 MySQL 的逻辑备份可能保存:

CREATE TABLE ...
INSERT INTO ...

恢复时重新执行这些 SQL。

RDB 与 AOF 的差别可以继续压缩成:

RDB
→ 保存结果
→ 直接从状态恢复

AOF
→ 保存过程
→ 通过重放操作恢复


五、AOF 的核心写入链路:AOF Buffer → write → Page Cache → fsync

这里是 AOF 最容易混淆、也是最重要的一部分。

很多人看到:

AOF 比 RDB 实时性更强。

就容易误以为:

每执行一条写命令

立刻发生一次磁盘 IO

写完以后再执行下一条

这显然不合理。

Redis 本身大量数据操作都在内存中完成,如果每条命令都必须同步等待磁盘,那么 Redis 的性能会直接被磁盘拖住。

真正的链路应该分成多层:

Redis 写命令

AOF Buffer

write()

内核 Page Cache

fsync()

Disk / SSD


1. 第一层:AOF Buffer

Redis 会先在用户态维护:

AOF Buffer

新产生的 AOF 日志首先追加到这块用户态缓冲区域。

例如连续执行:

SET a 1
INCR count
HSET user name Tom
LPUSH list hello

可以先形成:

AOF Buffer:

SET …
INCR …
HSET …
LPUSH …

这里的意义就是:

逻辑上每个写操作都需要被记录,但并不意味着每个写操作都必须单独调用一次 write。


2. 第二层:write()

当 Redis 在合适的时机处理 AOF Buffer 时,会调用:

write(fd, buf, len);

把一批 AOF 数据写入文件。

但是 Linux 中普通文件的 write() 通常并不意味着:

数据已经真正写进物理磁盘

更加常见的路径是:

Redis 用户态 Buffer

write()

内核 Page Cache

所以:

write 更接近“我把数据交给内核了”。


3. write 本身也不是没有成本

write() 是一个系统调用,会涉及:

用户态

系统调用

内核态

文件系统 / Page Cache

返回用户态

因此如果:

一条命令 → 一次 write

大量系统调用本身也会造成额外开销。

所以 Redis 会通过缓冲和批处理,使得:

命令1 ┐
命令2 │
命令3 ├──→ AOF Buffer ──→ 一次/少量 write()
命令4 │
命令5 ┘

从而摊薄 syscall 开销。

因此需要建立一个非常重要的认识:

命令记录粒度

write 系统调用粒度

AOF 可以做到:

每条写命令都逻辑记录

同时又做到:

多条记录批量 write

这两件事情完全不冲突。


4. 第三层:Page Cache

write() 完成后,数据通常已经进入:

Linux Page Cache

但是 Page Cache 本质上仍然是内存。

如果此时:

机器突然掉电

那么尚未真正持久化的数据仍然可能丢失。

Linux 自己存在脏页回写机制:

Page Cache 中的文件页被修改

成为 Dirty Page

内核根据自身策略

在合适时机回写磁盘

所以即使应用程序不主动做任何事情,操作系统最终也会尝试把脏页写回磁盘。

但是问题在于:

操作系统的目标并不是向 Redis 保证“最多只丢 1 秒的数据”。

Linux 要综合考虑:

内存压力
脏页比例
IO 调度
磁盘吞吐
系统整体负载

因此如果 Redis 完全依赖内核自行回写,就无法精确控制持久化的数据窗口。


六、fsync:从“写进内核”到“要求真正持久化”

1. fsync 到底是什么

fsync() 是一个同步系统调用。

例如:

write(fd, buf, len);
fsync(fd);

可以先理解成:

write()

数据进入 Page Cache

fsync()

要求内核同步该文件相关脏数据

等待持久化完成

fsync 返回

所以:

fsync 不是“配置刷盘频率”的接口,而是一次“现在把这个文件同步下去”的请求。

真正的“刷盘频率”来自:

应用程序多久调用一次 fsync()


2. fsync 为什么昂贵

fsync() 是同步调用。

调用线程执行:

fsync(fd);

之后:

进入内核

等待相关数据完成持久化

fsync 返回

线程继续执行

因此如果主线程直接频繁调用 fsync:

Redis 主线程

fsync

等待磁盘

才能继续

那么磁盘延迟就会直接进入 Redis 请求路径。

这就是为什么 AOF 的性能权衡最终集中到了:

多久 fsync 一次。


3. AOF 最重要的三层粒度

到这里可以建立一个非常重要的分层:

第一层:命令记录粒度
→ 哪些写命令需要进入 AOF

第二层:write 粒度
→ AOF Buffer 中多少数据一起交给内核

第三层:fsync 粒度
→ Page Cache 中的数据多久要求真正持久化一次

也就是:

记录粒度

write 粒度

fsync 粒度

其中:

write 批处理
→ 主要影响系统调用开销

fsync 频率
→ 直接影响性能与数据安全性之间的权衡


七、appendfsync:三种 AOF 刷盘策略

Redis 配置文件中最核心的三个选项就是:

appendfsync always
appendfsync everysec
appendfsync no

在这里插入图片描述


1. appendfsync always

always 可以先理解为:

AOF 数据 write 到 Page Cache 后,立即要求同步 fsync,并且请求完成路径需要等待刷盘成功。

基本链路:

执行一批写命令

追加 AOF Buffer

write()

Page Cache

fsync()

等待磁盘完成

继续

因此:

可靠性最高
性能最低

需要注意,不要机械理解成:

每一条 Redis 命令都独立一次 write + fsync

Redis 可以对同一轮处理中的多条写命令进行批量提交,所以更适合理解成:

每批 AOF 写入后同步等待持久化完成。


2. appendfsync everysec

这是最值得理解的模式。

可以先建立两个角色:

Redis 主线程
+
AOF fsync 后台线程

主线程负责:

执行命令

追加 AOF Buffer

批量 write 到 Page Cache

继续处理请求

后台线程负责:

大约每秒安排一次 fsync

后台线程调用 fsync

后台线程自己阻塞等待

这样:

fsync() 依然是同步阻塞接口,但是被阻塞的主要是后台线程,而不是主事件循环线程。

因此相比 always:

性能明显更好
+
通常只承担约 1 秒量级的数据丢失窗口


3. “everysec 每秒一次”到底是什么意思

这里很容易产生一个误区:

后台线程每 sleep(1)

无条件再启动一个 fsync

不是这样。

更加准确的理解是:

Redis 以“大约每秒一次”为目标节奏安排后台 fsync。

假设:

t = 0
后台线程开始 fsync

如果:

0.1s
fsync 返回

那么后续大约到下一秒附近,再根据状态安排新的 fsync。

但是如果磁盘非常慢:

t = 0
fsync 开始

t = 1s
仍然没有返回

t = 2s
仍然没有返回

t = 2.5s
fsync 才返回

这期间不会出现:

1s 再启动一个 fsync
2s 再启动一个 fsync

因为负责 fsync 的后台线程本身还阻塞在上一次系统调用中。

当它恢复以后,Redis 再根据:

当前时间
上一次刷盘状态
是否存在新的 AOF 数据
后台 fsync 是否可用

等条件决定后续刷盘。

所以:

everysec

硬实时保证“每秒一定完成一次 fsync”

everysec
=
尽量以一秒左右的粒度完成后台刷盘


4. Redis 怎么知道有没有新的 AOF 数据需要刷

Redis 并不是去询问 Linux:

“Page Cache 中这个文件到底是不是 Dirty?”

而是自己维护 AOF 写入状态。

可以抽象成:

当前已经成功 write 到哪里

例如 offset = 1200

上一次已经完成持久化的位置

例如 offset = 1000

那么:

1200 > 1000

说明:

上一次 fsync 之后又产生了新的 AOF 写入。

需要注意:

fsync() 的返回值并不会告诉 Redis:

“我刷到了 offset = 1200”

fsync 的返回值主要用于告诉调用者这次同步是否成功。

Redis 本身已经根据此前 write() 成功的字节数知道当前文件写入位置,因此在 fsync 成功以后,可以推进自己维护的持久化状态。


5. appendfsync no

no 表示:

Redis 自己不主动按照固定策略调用 fsync,把真正的脏页回写时机主要交给操作系统。

链路仍然有:

AOF Buffer

write()

Page Cache

但是:

Page Cache

什么时候真正刷到磁盘

主要由操作系统决定

所以:

性能通常最好
+
数据持久化窗口最不可控


6. 三种策略总结

策略fsync 行为性能数据安全
always 每批 AOF 写入后同步 fsync 最低 最高
everysec 大约每秒后台 fsync 较高 较高
no Redis 不主动控制,主要交给 OS 最高 最弱

因此 Redis 默认通常选择:

appendfsync everysec

其本质就是在:

吞吐 / 延迟

数据可靠性

之间做折中。


八、no-appendfsync-on-rewrite:重写期间还要不要继续 fsync

配置文件中还有:

no-appendfsync-on-rewrite no

这个名字比较绕,因为包含双重否定。

先看:

no-appendfsync-on-rewrite no

可以理解成:

在 BGSAVE / BGREWRITEAOF 等后台持久化任务期间,不禁止 AOF fsync。

也就是说,如果原来配置:

appendfsync everysec

那么后台正在生成 RDB 或 AOF Rewrite 时,AOF 仍然按照 everysec 的策略刷盘。

优点:

数据持久性更好

缺点:

后台子进程正在大量磁盘 IO
+
AOF 仍在 fsync

磁盘竞争加剧

延迟可能上升

如果配置:

no-appendfsync-on-rewrite yes

表示:

在 BGSAVE / BGREWRITEAOF 期间暂时不主动 fsync AOF。

此时 AOF 的 durability 在这一段时间内更接近:

appendfsync no

也就是主要依赖操作系统自行回写。

所以本质是:

yes
→ 用一部分数据可靠性换延迟

no
→ 重写期间仍保持原 fsync 策略
→ 更安全

默认设置为:

no

正是因为从持久性角度更加稳妥。


九、为什么 AOF 必须重写

AOF 的核心写入方式是:

Append Only

即只追加,不修改前面已经存在的历史记录。

例如:

SET age 1
SET age 2
SET age 3
SET age 4
SET age 5

最终真正有意义的状态只有:

age = 5

但是 AOF 如果一直不处理,前面的历史命令仍然全部保留。

运行时间越来越长以后:

写操作越来越多

AOF 文件越来越大

会带来几个问题。


1. 磁盘空间不断增大

这是最直观的问题。

AOF 保存大量历史命令,而其中很多命令从最终状态角度看已经没有意义。

例如:

SET x 1
SET x 2
SET x 3
DEL x
SET x 100

真正恢复当前状态只需要:

SET x 100

前面的操作都属于历史过程。


2. Redis 启动恢复越来越慢

AOF 恢复不是单纯:

读取文件

还需要:

解析 RESP

恢复命令

重新执行命令

修改 Redis 内部数据结构

而此前学习 Redis 命令时,我们已经分析过:

SET
ZADD
LPUSH
HSET

这些操作本身都有自己的执行成本和复杂度。

因此如果 Redis 运行几年以后积累了海量历史命令:

AOF 读取成本
+
RESP 解析成本
+
命令重放成本

都会增加。

最终:

Redis 重启恢复会越来越慢。

所以 AOF 必须拥有一个“压缩历史”的机制。

这个机制就是:

AOF Rewrite


十、AOF Rewrite:不是扫描旧日志做去重

这里一定要纠正一个非常容易形成的误区。

所谓 AOF Rewrite,不是:

打开旧 AOF

从头扫描

解析每条命令

进行语法分析

判断哪些命令冗余

删除重复命令

得到压缩文件

Redis 不需要这样做。

因为 Redis 内存中已经存在:

所有历史命令执行完成以后得到的最终数据库状态。

所以更加直接的方式是:

直接读取 Redis 当前数据库状态

根据当前状态

重新生成一组能够直接构造出该状态的命令

例如旧 AOF:

SET age 1
SET age 2
SET age 3
DEL age
SET age 100

当前内存:

age = 100

那么 Rewrite 只需要生成足够恢复当前状态的命令,例如:

SET age 100

因此:

AOF Rewrite 的输入核心是当前内存状态,而不是旧 AOF 历史本身。


1. AOF Rewrite 和 RDB 为什么看起来很像

这里会出现一个非常自然的问题:

AOF Rewrite 既然也是根据当前状态重新生成一份基准,那这不就是“拍快照”吗?

从“思想”上看,两者确实非常接近:

RDB
→ 根据当前状态生成状态快照

AOF Rewrite
→ 根据当前状态生成一份新的恢复基准

区别主要在最终表示方式。

经典 AOF Rewrite 中:

当前状态

转换成能够恢复该状态的命令

RDB 中:

当前状态

编码成 RDB 二进制快照

所以可以进一步形成:

AOF Rewrite 把一段很长的历史过程折叠成一个新的当前状态起点。


2. 为什么之前的 AOF 增量记录不是白写

如果我们每隔一段时间才生成一次基准,而中间完全不记录变化:

t0:生成基准

t0 ~ t1:发生大量修改

Redis 突然宕机

那么 t0 到宕机之间的数据还是全部丢失,这又退化成了 RDB 的数据窗口问题。

所以 AOF 的本质可以理解成:

基准
+
基准之后的增量日志

即:

Checkpoint + Incremental Log

重写只是:

旧基准 + 大量增量历史

重新折叠

新基准

然后:

新基准
+
继续记录新的增量

这就是为什么 AOF 可以同时兼顾:

恢复基准不要太庞大
+
中间变化仍然细粒度记录


十一、Redis 7.0 之前:经典 AOF Rewrite 机制

理解了原理以后,再看实现机制。

经典 Redis AOF Rewrite 同样使用:

fork + Copy On Write


1. fork 子进程

触发:

BGREWRITEAOF

以后:

父进程

fork()

子进程

子进程看到:

fork 时刻 t0 的数据库视图

然后:

遍历当前 KV

根据最终状态

生成新的临时 AOF

与此同时:

父进程继续处理客户端请求

这里自然会出现一个问题:

子进程生成的是 t0 时刻的数据,而父进程 t0 之后还在继续处理 SET、INCR、DEL 等操作,这些新的变化怎么办?


2. 父进程需要写“两份”

Redis 7.0 之前的经典心智模型可以理解成:

t0 之后的新写命令

├──→ 正常 AOF Buffer
│ ↓
│ 继续写旧 AOF

└──→ AOF Rewrite Buffer

为什么还要继续写旧 AOF?

因为:

如果这次 Rewrite 失败了,旧 AOF 仍然必须保持完整可恢复。

所以旧 AOF 不能因为“我现在准备生成新 AOF”就停止维护。

与此同时,AOF Rewrite Buffer 保存:

t0 之后产生的增量变化

这些变化最终要补到子进程生成的新 AOF 后面。


3. 子进程完成以后

子进程最终生成:

临时新 AOF
=
t0 时刻状态对应的恢复命令

子进程完成任务后退出。

父进程检测到子进程结束以后:

AOF Rewrite Buffer

追加到临时新 AOF 文件末尾

最终:

新 AOF
=
t0 时刻基准
+
t0 之后的新增命令

然后:

rename

原子替换旧 AOF

所以完整链路:

fork @ t0

┌─────────┴─────────┐
↓ ↓
子进程 父进程
↓ ↓
根据 t0 状态 继续执行新写命令
生成临时 AOF │
├→ 旧 AOF

└→ Rewrite Buffer


子进程完成

父进程将 Rewrite Buffer
追加到临时 AOF 尾部

rename

替换旧 AOF

源码版本的细节可能还会通过进程间通信等方式减少最终补尾阶段的停顿,但是学习主线时,先用“旧 AOF + Rewrite Buffer + 临时新 AOF + rename”建立模型即可。


十二、BGREWRITEAOF:手动触发与并发限制

手动触发 AOF Rewrite 使用:

BGREWRITEAOF

其作用就是:

请求 Redis

在后台进行一次 AOF Rewrite

如果当前已经存在一个 AOF Rewrite 子进程:

BGREWRITEAOF

不会再 fork 第二个 AOF Rewrite 子进程

Redis 会拒绝重复执行。

因为多个重型持久化子进程同时工作,会带来:

fork 成本
+
COW 内存压力
+
CPU 竞争
+
磁盘 IO 竞争


1. 如果此时正在 BGSAVE 呢

如果当前不是 AOF Rewrite,而是:

BGSAVE 正在进行

此时请求:

BGREWRITEAOF

Redis 不会同时再启动一个 AOF Rewrite 子进程。

但是这个请求可以被:

标记为稍后执行

等 BGSAVE 子进程结束以后,再启动 AOF Rewrite。

因此可以记成:

已有 AOF Rewrite
→ 新 BGREWRITEAOF 拒绝

已有 BGSAVE
→ BGREWRITEAOF 延后
→ 等 BGSAVE 结束后再执行


十三、子进程结束以后,父进程怎么知道

Linux 中:

子进程退出

会产生子进程状态变化,父进程最终需要通过:

waitpid(..., WNOHANG)

之类的方式回收子进程并获取退出状态。

从操作系统概念上看,子进程退出和:

SIGCHLD

联系在一起,因此可以把它理解成一种异步事件通知语义。

但是这里需要进一步把 Redis 的实现模型说严谨:

Redis 不会让主线程阻塞在 wait 上等待后台子进程结束,也不会把复杂的 RDB/AOF 收尾逻辑全部塞到 signal handler 里。

Redis 主循环中的周期任务会非阻塞检查后台子进程:

serverCron

checkChildrenDone

waitpid(…, WNOHANG)

一旦发现后台子进程结束,再根据:

RDB child
还是
AOF child

调用相应的完成处理逻辑。

所以这里真正应该建立的是:

后台子进程工作

主进程继续服务

主进程周期性非阻塞检查子进程状态

发现结束

执行 RDB / AOF 对应收尾


十四、Redis 7.0:AOF Rewrite 机制的重要演进

Redis 7.0 之后,AOF 结构发生了一个非常重要的变化:

Single AOF

Multi-Part AOF

也就是:

AOF 不再必须由单一文件承担全部职责。

Redis 7.0+ 可以把 AOF 拆成:

Base AOF
+
一个或多个 Incremental AOF
+
Manifest


1. Base:负责保存一个完整基准

Base 的职责是:

描述某一个时刻完整的数据库状态。

它可以使用:

AOF 格式

也可以使用:

RDB 格式

而默认通常更倾向 RDB 格式,因为:

文件更紧凑
+
加载更快
+
作为“状态基准”非常合适

所以这里可以看到一个很自然的组合:

Base
→ RDB 擅长保存完整状态

Incremental
→ AOF 擅长记录后续变化


2. Incremental AOF:负责保存基准之后的新变化

假设:

t0

开始一次 Rewrite。

父进程可以创建一个新的:

incr.aof

然后:

t0 之后发生的 SET
t0 之后发生的 INCR
t0 之后发生的 DEL

全部继续追加到新的 Incremental AOF。

与此同时子进程:

根据 t0 时刻的内存视图

生成新的 Base

因此:

子进程
→ 新 Base

父进程
→ 新 Incremental AOF

它们写的是不同文件。

所以不需要:

父子进程抢同一个 AOF 文件
+
通过文件锁协调

真正存在的竞争主要变成:

磁盘 IO 带宽竞争


3. 为什么 Redis 7.0 以后不需要经典 Rewrite Buffer

Redis 7.0 之前:

父进程
→ 继续写旧 AOF
+
→ Rewrite Buffer

增量需要额外维护一份内存缓存。

Redis 7.0+:

子进程
→ 新 Base

父进程
→ 直接写新的 incr.aof

于是重写期间产生的新变化已经直接成为新 AOF 体系中的增量文件。

因此经典的:

AOF Rewrite Buffer

不再是这套 Multi-Part AOF 的核心机制。


4. Redis 7.0+ 的完整 Rewrite 心智模型

触发 AOF Rewrite

fork @ t0

┌────────────────────┬────────────────────┐
│ │ │
↓ ↓
子进程 父进程
│ │
├→ 遍历 t0 数据 ├→ 继续处理请求
│ │
└→ 生成新 Base └→ 创建新的 incr.aof

记录 t0 后变化

└────────────┬────────────────────┘

Base 生成成功

更新 Manifest

新 Base + 新 Incremental 生效


十五、Manifest:AOF 文件集合的“目录”

既然 Redis 7.0+ 可能存在:

Base
+
incr1.aof
+
incr2.aof
+
incr3.aof

那么 Redis 启动时就必须知道:

哪个 Base 是当前有效的?
有哪些 Incremental?
这些 Incremental 的顺序是什么?

于是就需要:

Manifest

Manifest 可以理解成:

当前 AOF 文件集合的清单。

它不负责保存真正的业务数据,而是描述:

当前 Base 是谁
+
当前有效 Incremental 有哪些
+
加载顺序是什么

所以恢复时:

读取 Manifest

找到 Base

加载 Base

按照顺序重放 Incremental

恢复完整状态


1. 为什么一个 Base 后面可以有多个 Incremental

一套有效 AOF 中:

Base

通常最多只有一个有效基准。

但是:

Incremental

可能存在多个。

例如:

旧 Base
+
incr1

随后又发起一次 Rewrite:

父进程新建 incr2

但是这次 Rewrite 失败了。

新的 Base 没有生成成功,但是:

incr2

里面已经保存了重写期间产生的新变化。

那么为了保持完整恢复能力:

旧 Base
+
incr1
+
incr2

依然是一套有效链路。

所以不能简单理解成:

一个 Base
只能配一个 incr

更准确是:

1 个 Base
+
0 ~ N 个 Incremental AOF


十六、Redis 7.0+ 的恢复过程

如果 Base 使用 RDB 格式:

Base RDB

快速恢复到 t0 时刻的数据状态

然后:

Incremental AOF

解析 RESP

得到命令

依次重放

最终:

当前内存状态
=
Base 基准状态
+
Base 之后的增量变化

这就是 Redis 7.0+ AOF 非常清晰的一条恢复链路。

需要注意:

这里的 Base 虽然使用 RDB 格式,但它仍然属于 AOF 体系中的 Base 文件。

它和 Redis 独立的:

dump.rdb

不是同一个概念。


十七、AOF Rewrite 的自动触发

除了:

BGREWRITEAOF

手动触发以外,Redis 还可以自动判断 AOF 是否已经膨胀到值得 Rewrite。

核心配置:

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

在这里插入图片描述


1. auto-aof-rewrite-min-size

它表示:

当前 AOF 至少达到一定规模,才考虑自动 Rewrite。

例如:

auto-aof-rewrite-min-size 64mb

意味着如果 AOF 还非常小,即使增长比例已经很大,也没有必要频繁 Rewrite。

例如:

1MB → 2MB

虽然增长:

100%

但文件本身很小,为此 fork 一个重写子进程显然意义不大。

所以最小大小负责过滤:

文件虽然增长比例大,但绝对规模仍然很小。


2. auto-aof-rewrite-percentage

Redis 会记录:

上一次 Rewrite 完成后的 AOF 基准大小

再和:

当前 AOF 大小

比较。

例如:

上一次 Rewrite 后
AOF = 100MB

配置:

auto-aof-rewrite-percentage 100

那么:

当前 AOF ≈ 200MB

时,相比上一次 Rewrite 后的大小增长了约:

100%

于是满足增长比例条件。

所以自动 Rewrite 可以简单理解成两个门槛:

当前 AOF 大小 >= 最小阈值
&&
当前 AOF 相比上次 Rewrite 后
增长比例 >= 配置百分比

满足以后:

自动触发 BGREWRITEAOF 对应的后台 Rewrite 流程

如果:

auto-aof-rewrite-percentage 0

则可以关闭自动 AOF Rewrite。


十八、AOF 其他几个重要配置项

在这里插入图片描述


1. aof-load-truncated

配置:

aof-load-truncated yes

控制:

Redis 启动加载 AOF 时,如果发现文件尾部因为异常中断而没有写完整,该怎么办。

例如原本应该:

SET a 1
SET b 2
SET c 3

但是最后一次写入时机器宕机:

SET a 1
SET b 2
SET c …

尾部出现截断。

如果:

aof-load-truncated yes

Redis 会:

尽可能加载前面完整的 AOF
+
记录相关日志
+
继续启动

如果:

aof-load-truncated no

Redis 会:

发现尾部截断

拒绝启动

需要人工检查 / 修复 AOF

需要注意:

这个选项主要针对文件尾部截断,不代表 AOF 中间任意位置损坏都可以忽略。

如果 AOF 中间发生结构性损坏,Redis 仍可能拒绝启动。


2. aof-use-rdb-preamble

配置:

aof-use-rdb-preamble yes

表示:

AOF Rewrite 生成 Base 时,可以使用 RDB 格式保存基准状态。

这正好对应前面 Redis 7.0+ 的:

Base RDB
+
Incremental AOF

RDB 作为 Base 的优势:

紧凑
加载快
非常适合描述完整基准状态

如果配置为:

no

则 Base 可以继续采用 AOF 命令形式,主要用于兼容场景。


3. aof-timestamp-enabled

配置:

aof-timestamp-enabled no

表示默认不在 AOF 中开启时间戳注解。

如果开启:

aof-timestamp-enabled yes

AOF 会记录额外的时间信息,从而帮助支持:

Point-In-Time Restore

也就是:

按指定时间点恢复。

例如:

我不想恢复到最新状态
而是想恢复到 10:30 左右

时间戳可以帮助恢复工具判断:

哪些 AOF 操作位于该时间点之前
哪些位于该时间点之后

代价是:

AOF 格式发生扩展
+
部分旧解析器 / 第三方工具可能不兼容

所以默认通常保持:

no


十九、RDB 和 AOF 可以同时开启吗

可以。

这里一定不要建立:

开启 RDB
→ 就不能开启 AOF

或者:

开启 AOF
→ RDB 就停止

这种二选一认知。

Redis 可以同时维护:

RDB
+
AOF

两套独立持久化体系。

正常运行时:

Redis 内存

├── RDB
│ └→ 满足 save 条件时生成快照

└── AOF
└→ 持续记录写操作并按照策略持久化


1. 两者同时开启时,恢复优先谁

正常情况下:

RDB + AOF 都开启

Redis 启动会优先使用:

AOF

来恢复数据库。

原因非常直接:

AOF 通常拥有更细的记录粒度,能够恢复到比周期性 RDB 快照更新的状态。

所以不要理解成:

先用 RDB 恢复
+
再把独立 AOF 全部继续重放

这是两个独立的持久化体系。


2. 那同时开启以后,RDB 有什么价值

既然恢复主要依赖 AOF,那么 RDB 仍然可以承担:

独立快照
+
周期性备份
+
异地备份
+
灾难恢复时的重要备份副本

所以可以把它理解成:

AOF 主要承担更细粒度的持久化恢复,RDB 仍然是一份非常有价值的独立状态备份。

但是不要简单理解成:

AOF 一坏
Redis 自动无条件切换 RDB

“独立备份”与“自动故障切换”不是一回事。


二十、BGSAVE 与 BGREWRITEAOF 为什么不会同时跑两个重型子进程

RDB 的后台快照:

BGSAVE

以及 AOF Rewrite:

BGREWRITEAOF

都会涉及:

fork
+
遍历大量内存
+
大量磁盘写入

如果两者同时 fork:

RDB Child
+
AOF Rewrite Child

会明显增加:

COW 内存压力
+
CPU 压力
+
磁盘 IO 竞争

所以 Redis 会避免两个互斥的重型后台持久化子进程同时运行。

例如:

BGSAVE 正在执行

此时触发 BGREWRITEAOF

AOF Rewrite 延后

等待 BGSAVE 结束

再执行 Rewrite

但是注意:

等待的是 AOF Rewrite,而不是正常 AOF 追加。

也就是说,在 BGSAVE 期间:

AOF Buffer

write

Page Cache

按照 appendfsync 策略刷盘

仍然可以正常工作。


二十一、Redis 7.0+ 中“两种 RDB”一定不要混

学习到 Redis 7.0+ 后,非常容易看到:

RDB

这个词出现两次,然后混为一谈。

实际上有两个完全不同层次。


1. 独立的 RDB 持久化

Redis Persistence

RDB

dump.rdb

这是:

SAVE / BGSAVE / save condition

对应的独立 RDB 持久化机制。


2. AOF 内部的 Base RDB

Redis 7.0+ 的 AOF:

AOF
├── Base
│ └── 可以使用 RDB 格式
├── Incremental AOF
└── Manifest

这里的:

base.rdb

虽然使用 RDB 二进制格式,但它的身份是:

AOF 文件集合中的 Base。

所以最终结构可以理解成:

Redis 持久化

├── 独立 RDB
│ └── dump.rdb

└── AOF
├── Base
│ └── 可以使用 RDB 格式
├── Incremental AOF
└── Manifest

这两个层次一定不要混淆。


二十二、最终把整套持久化机制串起来

到这里可以把整个 Redis 持久化压缩成一条完整逻辑链。

Redis 数据主要驻留内存

内存掉电会丢失

需要持久化

────────────────────────────

RDB

周期性保存某一时刻完整状态

SAVE / BGSAVE / 自动 save

BGSAVE 使用 fork + COW

生成二进制快照

恢复快、文件紧凑

但是存在快照间的数据窗口
────────────────────────────

AOF

持续记录写操作

RESP 命令日志

AOF Buffer

批量 write()

Page Cache

按照 appendfsync 策略 fsync

磁盘
────────────────────────────

AOF 日志不断增长

恢复命令越来越多

需要 AOF Rewrite

不是扫描旧 AOF 去重

根据当前内存状态生成新的恢复基准
────────────────────────────

Redis < 7.0

子进程生成临时新 AOF
父进程继续写旧 AOF + Rewrite Buffer

子进程完成

Rewrite Buffer 追加到新 AOF

rename 替换
────────────────────────────

Redis >= 7.0

Multi-Part AOF

Base + Incremental AOF + Manifest

Base 可采用 RDB 格式

父进程直接创建新的 Incremental AOF

重写成功后切换新的 AOF 文件集合


二十三、RDB 与 AOF 的最终对比

维度RDBAOF
核心思想 保存某一时刻状态 持续记录写操作
记录粒度 快照级 写操作级
文件形式 RDB 二进制 RESP/AOF 日志;Redis 7+ Base 可为 RDB
恢复方式 读取快照重建状态 加载 Base + 重放增量命令
恢复速度 通常更快 传统纯 AOF 重放较慢
文件体积 通常更紧凑 长期追加容易膨胀
数据窗口 通常更大 取决于 appendfsync 策略
后台优化 BGSAVE + fork + COW BGREWRITEAOF + fork + COW
主要问题 快照之间可能丢数据 文件不断膨胀,需要 Rewrite

二十四、最后建立几个最重要的认知

认知一:Redis 持久化不是因为 Redis 只能做缓存

Redis

MySQL 前面的临时缓存层

Redis
也可以承担重要数据存储

所以持久化本身是 Redis 非常核心的能力。


认知二:RDB 和 AOF 的根本区别不是“一个二进制,一个文本”

更加本质的是:

RDB
→ 保存状态

AOF
→ 保存变化

文件格式只是这种设计思想最终落地的一部分。


认知三:AOF 的实时性不等于每条命令都同步磁盘 IO

真正的链路:

写命令

AOF Buffer

write()

Page Cache

fsync()

磁盘

所以:

每条命令都记录

每条命令一次 write

每条命令一次 fsync


认知四:AOF 的核心性能权衡在 fsync

fsync 越频繁
→ 数据安全性越高
→ 磁盘同步成本越高

fsync 越少
→ 性能越好
→ 数据丢失窗口越大

所以才有:

always
everysec
no

三种策略。


认知五:AOF Rewrite 不是旧日志去重

错误理解:
扫描旧 AOF
→ 解析
→ 判断冗余
→ 删除

正确理解:
直接看当前数据库状态
→ 重新生成能够恢复当前状态的基准


认知六:Redis 7.0+ 把“基准”和“增量”真正拆成了不同文件

Base
→ 完整基准

Incremental AOF
→ 基准后的变化

Manifest
→ 描述当前有效文件集合

当 Base 使用 RDB 格式以后:

RDB 的紧凑、快速恢复
+
AOF 的细粒度增量记录

被组合到了 AOF 自己的体系内部。


总结

Redis 的两种持久化机制看起来实现方式不同,但是从更高层次来看,它们实际上一直在解决同一个问题:

如何在性能、恢复速度以及数据可靠性之间做权衡。

RDB 选择:

周期性保存完整状态

因此:

恢复快
+
持久化开销相对集中
+
存在较大的数据时间窗口

AOF 选择:

持续记录增量变化

因此:

数据窗口更小
+
需要处理 write / fsync 性能问题
+
需要通过 Rewrite 控制日志膨胀

而 Redis 7.0+ 的 Multi-Part AOF 又进一步说明:

“快照”和“增量日志”并不是互斥思想,真正优秀的存储系统往往会把两者组合起来:用一个完整基准解决历史过长的问题,再用增量日志解决基准之间的数据实时性问题。

这也就是我们在其他数据库、文件系统以及存储系统中经常看到的:

Checkpoint
+
Incremental Log

这一类设计思想。

在这里插入图片描述

赞(0)
未经允许不得转载:网硕互联帮助中心 » 【Redis进阶系列】:从 RDB 到 AOF:Redis 持久化、刷盘与重写机制详解
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!