Redis 从入门到精通(三):持久化机制 ------ RDB 与 AOF 深度解析
一、为什么需要持久化?先理解 Redis 的数据分层
Redis 的数据存在内存中,这是它快的原因,也是它脆弱的根源。一旦进程崩溃或服务器断电,所有数据瞬间消失。
持久化的核心矛盾:
高性能(写内存) ⇄ 数据安全(写磁盘)
Redis 的解法:两者都要,但不追求实时同步
Redis 提供了三种持久化策略,各自解决不同场景的问题:
| 策略 | 一句话描述 | 数据安全性 | 性能影响 |
|---|---|---|---|
| RDB | 定时快照,把某一时刻的内存数据全量写入磁盘 | 可能丢几分钟数据 | 低(fork 子进程后台写) |
| AOF | 记录每次写命令,重启时回放 | 最多丢 1 秒(可配) | 中(取决于 fsync 策略) |
| 混合持久化 | RDB 快照 + 增量 AOF 合二为一 | 兼顾恢复速度与数据安全 | 中 |
二、RDB(Redis Database):内存快照的艺术
2.1 RDB 是什么?
RDB 就是在某个时间点 把 Redis 中所有数据生成一个二进制压缩文件(默认 dump.rdb),等同于给内存拍了一张照片。恢复时直接把这个文件加载回内存,速度极快。
2.2 触发方式
RDB 有两种触发方式:
① 手动触发
bash
SAVE # 主线程执行,阻塞所有客户端请求,生产禁用
BGSAVE # fork 子进程执行,主线程继续处理请求,生产唯一选择
② 自动触发(通过配置)
bash
# redis.conf ------ save 配置格式:save <seconds> <changes>
save 900 1 # 900 秒内至少 1 次修改 → 触发 BGSAVE
save 300 10 # 300 秒内至少 10 次修改 → 触发 BGSAVE
save 60 10000 # 60 秒内至少 10000 次修改 → 触发 BGSAVE
# 关闭自动 RDB
save ""
这三个条件是或 的关系,满足任意一个就触发。配置的含义是:在保证性能的前提下,尽量减少数据丢失------写入频繁时就多快照,写入稀疏时就少快照。
2.3 BGSAVE 的完整流程
#mermaid-svg-hpz5IIqGdhHYbCet{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hpz5IIqGdhHYbCet .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hpz5IIqGdhHYbCet .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hpz5IIqGdhHYbCet .error-icon{fill:#552222;}#mermaid-svg-hpz5IIqGdhHYbCet .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hpz5IIqGdhHYbCet .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hpz5IIqGdhHYbCet .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hpz5IIqGdhHYbCet .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hpz5IIqGdhHYbCet .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hpz5IIqGdhHYbCet .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hpz5IIqGdhHYbCet .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hpz5IIqGdhHYbCet .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hpz5IIqGdhHYbCet .marker.cross{stroke:#333333;}#mermaid-svg-hpz5IIqGdhHYbCet svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hpz5IIqGdhHYbCet p{margin:0;}#mermaid-svg-hpz5IIqGdhHYbCet .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hpz5IIqGdhHYbCet .cluster-label text{fill:#333;}#mermaid-svg-hpz5IIqGdhHYbCet .cluster-label span{color:#333;}#mermaid-svg-hpz5IIqGdhHYbCet .cluster-label span p{background-color:transparent;}#mermaid-svg-hpz5IIqGdhHYbCet .label text,#mermaid-svg-hpz5IIqGdhHYbCet span{fill:#333;color:#333;}#mermaid-svg-hpz5IIqGdhHYbCet .node rect,#mermaid-svg-hpz5IIqGdhHYbCet .node circle,#mermaid-svg-hpz5IIqGdhHYbCet .node ellipse,#mermaid-svg-hpz5IIqGdhHYbCet .node polygon,#mermaid-svg-hpz5IIqGdhHYbCet .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hpz5IIqGdhHYbCet .rough-node .label text,#mermaid-svg-hpz5IIqGdhHYbCet .node .label text,#mermaid-svg-hpz5IIqGdhHYbCet .image-shape .label,#mermaid-svg-hpz5IIqGdhHYbCet .icon-shape .label{text-anchor:middle;}#mermaid-svg-hpz5IIqGdhHYbCet .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hpz5IIqGdhHYbCet .rough-node .label,#mermaid-svg-hpz5IIqGdhHYbCet .node .label,#mermaid-svg-hpz5IIqGdhHYbCet .image-shape .label,#mermaid-svg-hpz5IIqGdhHYbCet .icon-shape .label{text-align:center;}#mermaid-svg-hpz5IIqGdhHYbCet .node.clickable{cursor:pointer;}#mermaid-svg-hpz5IIqGdhHYbCet .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hpz5IIqGdhHYbCet .arrowheadPath{fill:#333333;}#mermaid-svg-hpz5IIqGdhHYbCet .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hpz5IIqGdhHYbCet .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hpz5IIqGdhHYbCet .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hpz5IIqGdhHYbCet .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hpz5IIqGdhHYbCet .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hpz5IIqGdhHYbCet .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hpz5IIqGdhHYbCet .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hpz5IIqGdhHYbCet .cluster text{fill:#333;}#mermaid-svg-hpz5IIqGdhHYbCet .cluster span{color:#333;}#mermaid-svg-hpz5IIqGdhHYbCet div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hpz5IIqGdhHYbCet .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hpz5IIqGdhHYbCet rect.text{fill:none;stroke-width:0;}#mermaid-svg-hpz5IIqGdhHYbCet .icon-shape,#mermaid-svg-hpz5IIqGdhHYbCet .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hpz5IIqGdhHYbCet .icon-shape p,#mermaid-svg-hpz5IIqGdhHYbCet .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hpz5IIqGdhHYbCet .icon-shape .label rect,#mermaid-svg-hpz5IIqGdhHYbCet .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hpz5IIqGdhHYbCet .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hpz5IIqGdhHYbCet .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hpz5IIqGdhHYbCet :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 成功
失败
客户端发起 BGSAVE
主进程 fork 子进程
fork 是否成功?
子进程:遍历数据库
将数据写入临时 RDB 文件
返回错误
(通常因为内存不足)
子进程:写入完成
原子 rename 替换旧 RDB
子进程退出
主进程收到 SIGCHLD
BGSAVE 完成
主进程:继续处理客户端请求
主进程:写操作触发 Copy-On-Write
主进程:修改的内存页
由 OS 复制一份给子进程
关键点解析:
-
fork 操作 :
fork()创建子进程,子进程拥有父进程的内存快照(通过页表共享同一块物理内存)。fork 本身是阻塞的,但通常很快(几毫秒到几十毫秒,取决于内存大小和系统负载)。 -
Copy-On-Write(COW):fork 之后,父子进程共享同一块物理内存。当父进程(Redis 主线程)修改数据时,操作系统会把被修改的内存页复制一份,父进程修改副本,子进程继续读原来的页。这就是为什么 BGSAVE 期间 Redis 还能正常处理写请求。
-
COW 的内存风险 :如果 BGSAVE 期间写入量巨大,大量内存页被复制,可能导致内存使用量翻倍。这是生产环境最需要关注的点------预留足够内存,或者避免在写入高峰期做 BGSAVE。
2.4 RDB 文件结构
RDB 文件是一个紧凑的二进制格式:
REDIS (魔数,5 字节)
├── RDB_VERSION (4 字节,版本号)
├── AUX_FIELDS (可选,如 redis-ver、redis-bits 等)
├── database 0
│ ├── SELECTDB (1 字节标记)
│ ├── db_number (变长编码)
│ ├── RESIZEDB (可选,哈希表大小提示)
│ └── key-value pairs
│ ├── EXPIRETIME_MS (可选,过期时间)
│ ├── value_type (1 字节)
│ ├── key (字符串)
│ └── value (根据类型编码)
├── database 1
│ └── ...
├── EOF (1 字节,0xFF)
└── CRC64 (8 字节,校验和)
RDB 的压缩 :Redis 默认使用 LZF 算法压缩字符串类型的 value(rdbcompression yes),文本数据(JSON、XML)压缩率很高,但二进制数据(图片、序列化对象)效果有限。可通过 rdbcompression no 关闭,牺牲磁盘空间换取更少的 CPU 消耗。
2.5 RDB 的优缺点
| 优点 | 缺点 |
|---|---|
| 文件紧凑,适合备份和灾难恢复 | 两次快照之间的数据可能丢失 |
| 恢复速度快(直接加载到内存) | fork 操作在大内存下可能耗时较长 |
| 对主线程性能影响小(子进程写盘) | COW 可能导致内存膨胀 |
| 适合冷备、异地容灾 | 不支持秒级持久化 |
三、AOF(Append Only File):每条写命令都不放过
3.1 AOF 是什么?
AOF 记录的是所有修改 Redis 数据的写命令(以 RESP 协议文本格式追加写入)。重启时,Redis 按顺序回放 AOF 文件中的命令,重建整个数据集。
# AOF 文件示例(appendonly.aof)
*2\r
$6\r
SELECT\r
$1\r
0\r
# SELECT 0
*3\r
$3\r
SET\r
$3\r
key\r
$5\r
value\r
# SET key value
*3\r
$4\r
INCR\r
$7\r
counter\r
# INCR counter
3.2 AOF 的写入流程 ------ 三部曲
#mermaid-svg-Nlq9yvFFCJscB9Mw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Nlq9yvFFCJscB9Mw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Nlq9yvFFCJscB9Mw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Nlq9yvFFCJscB9Mw .error-icon{fill:#552222;}#mermaid-svg-Nlq9yvFFCJscB9Mw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Nlq9yvFFCJscB9Mw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Nlq9yvFFCJscB9Mw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Nlq9yvFFCJscB9Mw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Nlq9yvFFCJscB9Mw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Nlq9yvFFCJscB9Mw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Nlq9yvFFCJscB9Mw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Nlq9yvFFCJscB9Mw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Nlq9yvFFCJscB9Mw .marker.cross{stroke:#333333;}#mermaid-svg-Nlq9yvFFCJscB9Mw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Nlq9yvFFCJscB9Mw p{margin:0;}#mermaid-svg-Nlq9yvFFCJscB9Mw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Nlq9yvFFCJscB9Mw .cluster-label text{fill:#333;}#mermaid-svg-Nlq9yvFFCJscB9Mw .cluster-label span{color:#333;}#mermaid-svg-Nlq9yvFFCJscB9Mw .cluster-label span p{background-color:transparent;}#mermaid-svg-Nlq9yvFFCJscB9Mw .label text,#mermaid-svg-Nlq9yvFFCJscB9Mw span{fill:#333;color:#333;}#mermaid-svg-Nlq9yvFFCJscB9Mw .node rect,#mermaid-svg-Nlq9yvFFCJscB9Mw .node circle,#mermaid-svg-Nlq9yvFFCJscB9Mw .node ellipse,#mermaid-svg-Nlq9yvFFCJscB9Mw .node polygon,#mermaid-svg-Nlq9yvFFCJscB9Mw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Nlq9yvFFCJscB9Mw .rough-node .label text,#mermaid-svg-Nlq9yvFFCJscB9Mw .node .label text,#mermaid-svg-Nlq9yvFFCJscB9Mw .image-shape .label,#mermaid-svg-Nlq9yvFFCJscB9Mw .icon-shape .label{text-anchor:middle;}#mermaid-svg-Nlq9yvFFCJscB9Mw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Nlq9yvFFCJscB9Mw .rough-node .label,#mermaid-svg-Nlq9yvFFCJscB9Mw .node .label,#mermaid-svg-Nlq9yvFFCJscB9Mw .image-shape .label,#mermaid-svg-Nlq9yvFFCJscB9Mw .icon-shape .label{text-align:center;}#mermaid-svg-Nlq9yvFFCJscB9Mw .node.clickable{cursor:pointer;}#mermaid-svg-Nlq9yvFFCJscB9Mw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Nlq9yvFFCJscB9Mw .arrowheadPath{fill:#333333;}#mermaid-svg-Nlq9yvFFCJscB9Mw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Nlq9yvFFCJscB9Mw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Nlq9yvFFCJscB9Mw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Nlq9yvFFCJscB9Mw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Nlq9yvFFCJscB9Mw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Nlq9yvFFCJscB9Mw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Nlq9yvFFCJscB9Mw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Nlq9yvFFCJscB9Mw .cluster text{fill:#333;}#mermaid-svg-Nlq9yvFFCJscB9Mw .cluster span{color:#333;}#mermaid-svg-Nlq9yvFFCJscB9Mw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Nlq9yvFFCJscB9Mw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Nlq9yvFFCJscB9Mw rect.text{fill:none;stroke-width:0;}#mermaid-svg-Nlq9yvFFCJscB9Mw .icon-shape,#mermaid-svg-Nlq9yvFFCJscB9Mw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Nlq9yvFFCJscB9Mw .icon-shape p,#mermaid-svg-Nlq9yvFFCJscB9Mw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Nlq9yvFFCJscB9Mw .icon-shape .label rect,#mermaid-svg-Nlq9yvFFCJscB9Mw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Nlq9yvFFCJscB9Mw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Nlq9yvFFCJscB9Mw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Nlq9yvFFCJscB9Mw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 1. 命令执行
写入 AOF 缓冲区
(aof_buf)
2. 缓冲区 → 内核
write() 系统调用
数据进入 OS page cache
3. 内核 → 磁盘
fsync() 系统调用
真正落盘
| 步骤 | 位置 | 风险 |
|---|---|---|
| 命令写入 aof_buf | Redis 进程内存 | 进程崩溃就丢失 |
| write() 到 page cache | 操作系统内核缓冲区 | 机器断电就丢失 |
| fsync() 到磁盘 | 物理磁盘 | 磁盘损坏才丢失 |
3.3 fsync 策略:AOF 的性能与安全博弈
Redis 通过 appendfsync 配置控制第三步的触发频率:
| 策略 | 行为 | 数据安全 | 性能 | 推荐场景 |
|---|---|---|---|---|
| always | 每条命令都 fsync | 最高,基本不丢数据 | 最差(磁盘瓶颈) | 对数据安全性要求极致的场景 |
| everysec | 每秒 fsync 一次 | 最多丢 1 秒数据 | 较好(默认,推荐) | 99% 的生产环境 |
| no | 不主动 fsync,由 OS 决定 | 不可控(通常 30 秒) | 最好 | 纯缓存,数据可丢 |
everysec 的实现细节 :Redis 启动一个后台线程,每秒检查一次,如果距离上次 fsync 超过 1 秒,就执行 fsync。这里的精妙之处在于------它是一个独立的 fsync 线程,不会阻塞主线程处理命令。即使磁盘 IO 慢,也只是 fsync 线程被阻塞,客户端请求照常处理。
3.4 AOF 重写(Rewrite):给 AOF 文件瘦身
AOF 文件会随着时间无限增长。一个计数器 INCR counter 执行 100 万次,AOF 文件里会有 100 万条 INCR 命令,但实际上只需要一条 SET counter 1000000 就够了。
重写的目的:用最少命令重建当前数据集,消除冗余。
bash
# 原始 AOF 文件(冗余)
SET counter 1
INCR counter # × 99999 次
# 文件大小:~5MB
# 重写后的 AOF 文件(精简)
SET counter 100000
# 文件大小:~30 字节
AOF 重写也是通过 BGREWRITEAOF + 子进程完成的,流程与 BGSAVE 类似:
#mermaid-svg-Epg8muNqTerClcEJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Epg8muNqTerClcEJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Epg8muNqTerClcEJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Epg8muNqTerClcEJ .error-icon{fill:#552222;}#mermaid-svg-Epg8muNqTerClcEJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Epg8muNqTerClcEJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Epg8muNqTerClcEJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Epg8muNqTerClcEJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Epg8muNqTerClcEJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Epg8muNqTerClcEJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Epg8muNqTerClcEJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Epg8muNqTerClcEJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Epg8muNqTerClcEJ .marker.cross{stroke:#333333;}#mermaid-svg-Epg8muNqTerClcEJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Epg8muNqTerClcEJ p{margin:0;}#mermaid-svg-Epg8muNqTerClcEJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Epg8muNqTerClcEJ .cluster-label text{fill:#333;}#mermaid-svg-Epg8muNqTerClcEJ .cluster-label span{color:#333;}#mermaid-svg-Epg8muNqTerClcEJ .cluster-label span p{background-color:transparent;}#mermaid-svg-Epg8muNqTerClcEJ .label text,#mermaid-svg-Epg8muNqTerClcEJ span{fill:#333;color:#333;}#mermaid-svg-Epg8muNqTerClcEJ .node rect,#mermaid-svg-Epg8muNqTerClcEJ .node circle,#mermaid-svg-Epg8muNqTerClcEJ .node ellipse,#mermaid-svg-Epg8muNqTerClcEJ .node polygon,#mermaid-svg-Epg8muNqTerClcEJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Epg8muNqTerClcEJ .rough-node .label text,#mermaid-svg-Epg8muNqTerClcEJ .node .label text,#mermaid-svg-Epg8muNqTerClcEJ .image-shape .label,#mermaid-svg-Epg8muNqTerClcEJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-Epg8muNqTerClcEJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Epg8muNqTerClcEJ .rough-node .label,#mermaid-svg-Epg8muNqTerClcEJ .node .label,#mermaid-svg-Epg8muNqTerClcEJ .image-shape .label,#mermaid-svg-Epg8muNqTerClcEJ .icon-shape .label{text-align:center;}#mermaid-svg-Epg8muNqTerClcEJ .node.clickable{cursor:pointer;}#mermaid-svg-Epg8muNqTerClcEJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Epg8muNqTerClcEJ .arrowheadPath{fill:#333333;}#mermaid-svg-Epg8muNqTerClcEJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Epg8muNqTerClcEJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Epg8muNqTerClcEJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Epg8muNqTerClcEJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Epg8muNqTerClcEJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Epg8muNqTerClcEJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Epg8muNqTerClcEJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Epg8muNqTerClcEJ .cluster text{fill:#333;}#mermaid-svg-Epg8muNqTerClcEJ .cluster span{color:#333;}#mermaid-svg-Epg8muNqTerClcEJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Epg8muNqTerClcEJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Epg8muNqTerClcEJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-Epg8muNqTerClcEJ .icon-shape,#mermaid-svg-Epg8muNqTerClcEJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Epg8muNqTerClcEJ .icon-shape p,#mermaid-svg-Epg8muNqTerClcEJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Epg8muNqTerClcEJ .icon-shape .label rect,#mermaid-svg-Epg8muNqTerClcEJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Epg8muNqTerClcEJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Epg8muNqTerClcEJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Epg8muNqTerClcEJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 触发 AOF 重写
fork 子进程
子进程:遍历数据库
生成新的 AOF 文件
(写入临时文件)
重写期间
主进程的写命令
同时写入 aof_buf + aof_rewrite_buf
子进程完成重写
主进程:将 aof_rewrite_buf
追加到新 AOF 文件
原子 rename 替换旧文件
触发条件(redis.conf):
bash
auto-aof-rewrite-percentage 100 # 当前 AOF 文件是上次重写后大小的 2 倍
auto-aof-rewrite-min-size 64mb # 且 AOF 文件大于 64MB
四、混合持久化(Redis 4.0+):RDB + AOF 的王炸组合
4.1 解决了什么问题?
RD B 和 AOF 各有短板:
- RDB 恢复快但可能丢数据多 → AOF 恢复慢但丢数据少
- AOF 恢复慢但丢数据少 → RDB 恢复快但可能丢数据多
有没有一种方案,能用 RDB 的速度做恢复,用 AOF 的粒度保数据?这就是混合持久化。
4.2 混合持久化的文件结构
bash
# redis.conf
aof-use-rdb-preamble yes # Redis 5.0+ 默认开启
混合持久化的 AOF 文件结构:
[ RDB 二进制快照部分 ] + [ AOF 文本命令部分 ]
↑ 重写时的全量数据 ↑ 重写后的增量命令
文件内容示意:
REDIS0009ú...(RDB 格式)...þ(EOF 标记)
*3\r\n$3\r\nSET\r\n... (后续增量 AOF 命令)
*3\r\n$4\r\nINCR\r\n...
重写时:子进程以 RDB 格式把当前全量数据写入 AOF 文件开头。
重写后:主进程的所有新写命令以 AOF 格式追加在文件末尾。
恢复时:先按 RDB 协议加载前半部分的快照数据,再按 AOF 协议回放后半部分的增量命令。
4.3 这是目前的最佳实践
混合持久化 = RDB 的恢复速度 + AOF 的数据安全性 ,是 Redis 4.0 以来最重要的持久化改进。2024 年的生产环境,aof-use-rdb-preamble yes 应该成为标配。
五、Redis 7.0 Multi-Part AOF:持久化架构的一次重构
5.1 老版本 AOF 的痛点
在 Redis 7.0 之前,AOF 管理有几个让人头疼的问题:
-
重写过程中的增量缓冲 :旧版本用一个内存缓冲区
aof_rewrite_buf存重写期间的增量命令。如果写入量大,这个缓冲区可能撑爆内存。 -
单一 AOF 文件:只有一个 AOF 文件,重写时要创建临时文件然后 rename。如果中途失败,旧文件状态不确定。
-
AOF 时间戳没有原生支持:做增量同步或数据恢复时不够灵活。
5.2 Multi-Part AOF 架构
Redis 7.0 将 AOF 文件拆分为多个部分,每个部分有明确的职责:
#mermaid-svg-2fTNo7gXH7Ah8AXL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2fTNo7gXH7Ah8AXL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2fTNo7gXH7Ah8AXL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2fTNo7gXH7Ah8AXL .error-icon{fill:#552222;}#mermaid-svg-2fTNo7gXH7Ah8AXL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2fTNo7gXH7Ah8AXL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2fTNo7gXH7Ah8AXL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2fTNo7gXH7Ah8AXL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2fTNo7gXH7Ah8AXL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2fTNo7gXH7Ah8AXL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2fTNo7gXH7Ah8AXL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2fTNo7gXH7Ah8AXL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2fTNo7gXH7Ah8AXL .marker.cross{stroke:#333333;}#mermaid-svg-2fTNo7gXH7Ah8AXL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2fTNo7gXH7Ah8AXL p{margin:0;}#mermaid-svg-2fTNo7gXH7Ah8AXL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2fTNo7gXH7Ah8AXL .cluster-label text{fill:#333;}#mermaid-svg-2fTNo7gXH7Ah8AXL .cluster-label span{color:#333;}#mermaid-svg-2fTNo7gXH7Ah8AXL .cluster-label span p{background-color:transparent;}#mermaid-svg-2fTNo7gXH7Ah8AXL .label text,#mermaid-svg-2fTNo7gXH7Ah8AXL span{fill:#333;color:#333;}#mermaid-svg-2fTNo7gXH7Ah8AXL .node rect,#mermaid-svg-2fTNo7gXH7Ah8AXL .node circle,#mermaid-svg-2fTNo7gXH7Ah8AXL .node ellipse,#mermaid-svg-2fTNo7gXH7Ah8AXL .node polygon,#mermaid-svg-2fTNo7gXH7Ah8AXL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2fTNo7gXH7Ah8AXL .rough-node .label text,#mermaid-svg-2fTNo7gXH7Ah8AXL .node .label text,#mermaid-svg-2fTNo7gXH7Ah8AXL .image-shape .label,#mermaid-svg-2fTNo7gXH7Ah8AXL .icon-shape .label{text-anchor:middle;}#mermaid-svg-2fTNo7gXH7Ah8AXL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2fTNo7gXH7Ah8AXL .rough-node .label,#mermaid-svg-2fTNo7gXH7Ah8AXL .node .label,#mermaid-svg-2fTNo7gXH7Ah8AXL .image-shape .label,#mermaid-svg-2fTNo7gXH7Ah8AXL .icon-shape .label{text-align:center;}#mermaid-svg-2fTNo7gXH7Ah8AXL .node.clickable{cursor:pointer;}#mermaid-svg-2fTNo7gXH7Ah8AXL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2fTNo7gXH7Ah8AXL .arrowheadPath{fill:#333333;}#mermaid-svg-2fTNo7gXH7Ah8AXL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2fTNo7gXH7Ah8AXL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2fTNo7gXH7Ah8AXL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2fTNo7gXH7Ah8AXL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2fTNo7gXH7Ah8AXL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2fTNo7gXH7Ah8AXL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2fTNo7gXH7Ah8AXL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2fTNo7gXH7Ah8AXL .cluster text{fill:#333;}#mermaid-svg-2fTNo7gXH7Ah8AXL .cluster span{color:#333;}#mermaid-svg-2fTNo7gXH7Ah8AXL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2fTNo7gXH7Ah8AXL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2fTNo7gXH7Ah8AXL rect.text{fill:none;stroke-width:0;}#mermaid-svg-2fTNo7gXH7Ah8AXL .icon-shape,#mermaid-svg-2fTNo7gXH7Ah8AXL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2fTNo7gXH7Ah8AXL .icon-shape p,#mermaid-svg-2fTNo7gXH7Ah8AXL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2fTNo7gXH7Ah8AXL .icon-shape .label rect,#mermaid-svg-2fTNo7gXH7Ah8AXL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2fTNo7gXH7Ah8AXL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2fTNo7gXH7Ah8AXL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2fTNo7gXH7Ah8AXL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} History AOF(历史文件)
Incremental AOF(增量文件)
Base AOF(基础文件)
重写后
重写后
AOF Manifest(清单文件)
appendonly.aof.manifest
记录所有 AOF 文件列表
appendonly.aof.1.base.rdb
RDB 格式的全量快照
appendonly.aof.1.incr.aof
增量 AOF 命令
appendonly.aof.2.incr.aof
下一个增量文件
appendonly.aof.1.base.rdb
被新 base 替代的旧快照
appendonly.aof.1.incr.aof
已被合并的旧增量
文件类型说明:
| 文件类型 | 后缀 | 内容 | 创建时机 |
|---|---|---|---|
| Base | .base.rdb |
RDB 格式的全量快照 | AOF 重写时创建 |
| Incremental | .incr.aof |
增量 AOF 命令 | 达到大小阈值时轮转 |
| Manifest | .manifest |
清单(记录所有文件的顺序和类型) | 始终存在 |
5.3 Manifest 文件示例
# appendonly.aof.manifest
file appendonly.aof.1.base.rdb seq 1 type b
file appendonly.aof.1.incr.aof seq 1 type i
file appendonly.aof.2.incr.aof seq 2 type i
seq:序列号,恢复时按序加载type b:base 文件(RDB 格式)type i:incremental 文件(AOF 格式)
5.4 Multi-Part AOF 的优势
| 问题 | 旧版本 | Redis 7.0 Multi-Part AOF |
|---|---|---|
| 重写时增量缓冲过大 | 单一大内存缓冲区 | 写到新的 incr 文件,无内存压力 |
| 重写失败 | 临时文件状态不清 | 新文件独立,旧文件仍可用 |
| 文件过大 | 手动 BGREWRITEAOF | 自动轮转(incr 文件达到阈值自动切新文件) |
| 恢复灵活性 | 全有或全无 | 可以按 manifest 选择性加载 |
| 磁盘空间 | 重写后才释放旧文件 | 渐进式清理历史文件 |
六、生产环境持久化方案选型
理论知识讲完了,下面是实实在在生产环境的配置方案。
6.1 方案对比
| 方案 | 配置 | 数据安全性 | 恢复速度 | 性能影响 | 适用场景 |
|---|---|---|---|---|---|
| 纯缓存 | RDB 关闭 + AOF 关闭 | 零 | - | 最优 | 纯缓存,数据可从数据库重建 |
| 仅 RDB | save 900 1 300 10 60 10000 |
可能丢几分钟 | 快 | 低 | 缓存为主,可接受少量数据丢失 |
| 仅 AOF | appendonly yes + appendfsync everysec |
高(最多 1 秒) | 慢 | 中 | 数据重要,可接受稍慢的恢复 |
| RDB + AOF 双开 | 同时开启 RDB 和 AOF | 高 | 较快(优先用 AOF 恢复) | 中 | 推荐方案 |
| 混合持久化 | 双开 + aof-use-rdb-preamble yes |
高 | 快 | 中 | 生产最佳实践 |
6.2 推荐的生产配置
bash
# ============ RDB 配置 ============
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes # BGSAVE 失败时拒绝写入(防止数据持续丢失而不自知)
rdbcompression yes # 开启 LZF 压缩
rdbchecksum yes # CRC64 校验
# ============ AOF 配置 ============
appendonly yes # 开启 AOF
appendfsync everysec # 每秒 fsync,性能与安全的平衡点
no-appendfsync-on-rewrite yes # 重写期间不 fsync(避免 IO 争抢,轻微降低安全性提升性能)
auto-aof-rewrite-percentage 100 # AOF 文件增长 100% 触发重写
auto-aof-rewrite-min-size 64mb # AOF 文件至少 64MB 才触发重写
aof-use-rdb-preamble yes # 混合持久化(Redis 7.0+ 默认)
# ============ Multi-Part AOF(Redis 7.0+)============
aof-timestamp-enabled no # 是否在 AOF 中记录时间戳(用于增量恢复,一般不需要)
6.3 常见坑点与避坑指南
坑 1:AOF 文件损坏
bash
# 症状:重启时 Redis 报错 "Bad file format reading the append only file"
# 修复:
redis-check-aof --fix appendonly.aof
# 原理:截断到最后一个完整命令,丢弃尾部损坏部分
坑 2:AOF 文件过大导致恢复极慢
bash
# 检查 AOF 大小
ls -lh appendonly.aof
# 如果几十 GB,手动触发重写
redis-cli BGREWRITEAOF
# 预防:降低 auto-aof-rewrite-min-size,提高重写频率
坑 3:BGSAVE 期间内存暴涨 OOM
bash
# 本质是 COW 导致的内存翻倍
# 解决:
# 1. 预留 50% 以上空闲内存
# 2. 调整 save 触发条件,避免写入高峰触发
# 3. 使用 info memory 监控 used_memory_rss / used_memory 比值
坑 4:写入性能突然下降
bash
# 如果配置了 appendfsync always,磁盘 IO 直接成为瓶颈
# 改成 everysec 即可解决大部分问题
# 如果不是 always 模式,检查是否正在 AOF 重写
info persistence | grep aof_rewrite_in_progress
# 设置 no-appendfsync-on-rewrite yes 可缓解
6.4 监控指标
生产环境必需监控的持久化相关指标:
bash
# 关键指标
INFO Persistence
# RDB
rdb_last_bgsave_status # ok / err,上次 BGSAVE 是否成功
rdb_last_bgsave_time_sec # 上次 BGSAVE 耗时(秒),突然变长要警惕
rdb_changes_since_last_save # 距离上次保存的变更数,越大越危险
# AOF
aof_enabled # 是否开启
aof_current_size # 当前 AOF 文件大小
aof_last_rewrite_time_sec # 上次重写耗时
aof_rewrite_in_progress # 是否正在重写
aof_last_bgrewrite_status # 上次重写状态
aof_buffer_length # AOF 缓冲区大小(过大说明写入积压)
# fork 耗时(重点监控)
latest_fork_usec # 最近一次 fork 的耗时(微秒)
# 如果这个值超过 100ms,说明系统内存压力大,需要关注
七、总结
本文从三个层面讲透了 Redis 持久化:
| 层面 | 核心内容 |
|---|---|
| 原理层 | RDB 的 fork+COW 机制、AOF 的三阶段写入和 fsync 策略、混合持久化的文件结构 |
| 演进层 | Redis 7.0 Multi-Part AOF 如何解决旧版本缓冲区膨胀和文件管理的痛点 |
| 实践层 | 多种方案的配置对比、5 个常见坑点及解决方案、生产监控指标 |
一句话总结持久化选型 :绝大多数场景直接上 RDB + AOF + 混合持久化 ,appendfsync 设 everysec,aof-use-rdb-preamble 设 yes。这是经过千万级实例验证的最佳配置。
如有疑问或指正,欢迎在评论区交流。