前置知识: Redis

AOF 日志持久化

7 min高级

Redis AOF日志持久化:appendfsync策略、AOF重写机制、配置优化与恢复流程

1. AOF 概述

AOF(Append Only File)以日志形式记录 Redis 服务器收到的每一条写命令,以追加(append)方式写入 AOF 文件。Redis 重启时通过重放 AOF 文件中的命令来恢复数据。

AOF 的核心优势:

  • 数据安全性高:最多丢失 1 秒数据(everysec 策略)
  • 可读性好:AOF 文件是文本格式,可直接查看和修改
  • 容错性强:即使 AOF 文件尾部损坏,redis-check-aof 可修复
  • 实时性:每条写命令都追加到日志

AOF 的主要不足:

  • 文件体积大:记录所有写命令,远大于 RDB
  • 恢复速度慢:需要重放所有命令
  • 对性能有影响:频繁的磁盘写入

2. AOF 工作流程

flowchart TD
    T0["客户端写命令"]
    T1["Redis 服务器执行命令"]
    T2["命令追加到 AOF 缓冲区(aof_buf)"]
    T3["根据 appendfsync 策略刷盘"]
    T4["AOF 文件"]
    T0 --> T1
    T1 --> T2
    T2 --> T3
    T3 --> T4

2.1 命令追加

每执行一条写命令后,Redis 将该命令以 Redis 协议格式追加到 aof_buf 缓冲区:

// 伪代码
void feedAppendOnlyFile(struct redisCommand *cmd, int argc, robj **argv) {
    // 将命令转换为 RESP 协议格式
    buf = catAppendOnlyGenericCommand(argc, argv);
    // 追加到 aof_buf
    aof_buf = sdscatlen(aof_buf, buf, sdslen(buf));
}

2.2 文件写入与同步

Redis 的事件循环(beforeSleep)中,将 aof_buf 的内容写入 AOF 文件:

// 伪代码
void flushAppendOnlyFile(int force) {
    // 将 aof_buf 写入 AOF 文件
    nwritten = write(server.aof_fd, server.aof_buf, sdslen(server.aof_buf));
    // 根据 appendfsync 策略决定是否 fsync
    if (server.aof_fsync == APPENDFSYNC_ALWAYS) {
        fsync(server.aof_fd);
    }
}

3. appendfsync 策略

appendfsync 配置项决定了 AOF 数据刷盘的频率,是数据安全与性能之间的关键权衡:

appendfsync always     # 每条命令都 fsync
appendfsync everysec   # 每秒 fsync(默认推荐)
appendfsync no         # 由操作系统决定刷盘时机

3.1 always

  • 行为:每条写命令执行后立即调用 fsync()
  • 数据安全:最高,最多丢失一条命令
  • 性能影响:最严重,每秒只能处理数百到数千次写入
  • 适用场景:对数据安全要求极高的金融场景

3.2 everysec

  • 行为:每秒调用一次 fsync()
  • 数据安全:较高,最多丢失 1 秒数据
  • 性能影响:可接受,与 no 策略性能差距不大
  • 适用场景:大多数生产环境(默认推荐)

3.3 no

  • 行为:不主动 fsync(),由操作系统决定
  • 数据安全:最低,可能丢失最近数秒数据
  • 性能影响:最好,依赖 OS 缓冲区刷盘
  • 适用场景:纯缓存或可容忍数据丢失的场景

3.4 三种策略对比

策略fsync 频率最多丢失数据写入性能推荐度
always每条命令1条命令低
everysec每秒1秒数据中
noOS决定数秒数据高

4. AOF 重写机制

随着时间推移,AOF 文件会不断增长。Redis 通过 AOF 重写(rewrite)机制压缩文件体积。

4.1 重写原理

AOF 重写不是读取旧 AOF 文件进行分析,而是直接读取当前数据库状态,用最少的命令重新生成 AOF 文件。

示例:

# 旧 AOF 文件中可能有以下 6 条命令
SET counter 1
INCR counter
INCR counter
INCR counter
DEL counter
SET counter 100

