一、核心概念

AOF(Append Only File) 是 Redis 的增量持久化机制,通过记录每一条写操作命令来保证数据持久化。

关键问题:

  1. 写命令如何被记录到 AOF 文件?
  2. 何时刷盘(fsync)到磁盘?
  3. 不同刷盘策略的性能和安全性差异?

二、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,由后台线程异步刷盘,不阻塞主线程。

详细版(回答结构):

  1. 写入流程四阶段
    • 命令执行后传播到 AOF 缓冲区(aof_buf,内存)
    • 事件循环结束时,write() 写入操作系统缓冲区
    • 根据刷盘策略调用 fsync() 刷盘
    • 数据持久化到磁盘文件 appendonly.aof
  2. 三种刷盘策略
    • always:每次写操作都 fsync,无丢失但性能下降 70%
    • everysec:每秒异步 fsync,最多丢1秒,性能影响 20%(推荐)
    • no:由操作系统决定(通常30秒),性能最好但可能丢几十秒
  3. everysec 实现原理
    • 后台 BIO 线程专门负责 fsync,不阻塞主线程
    • 主线程只负责 write(),立即返回处理下一个请求
    • 如果 fsync 延迟超过 2 秒,会延迟 write() 避免数据积压
  4. 生产环境建议
    • 推荐 appendfsync everysec + 混合持久化
    • 主从架构中,主节点可关闭 AOF,从节点开启 AOF
    • AOF 重写时设置 no-appendfsync-on-rewrite yes 避免磁盘 IO 竞争