AOF 日志持久化
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秒数据 | 中 | |
| no | OS决定 | 数秒数据 | 高 |
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
自动触发判断逻辑:
且当前文件大小 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):
- 检查 AOF 是否开启(
appendonly yes) - 加载 AOF 文件,逐条执行命令
- 如果 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 启动时的加载顺序:
- 如果 AOF 开启(
appendonly yes),优先加载 AOF - 如果 AOF 未开启,加载 RDB 文件
- 如果两者都不存在,启动空数据库
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 性能调优建议
| 场景 | appendfsync | rewrite 配置 | 说明 |
|---|---|---|---|
| 数据安全优先 | always | 100/64mb | 最高安全,最低性能 |
| 均衡方案 | everysec | 100/64mb | 推荐默认配置 |
| 性能优先 | no | 200/128mb | 减少磁盘压力 |
| 大数据集 | everysec | 100/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