# 重写后只需 1 条命令
SET counter 100

列表合并示例:

# 旧 AOF 文件
RPUSH list a b c
RPUSH list d e
LPOP list
RPUSH list f

# 重写后
RPUSH list b c d e f

4.2 AOF 重写触发条件

# 自动触发条件
auto-aof-rewrite-percentage 100   # AOF 文件大小比上次重写后增长 100%
auto-aof-rewrite-min-size 64mb    # AOF 文件最小 64MB 才触发重写

# 手动触发
BGREWRITEAOF

自动触发判断逻辑:

当前文件大小≥上次重写后大小×(1+auto-aof-rewrite-percentage100)\text{当前文件大小} \geq \text{上次重写后大小} \times (1 + \frac{\text{auto-aof-rewrite-percentage}}{100})

且当前文件大小 ≥\geq auto-aof-rewrite-min-size

4.3 AOF 重写流程(Redis 7.0+ 多文件实现)

Redis 7.0 重构了 AOF(multi-part AOF),重写不再使用「旧 AOF 缓冲区 + 重写缓冲区双写」的老方案,而是引入 base / incr 多文件结构:

1. 主进程 fork() 创建子进程
2. 子进程根据当前内存状态把全量数据写成 base 文件
   (appendonly.aof.<序号>.base.rdb,RDB 格式;可用 aof-use-rdb-preamble no
     改为纯 AOF 格式 base)
3. 主进程继续处理请求:
   - 新写命令照常追加到 aof_buf 并刷入「增量(incr)AOF 文件」
   - 不再有单独的 aof_rewrite_buf(6.0 及之前的重写缓冲区已移除,
     彻底避免重写期间的内存双写与 CPU 额外开销)
4. 子进程完成 base 文件后通知主进程
5. 主进程创建新的 incr 文件,更新 manifest(清单)文件,
   原子地把「新 base + 新 incr」登记为当前 AOF 集合
6. 旧的 base / incr 文件由 Redis 异步清理

恢复时 Redis 按 manifest 指示,先加载 base,再重放 incr。

关键细节:

  • appenddirname "appendonlydir":多文件所在目录(默认 appendonlydir)。
  • appendonly.aof.manifest:清单文件,记录当前有效的 base 与 incr 文件, 是多文件结构的「目录」;修复时要以它为入口(见 6.2)。
  • 增量文件会随重写轮转(*.incr.aof 序号递增),单个文件更小、清理更渐进。
  • 6.x 及之前的旧流程(双缓冲区 + rename 原子替换单文件)仅在老版本中存在, 阅读旧资料时注意区分。

4.4 重写期间的数据一致性

时间线(7.0+):
  T1: fork() 开始,子进程写 base 文件
  T2: SET key1 val1  → 写入当前 incr AOF 文件(数据不丢)
  T3: SET key2 val2  → 同上
  T4: 子进程完成 base,通知主进程
  T5: 主进程切换到新 incr 文件并更新 manifest
  T6: 旧 incr / 旧 base 后台清理

5. AOF 文件格式(多文件结构)

Redis 7.0 起 AOF 由一个目录下的多个文件组成(multi-part AOF):

appendonlydir/
  appendonly.aof.manifest        # 清单:记录当前有效的 base 与 incr 文件
  appendonly.aof.1.base.rdb      # base:重写生成的全量数据(默认 RDB 格式)
  appendonly.aof.1.incr.aof      # incr:重写之后的增量写命令(RESP 文本)
  • aof-use-rdb-preamble yes(默认):base 文件用 RDB 二进制格式, 加载快、体积小;设为 no 则 base 也用 RESP 文本。
  • incr 文件与 6.x 的单文件 AOF 一样是 RESP 文本格式:
*3\r\n
$3\r\n
SET\r\n
$4\r\n
key1\r\n
$6\r\n
value1\r\n

5.1 事务包裹

