219 lines
8.6 KiB
Markdown
219 lines
8.6 KiB
Markdown
|
|
---
|
|||
|
|
tags: [Redis, 缓存, 持久化, AOF]
|
|||
|
|
create time: 2026-05-15 18:13
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# AOF 持久化
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
AOF(Append Only File)以**追加写日志**的方式记录每个写操作。相比 RDB 的快照式恢复,AOF 提供更高的数据安全性——你可以根据策略做到**不丢数据、丢 1 秒、或丢 N 条**。代价是文件更大、恢复更慢。
|
|||
|
|
|
|||
|
|
## 核心机制
|
|||
|
|
|
|||
|
|
### 命令追加过程
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart LR
|
|||
|
|
Client["客户端写入"] --> Server["Redis 主线程<br/>执行命令 + 返回结果"]
|
|||
|
|
Server --> Buffer["aof_buffer<br/>内核/用户态缓冲区"]
|
|||
|
|
Buffer --> FSyncPolicy{"刷新策略?"}
|
|||
|
|
FSyncPolicy -->|everysec| KernelBuf["操作系统页缓存"]
|
|||
|
|
KernelBuf --> Disk["磁盘 aof 文件"]
|
|||
|
|
FSyncPolicy -->|always| Disk
|
|||
|
|
FSyncPolicy -->|no| OSPage["交给 OS 自己 flush"]
|
|||
|
|
|
|||
|
|
style Server fill:#f9d,stroke:#96c
|
|||
|
|
style Disk fill:#dfd,stroke:#6c6
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!TIP] 一条命令是怎么落到磁盘的?
|
|||
|
|
> 1. 客户端发送命令 → 2. Redis 在主线程执行并返回结果 → 3. 同时将命令追加到 `aof_buffer` → 4. 根据 `appendfsync` 策略最终刷入磁盘。整个过程对主线程几乎是异步的。
|
|||
|
|
|
|||
|
|
### 刷新策略配置
|
|||
|
|
|
|||
|
|
```conf
|
|||
|
|
# appendfsync always # 每次写入都 fsync -> 最安全,性能差
|
|||
|
|
appendfsync everysec # 每秒 fsync <- 推荐(最佳平衡)
|
|||
|
|
# appendfsync no # 完全依赖 OS -> 性能最好,风险最高
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| 策略 | 丢失风险 | 吞吐量 (ops/s) | 适用场景 |
|
|||
|
|
|------|---------|---------------|---------|
|
|||
|
|
| `always` | 零丢失 | ~900 | 金融级要求(极少用) |
|
|||
|
|
| `everysec` | 最多丢 1 秒 | ~74k | **生产标准配置** |
|
|||
|
|
| `no` | OS 决定,可能丢大量 | ~93k | 可接受全量丢失的场景 |
|
|||
|
|
|
|||
|
|
> [!TIP] everysec 的性能真相
|
|||
|
|
> `everysec` 不会阻塞主线程——它把 fsync 放到单独的后台线程。即使某个 fsync 花了 2s(磁盘卡顿),也只是这一秒的写入被推迟到下一周期,不会影响主线程。
|
|||
|
|
|
|||
|
|
## AOF 重写(Rewrite)
|
|||
|
|
|
|||
|
|
AOF 文件会不断增长,需要定期重写来去除无效命令(如 key 被 DEL 后又 SET)。
|
|||
|
|
|
|||
|
|
### 重写触发条件
|
|||
|
|
|
|||
|
|
```conf
|
|||
|
|
auto-aof-rewrite-percentage 100 # AOF 比上次 rewrite 后增大的百分比
|
|||
|
|
auto-aof-rewrite-min-size 64mb # AOF 最小绝对大小(避免刚启动就 rewrite)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
示例:当前 AOF = 128MB,上次 rewrite 后大小 = 64MB
|
|||
|
|
- 增量 = 128 - 64 = 64MB
|
|||
|
|
- 增量占比 = 64 / 64 = 100% -> 触发 rewrite
|
|||
|
|
- 但如果当前 AOF < 64MB -> 不触发
|
|||
|
|
|
|||
|
|
> [!QUESTION] 如果设置 auto-aof-rewrite-min-size 太大或太小会发生什么?
|
|||
|
|
> - **太大**:AOF 膨胀了很久才触发重写,磁盘空间浪费严重
|
|||
|
|
> - **太小**:频繁重写,增加不必要的 CPU 和 IO 开销
|
|||
|
|
>
|
|||
|
|
> 这本质上是一个「文件瘦身频率」的工程权衡题。
|
|||
|
|
|
|||
|
|
### 重写过程(类似 RDB fork)
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
sequenceDiagram
|
|||
|
|
participant S as Redis Server
|
|||
|
|
participant P as 主进程
|
|||
|
|
child CG as Rewriter Child
|
|||
|
|
participant C as 重写子进程
|
|||
|
|
participant Log as appendonly.aof
|
|||
|
|
participant NewLog as appendonly.aof_rewrite_tmp
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
S->>P: BGREWRITEAOF
|
|||
|
|
P->>S: OK
|
|||
|
|
P->>C: fork() 子进程
|
|||
|
|
C->>NewLog: fork 时刻起遍历内存 -> 重写为精简命令
|
|||
|
|
Note over P,C: Main process continues serving requests
|
|||
|
|
loop 每个新写命令
|
|||
|
|
P->>Log: 追加原始命令
|
|||
|
|
P->>C: 同时写入 aof_rewrite_buf<br/>(重写期间的新操作)
|
|||
|
|
end
|
|||
|
|
C->>NewLog: 追加 aof_rewrite_buf 中的内容
|
|||
|
|
C->>P: 完成
|
|||
|
|
P->>Log: rename(原 -> 旧.bak, 新 -> 当前)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!WARNING] 重写的内存开销
|
|||
|
|
> fork 后重写子进程也使用 COW 机制。如果你的 AOF 正在持续增长且写入量大,可能需要评估可用内存是否足够支撑 fork 的 COW 副本。在 100GB+ 的 Redis 实例上,一次 fork 可能导致额外的几 GB 内存占用。
|
|||
|
|
|
|||
|
|
## AOF 开启方式
|
|||
|
|
|
|||
|
|
```conf
|
|||
|
|
appendonly yes # 开启 AOF
|
|||
|
|
appendfilename "appendonly.aof" # AOF 文件名
|
|||
|
|
dir /var/lib/redis # AOF/RDB 文件存放目录
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Redis 7.x 目录结构(新版本使用多文件目录而非单文件)
|
|||
|
|
ls /var/lib/redis/appendonlydir/
|
|||
|
|
total 256M
|
|||
|
|
drwxr-x--- 2 redis redis 4.0K May 15 18:00 .
|
|||
|
|
drwxr-x--- 2 redis redis 4.0K May 15 18:00 ..
|
|||
|
|
-rw-r----- 1 redis redis 16K May 15 18:00 manifest.aof
|
|||
|
|
-rw-r----- 1 redis redis 128M May 15 18:00 base.rdb
|
|||
|
|
-rw-r----- 1 redis redis 128M May 15 18:01 incr-0000000000000003.aof
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 思考:为什么 AOF 会持续增长?
|
|||
|
|
|
|||
|
|
想象以下操作序列:`SET x 1 -> SET x 2 -> SET x 3 -> DEL x`。如果不做处理,AOF 中会记录这 4 条命令,但 key `x` 已经不存在了——这些写入了磁盘却没有任何价值。这就是为什么需要**重写**。
|
|||
|
|
|
|||
|
|
## 混合持久化(Hybrid Persistence)
|
|||
|
|
|
|||
|
|
混合持久化是 Redis 4.0 引入、Redis 7 默认开启的特性——在 AOF 重写时,先用 RDB 快照全量备份当前内存状态,再用 AOF 追加增量变化。
|
|||
|
|
|
|||
|
|
```conf
|
|||
|
|
# redis.conf
|
|||
|
|
aof-use-rdb-preamble yes # 开启混合持久化(默认值)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart LR
|
|||
|
|
BG["BGREWRITEAOF"] --> Fork["fork 子进程"]
|
|||
|
|
Fork --> RDBPart["RDB 全量部分<br/>二进制快照,写入极快"]
|
|||
|
|
Fork --> AOFPart["AOF 增量部分<br/>仅含 fork 后的写操作"]
|
|||
|
|
RDBPart --> Merge["合并为临时文件"]
|
|||
|
|
AOFPart --> Merge
|
|||
|
|
Merge --> Rename["原子 rename 替换"]
|
|||
|
|
|
|||
|
|
style RDBPart fill:#bbf,stroke:#66c
|
|||
|
|
style AOFPart fill:#dfd,stroke:#090
|
|||
|
|
style Merge fill:#ffd,stroke:#cc0
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**好处有两个:**
|
|||
|
|
1. **恢复速度快**:启动时先加载 RDB 全量快照(二进制格式比解析 AOF 命令快 10~100 倍),再回放少量 AOF 增量命令
|
|||
|
|
2. **文件体积小**:相比纯 AOF 记录每条操作指令,混合模式下文件通常缩小 70%~90%
|
|||
|
|
|
|||
|
|
> [!WARNING] 恢复流程的变化
|
|||
|
|
> 混合模式下,Redis 启动时先读取 `base.rdb` 还原全部数据,再逐条执行增量 `.aof` 段中的命令。如果只保留 AOF 段而删除 `base.rdb`,恢复将退化为纯 AOF 模式,速度大幅下降。
|
|||
|
|
|
|||
|
|
## AOF vs RDB 对比
|
|||
|
|
|
|||
|
|
| 维度 | AOF (`everysec`) | RDB(典型配置) |
|
|||
|
|
|------|-----------------|----------------|
|
|||
|
|
| 数据安全性 | ⭐⭐⭐ 最多丢 1s | ⭐ 可能丢数分钟 ~ 小时 |
|
|||
|
|
| 恢复速度 | ⭐⭐ 较慢(需逐条 replay) | ⭐⭐⭐ 快(二进制直接加载) |
|
|||
|
|
| CPU 开销 | ⭐⭐ 每秒一次 fsync | ⭐ 仅在 fork 时短暂增长 |
|
|||
|
|
| 磁盘 IO | ⭐⭐ 持续写入日志 | ⭐ 仅在快照间隔触发 |
|
|||
|
|
| 文件大小 | ⭐ 较大(逐条记录) | ⭐⭐⭐ 较小(二进制压缩) |
|
|||
|
|
| 数据丢失窗口 | <= 1 秒 | = save 间隔时间 |
|
|||
|
|
|
|||
|
|
> [!QUESTION] 什么时候该用 RDB 而不是 AOF?
|
|||
|
|
> 如果你的业务场景是**缓存而非数据库**——数据可以重新构建或允许短暂丢失,那 RDB 就够了。只有当数据丢失会造成长期损失(如订单、余额)时,才需要上 AOF。
|
|||
|
|
|
|||
|
|
## AOF 故障恢复
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 如果 AOF 损坏(断电等原因导致截断)
|
|||
|
|
redis-server --repair appendonly.aof
|
|||
|
|
# 或 Redis 7 下修复整个目录
|
|||
|
|
redis-server --fix appendonlydir/aof-manifest
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### fsync 失败时的行为
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
fsync -> EIO(磁盘错误)
|
|||
|
|
↓
|
|||
|
|
ERROR "I/O error writing to APPEND ONLY FILE..."
|
|||
|
|
↓
|
|||
|
|
立即 SHUTDOWN NOSAVE(停止服务防止数据进一步损坏)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!NOTE] Redis 为什么会主动 shutdown?
|
|||
|
|
> 这是一种「fail-fast」设计哲学。AOF 的核心价值就是数据安全,当底层磁盘出现问题时,继续运行只会导致更多数据静默损坏。宁可停机也不能让用户在「不确定」的数据上继续业务。
|
|||
|
|
|
|||
|
|
> [!WARNING] AOF 不是万能的
|
|||
|
|
> - AOF 修复工具只能恢复大部分,极端情况下会有部分命令丢失
|
|||
|
|
> - 定期做冷备份(RDB 迁移到 OSS/S3)仍然是必须的兜底手段
|
|||
|
|
|
|||
|
|
## AOF vs RDB 选型决策树
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD
|
|||
|
|
Start{"你的首要需求是?"} -->|"数据安全第一"| AOF["AOF everysec"]
|
|||
|
|
Start -->|"恢复速度第一"| RDB["RDB 仅"]
|
|||
|
|
Start -->|"两者兼顾"| HYBRID["混合持久化"]
|
|||
|
|
|
|||
|
|
HYBRID --> Check{"数据重要性极高?"}
|
|||
|
|
Check -->|"是"| ALWAYS["AOF always<br/>+ 每天冷备"]
|
|||
|
|
Check -->|"否"| EVERYSEC["AOF everysec<br/>+ 混合持久化"]
|
|||
|
|
|
|||
|
|
style HYBRID fill:#dfd,stroke:#090
|
|||
|
|
style ALWAYS fill:#fdd,stroke:#900
|
|||
|
|
style EVERYSEC fill:#dfd,stroke:#090
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!TIP] 生产环境黄金组合
|
|||
|
|
> `AOF everysec + 混合持久化 + 定时 rdb 冷备到对象存储`——几乎覆盖所有常见场景。
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[hhs/Redis/04-RDB持久化]] — RDB 快照机制
|
|||
|
|
- [[hhs/Redis/06-主从与哨兵]] — 哨兵监控中 AOF 状态检查
|
|||
|
|
- [[hhs/Redis/09-运维调优]] — AOF 文件膨胀治理
|