一、核心概念
AOF(Append Only File) 是 Redis 的增量持久化机制,通过记录每一条写操作命令来保证数据持久化。
关键问题:
- 写命令如何被记录到 AOF 文件?
- 何时刷盘(fsync)到磁盘?
- 不同刷盘策略的性能和安全性差异?
二、AOF 写入完整流程
1. 整体流程图
客户端命令 → Redis服务器 → 四个阶段
│
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
① 命令传播 ② 追加到 ③ 写入OS缓冲区 ④ 刷盘到磁盘
到AOF AOF缓冲区 (write) (fsync)
aof_buf 内存中 page cache 磁盘文件
appendonly.aof
2. 四个阶段详解
阶段 1:命令传播
// Redis服务器处理写命令
void processCommand(client *c) {
// 执行命令(如 SET key value)
call(c, CMD_CALL_FULL);
// 如果AOF开启,传播命令到AOF
if (server.aof_state == AOF_ON) {
propagate(c->cmd, c->db->id, c->argv, c->argc, PROPAGATE_AOF);
}
}
传播的内容:
客户端命令:SET name redis
AOF协议格式:
*3 # 参数个数
$3 # 第一个参数长度
SET # 命令
$4 # 第二个参数长度
name # key
$5 # 第三个参数长度
redis # value
阶段 2:追加到 AOF 缓冲区
// 将命令追加到 AOF 缓冲区(内存中)
void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
sds buf = sdsnewlen(NULL, 128); // 创建缓冲区
// 将命令格式化为AOF协议格式
buf = catAppendOnlyGenericCommand(buf, argc, argv);
// 追加到全局AOF缓冲区(server.aof_buf)
server.aof_buf = sdscatlen(server.aof_buf, buf, sdslen(buf));
sdsfree(buf);
}
AOF 缓冲区(aof_buf):
- 位于 Redis 进程的内存中
- 累积多条命令后批量写入
- 每次事件循环结束前处理
阶段 3:写入操作系统缓冲区(write)
// 在事件循环结束前,将 aof_buf 写入操作系统缓冲区
void flushAppendOnlyFile(int force) {
ssize_t nwritten = write(server.aof_fd, server.aof_buf, sdslen(server.aof_buf));
// 清空AOF缓冲区
sdsclear(server.aof_buf);
// 根据刷盘策略决定是否调用fsync
if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
redis_fsync(server.aof_fd); // 立即刷盘
}
}
操作系统缓冲区(Page Cache):
write()系统调用只是写到内核的 Page Cache- 数据还在内存中,未真正落盘
- 操作系统会定期刷盘(通常 30 秒)
阶段 4:刷盘到磁盘(fsync)
// 根据配置的刷盘策略执行fsync
void redis_fsync(int fd) {
#ifdef __linux__
fdatasync(fd); // Linux使用fdatasync,性能更好
#else
fsync(fd); // 其他系统使用fsync
#endif
}
fsync 作用:
- 强制将操作系统缓冲区的数据刷入磁盘
- 调用后,数据才真正持久化
- 是一个同步阻塞操作
3. 事件循环中的 AOF 处理
// Redis主事件循环(简化版)
void aeMain(aeEventLoop *eventLoop) {
while (!eventLoop->stop) {
// 1. 处理客户端命令
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
// 2. 在每次事件循环结束前,处理AOF缓冲区
if (server.aof_state == AOF_ON) {
flushAppendOnlyFile(0); // 写入OS缓冲区
}
}
}
时机:
- 每个事件循环结束时
- 所有命令执行完毕后
- 批量写入,减少系统调用次数
三、三种刷盘策略详解
1. always(每次刷盘)
配置:
appendfsync always
流程:
客户端命令
↓
命令执行
↓
写入 aof_buf
↓
write() 到 OS 缓冲区
↓
立即 fsync() 刷盘 ← 每条命令都执行
↓
返回客户端
特点:
- ✅ 数据安全:每条写命令都立即刷盘,无数据丢失
- ❌ 性能极差:每次写都要等待磁盘 IO,性能下降 70% 以上
- ⚠️ 磁盘损耗:频繁刷盘加速磁盘老化
适用场景:
- 对数据安全要求极高(如金融核心系统)
- 写入量很小的场景
性能测试:
# 测试工具:redis-benchmark
redis-benchmark -t set -n 100000 -q
纯内存:100,000 requests/sec
everysec:80,000 requests/sec
always: 30,000 requests/sec # 性能下降70%
2. everysec(每秒刷盘)— 推荐
配置:
appendfsync everysec
流程:
主线程:
客户端命令 → 执行 → 写入aof_buf → write() → 返回客户端
↓
后台线程(每秒一次):
定时器触发 → fsync() → 刷盘
实现机制:
// Redis启动一个后台线程专门负责fsync
void *bioProcessBackgroundJobs(void *arg) {
while (1) {
// 从队列中取出fsync任务
job = bioJobDequeue(bio_type);
if (job->type == BIO_AOF_FSYNC) {
// 执行fsync(不阻塞主线程)
redis_fsync(job->fd);
}
}
}
// 主线程每秒投递一个fsync任务
void serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
// 检查是否需要fsync(距离上次超过1秒)
if (server.aof_last_fsync_time + 1 < server.unixtime) {
// 投递后台fsync任务
bioCreateBackgroundJob(BIO_AOF_FSYNC, server.aof_fd);
server.aof_last_fsync_time = server.unixtime;
}
}
特点:
- ✅ 性能与安全平衡:对主线程影响小(异步刷盘)
- ⚠️ 最多丢失 1 秒数据:如果 Redis 在两次 fsync 之间崩溃
- ✅ 生产环境推荐
数据丢失场景:
时间轴:
00:00.000 - 上次fsync完成
00:00.001 - 执行 SET key1 value1 (写入OS缓冲区)
00:00.500 - 执行 SET key2 value2 (写入OS缓冲区)
00:00.999 - 执行 SET key3 value3 (写入OS缓冲区)
00:01.000 - 后台线程准备fsync
00:01.000 - Redis进程崩溃 ← 此时OS缓冲区数据未刷盘
结果:丢失 key1、key2、key3(最多1秒数据)
3. no(操作系统决定)
配置:
appendfsync no
流程:
客户端命令 → 执行 → 写入aof_buf → write() → 返回客户端
↓
OS缓冲区(Page Cache)
↓
操作系统自动刷盘(通常30秒)
特点:
- ✅ 性能最好:不主动调用 fsync,写入速度快
- ❌ 数据安全最差:可能丢失几十秒数据(取决于操作系统)
- ⚠️ 不可预测:刷盘时机由操作系统控制
数据丢失场景:
时间轴:
00:00 - OS上次刷盘
00:15 - 发生大量写操作(积累在OS缓冲区)
00:30 - OS准备刷盘
00:29 - 机器断电 ← OS缓冲区数据全部丢失
结果:丢失 00:00 - 00:29 的近30秒数据
适用场景:
- 对性能要求极高
- 可容忍较多数据丢失
- 有其他数据保障机制(如主从复制)
四、三种策略对比
| 维度 | always | everysec | no |
|---|---|---|---|
| 数据安全 | 最高(0丢失) | 高(最多1秒) | 低(可能几十秒) |
| 性能影响 | 严重(-70%) | 较小(-20%) | 几乎无 |
| 主线程阻塞 | 是 | 否(后台线程) | 否 |
| fsync频率 | 每次写操作 | 每秒一次 | 由OS决定(~30秒) |
| 磁盘IO压力 | 非常高 | 中 | 低 |
| 适用场景 | 金融核心 | 生产环境(推荐) | 缓存场景 |
五、源码关键点
1. AOF 缓冲区管理
// Redis服务器结构体(server.h)
struct redisServer {
// AOF状态
int aof_state; // AOF开启/关闭
int aof_fd; // AOF文件描述符
// AOF缓冲区
sds aof_buf; // 待写入的AOF缓冲区
// 刷盘策略
int aof_fsync; // AOF_FSYNC_ALWAYS / EVERYSEC / NO
// 时间记录
time_t aof_last_fsync_time; // 上次fsync的时间
// 后台任务
list *aof_rewrite_buf_blocks; // AOF重写缓冲区
};
2. flushAppendOnlyFile 核心逻辑
void flushAppendOnlyFile(int force) {
ssize_t nwritten;
// 1. 如果缓冲区为空,直接返回
if (sdslen(server.aof_buf) == 0) return;
// 2. 根据刷盘策略决定行为
if (server.aof_fsync == AOF_FSYNC_EVERYSEC) {
// everysec:检查后台fsync是否在进行
if (!sync_in_progress) {
// 写入OS缓冲区
nwritten = aofWrite(server.aof_fd, server.aof_buf, sdslen(server.aof_buf));
// 投递后台fsync任务(如果距离上次超过1秒)
if (server.unixtime > server.aof_last_fsync_time + 1) {
bioCreateBackgroundJob(BIO_AOF_FSYNC, (void*)(long)server.aof_fd);
server.aof_last_fsync_time = server.unixtime;
}
}
} else if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
// always:同步写入并立即fsync
nwritten = aofWrite(server.aof_fd, server.aof_buf, sdslen(server.aof_buf));
redis_fsync(server.aof_fd); // 阻塞主线程
server.aof_last_fsync_time = server.unixtime;
} else {
// no:只写入OS缓冲区,不调用fsync
nwritten = aofWrite(server.aof_fd, server.aof_buf, sdslen(server.aof_buf));
}
// 3. 清空AOF缓冲区
sdsclear(server.aof_buf);
}
六、性能优化建议
1. 生产环境配置
# 推荐配置
appendonly yes
appendfsync everysec
no-appendfsync-on-rewrite yes # AOF重写时不fsync,避免磁盘IO竞争
# AOF重写配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# 混合持久化(Redis 4.0+)
aof-use-rdb-preamble yes
2. 高性能场景优化
场景 1:主从架构
# 主节点:关闭AOF,提升性能
appendonly no
# 从节点:开启AOF,保证数据安全
appendonly yes
appendfsync everysec
场景 2:高并发写入
# 使用 no 策略,但配合定期RDB
appendfsync no
save 300 10 # 5分钟内10个key变化时生成RDB
# 或配合主从复制
replicaof 192.168.1.100 6379
3. 避免的配置
❌ 避免:
# 同时使用always和频繁RDB
appendfsync always
save 60 1 # 每分钟RDB
# 原因:磁盘IO压力巨大,性能崩溃
七、常见问题与答案
Q1:everysec 为什么不会阻塞主线程?
答: Redis 使用 BIO(Background I/O)后台线程 执行 fsync:
// Redis启动时创建后台IO线程
void bioInit(void) {
pthread_t thread;
pthread_create(&thread, NULL, bioProcessBackgroundJobs, NULL);
}
// 主线程只负责投递任务,不等待完成
void flushAppendOnlyFile(int force) {
aofWrite(...); // 主线程执行write()
bioCreateBackgroundJob(BIO_AOF_FSYNC, ...); // 投递fsync任务
// 不等待,立即返回
}
Q2:如果后台 fsync 太慢会怎样?
答: Redis 会检测 fsync 延迟,如果超过 2 秒,会延迟写入:
if (sync_in_progress && server.aof_fsync_delay > 2) {
// 延迟写入,避免积累太多数据
return;
}
这可能导致短暂的写入延迟,但避免数据丢失。
Q3:为什么 Linux 使用 fdatasync 而不是 fsync?
答:
fsync():刷数据 + 元数据(如修改时间)fdatasync():只刷数据(性能更好)
Redis 只关心数据内容,不关心元数据,所以使用 fdatasync()。
八、面试答题模板
简洁版:
Redis AOF 写入流程分四步:命令传播到 aof_buf(内存缓冲区)→ write() 到 OS 缓冲区 → fsync() 刷盘到磁盘 → 返回客户端。刷盘策略有三种:always(每次刷盘,无丢失但慢)、everysec(每秒刷盘,最多丢1秒,推荐)、no(OS决定,可能丢几十秒)。生产环境推荐 everysec,由后台线程异步刷盘,不阻塞主线程。
详细版(回答结构):
- 写入流程四阶段:
- 命令执行后传播到 AOF 缓冲区(aof_buf,内存)
- 事件循环结束时,write() 写入操作系统缓冲区
- 根据刷盘策略调用 fsync() 刷盘
- 数据持久化到磁盘文件 appendonly.aof
- 三种刷盘策略:
- always:每次写操作都 fsync,无丢失但性能下降 70%
- everysec:每秒异步 fsync,最多丢1秒,性能影响 20%(推荐)
- no:由操作系统决定(通常30秒),性能最好但可能丢几十秒
- everysec 实现原理:
- 后台 BIO 线程专门负责 fsync,不阻塞主线程
- 主线程只负责 write(),立即返回处理下一个请求
- 如果 fsync 延迟超过 2 秒,会延迟 write() 避免数据积压
- 生产环境建议:
- 推荐
appendfsync everysec+ 混合持久化 - 主从架构中,主节点可关闭 AOF,从节点开启 AOF
- AOF 重写时设置
no-appendfsync-on-rewrite yes避免磁盘 IO 竞争
- 推荐