写入 incr 的多条命令会被 MULTI/EXEC 包裹,保证加载时按事务原子重放:

*1\r\n
$5\r\n
MULTI\r\n
*3\r\n
$3\r\n
SET\r\n
...(若干命令)...
*1\r\n
$4\r\n
EXEC\r\n

6. AOF 文件恢复

6.1 自动恢复

Redis 启动时自动加载 AOF 文件(AOF 优先级高于 RDB):

  1. 检查 AOF 是否开启(appendonly yes)
  2. 加载 AOF 文件,逐条执行命令
  3. 如果 AOF 文件损坏,拒绝启动

6.2 AOF 文件修复

# 检查 AOF(7.0+ 传 manifest,工具会按清单校验 base+incr 全部文件)
redis-check-aof appendonlydir/appendonly.aof.manifest

# 修复(截断末尾损坏/不完整的命令)
redis-check-aof --fix appendonlydir/appendonly.aof.manifest

# 6.x 单文件 AOF 直接传 .aof 文件即可
redis-check-aof --fix appendonly.aof

6.3 AOF 与 RDB 加载优先级

Redis 启动时的加载顺序:

  1. 如果 AOF 开启(appendonly yes),优先加载 AOF
  2. 如果 AOF 未开启,加载 RDB 文件
  3. 如果两者都不存在,启动空数据库

7. 配置优化

7.1 关键配置项

# 开启 AOF
appendonly yes

# AOF 文件名
appendfilename "appendonly.aof"

# 刷盘策略
appendfsync everysec

# 自动重写条件
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

# 重写期间是否禁止 fsync
no-appendfsync-on-rewrite no

# 加载时忽略最后一条不完整命令
aof-load-truncated yes

# AOF 文件存储目录
dir /var/lib/redis

7.2 no-appendfsync-on-rewrite

当设为 yes 时,AOF 重写期间不执行 fsync(),减少磁盘 I/O 争用:

  • 优点:避免重写期间主线程因 fsync 阻塞
  • 风险:重写期间如果宕机,可能丢失更多数据
  • 建议:在磁盘 I/O 成为瓶颈时考虑开启

7.3 性能调优建议

场景appendfsyncrewrite 配置说明
数据安全优先always100/64mb最高安全,最低性能
均衡方案everysec100/64mb推荐默认配置
性能优先no200/128mb减少磁盘压力
大数据集everysec100/256mb减少重写频率

8. AOF 常见问题

8.1 AOF 文件过大

  • 检查重写是否正常触发:INFO Persistence
  • 手动触发重写:BGREWRITEAOF
  • 调整 auto-aof-rewrite-percentage 降低触发阈值

8.2 重写期间的内存与磁盘压力

  • 7.0+ 已移除重写缓冲区(双写)问题,但 fork 本身仍受写时复制影响: 重写期间主进程写入越频繁,页复制带来的内存放大越明显
  • 高写入场景下建议:低峰期手动 BGREWRITEAOF、调大重写触发阈值、 监控 INFO Persistence 中的 aof_rewrite_in_progress 与内存曲线

8.3 fsync 阻塞主线程

  • everysec 策略下,fsync 由后台线程执行
  • 如果后台线程 fsync 耗时超过 1 秒,主线程会等待
  • 解决方案:使用更快的磁盘(SSD)或开启 no-appendfsync-on-rewrite

工作流程

函数源码写法:命令追加到 AOF 缓冲区 void feedAppendOnlyFile(struct redisCommand *cmd, int argc, robj **argv)

// 将命令转换为 RESP 协议格式并追加到 aof_buf
void feedAppendOnlyFile(struct redisCommand *cmd, int argc, robj **argv) {
    buf = catAppendOnlyGenericCommand(argc, argv);
    aof_buf = sdscatlen(aof_buf, buf, sdslen(buf));
}

函数源码写法:写入文件并根据策略刷盘 void flushAppendOnlyFile(int force)

// 将 aof_buf 写入 AOF 文件并根据 appendfsync 策略决定是否 fsync
void flushAppendOnlyFile(int force) {
    nwritten = write(server.aof_fd, server.aof_buf, sdslen(server.aof_buf));
    if (server.aof_fsync == APPENDFSYNC_ALWAYS) {
        fsync(server.aof_fd);
    }
}

appendfsync 策略

基本写法:每条命令都 fsync appendfsync always

# 每条写命令执行后立即调用 fsync
appendfsync always

基本写法:每秒 fsync(默认推荐) appendfsync everysec

# 每秒调用一次 fsync
appendfsync everysec

基本写法:由操作系统决定刷盘 appendfsync no

# 不主动 fsync,由操作系统决定刷盘时机
appendfsync no

AOF 重写

基本写法:手动触发 AOF 重写 BGREWRITEAOF

# 手动触发 AOF 重写
BGREWRITEAOF

基本写法:配置重写增长百分比阈值 auto-aof-rewrite-percentage <percentage>

# AOF 文件大小比上次重写后增长 100% 时触发重写
auto-aof-rewrite-percentage 100

基本写法:配置重写最小文件大小 auto-aof-rewrite-min-size <size>

# AOF 文件最小 64MB 才触发重写
auto-aof-rewrite-min-size 64mb

基本写法:重写时合并列表操作 RPUSH <key> <values>

# 重写后将多次 RPUSH 和 LPOP 合并为一条命令
RPUSH list b c d e f

AOF 文件格式

基本写法:RESP 协议格式 *<count>\r\n$<len>\r\n<command>\r\n...

# AOF 文件使用 RESP 协议格式存储命令
*2
$6
SELECT
$1
0

基本写法:SET 命令的 RESP 格式 *3\r\n$3\r\nSET\r\n$<keylen>\r\n<key>\r\n$<vallen>\r\n<value>\r\n

# SET key1 value1 的 RESP 格式
*3
$3
SET
$4
key1
$6
value1

基本写法:MULTI/EXEC 合并格式(Redis 4.0+) *1\r\n$5\r\nMULTI\r\n...*1\r\n$4\r\nEXEC\r\n

# Redis 4.0+ 使用 MULTI/EXEC 包裹多条命令减少文件体积
*1
$5
MULTI
*3
$3
SET
$3
key
$5
value
*1
$4
EXEC

AOF 文件恢复

基本写法:开启 AOF 持久化 appendonly yes

# 开启 AOF,Redis 启动时自动加载 AOF 文件
appendonly yes

基本写法:检查 AOF 文件 redis-check-aof <file>

# 检查 AOF 文件完整性
redis-check-aof appendonly.aof

基本写法:修复 AOF 文件 redis-check-aof --fix <file>

# 修复 AOF 文件,截断损坏部分
redis-check-aof --fix appendonly.aof

配置优化

基本写法:开启 AOF appendonly yes

# 开启 AOF 持久化
appendonly yes

基本写法:设置 AOF 文件名 appendfilename <name>

# 设置 AOF 文件名
appendfilename "appendonly.aof"

基本写法:设置刷盘策略 appendfsync <strategy>

# 设置刷盘策略为每秒
appendfsync everysec

基本写法:设置自动重写增长百分比 auto-aof-rewrite-percentage <percentage>

# 设置自动重写增长百分比为 100
auto-aof-rewrite-percentage 100

基本写法:设置自动重写最小文件大小 auto-aof-rewrite-min-size <size>

# 设置自动重写最小文件大小为 64MB
auto-aof-rewrite-min-size 64mb

基本写法:重写期间禁止 fsync no-appendfsync-on-rewrite <yes|no>

# AOF 重写期间不执行 fsync
no-appendfsync-on-rewrite yes

基本写法:加载时忽略不完整命令 aof-load-truncated <yes|no>

# 加载时忽略最后一条不完整命令
aof-load-truncated yes

基本写法:设置 AOF 文件存储目录 dir <path>

# 设置 AOF 文件存储目录
dir /var/lib/redis