如无特殊说明,本文涉及的源码均位于 aof.c 和 aof.h 中


AOF 简介

AOF 是 “Append Only File” 的缩写,它存储的是 Redis 服务器的顺序指令序列,并且只记录对内存进行修改的指令。假设 AOF 日志记录了自 Redis 实例创建以来所有的修改性指令序列,那么便可以通过对一个空的 Redis 实例顺序执行所有指令来恢复 Redis 当前实例的内存数据结构的状态,即“重放”。

Redis 包含两种与 AOF 相关的操作:

  • AOF 持久化:客户端向 Redis 服务器发送增、删、改指令时,这些指令会被传播到 AOF 缓冲区,并随后按照特定策略持久化至磁盘。
  • AOF 重写:由于 AOF 文件保存的是通过 RESP 协议封装的文本内容,会占用大量的磁盘空间,因此 Redis 会定期重写 AOF 日志,以达到文件瘦身的效果。

AOF 机制的常用配置如下:

  • appendonly:是否启用 AOF 机制,默认为 no
  • appendfilename:AOF 文件名,默认值为 appendonly.aof
  • appendfsync:日志刷盘策略:
    • everysec(默认值):表示每秒最多调用一次 fsync。每次将数据写入文件缓冲区后,将刷盘任务提交给 BIO 后台线程异步执行,提交任务的频率由 BIO 控制。
    • always:每次将数据写入文件缓冲区后(write 调用),立即执行 fsync 操作以实现同步。
    • no:只写入文件缓冲区,不手动刷盘,具体的落盘时机交给操作系统控制。
  • no-appendfsync-on-rewrite:默认值为 no,表示当有后台子进程生成 RDB 或者重写 AOF 时,仍允许 AOF 缓冲区数据同步到磁盘。
  • auto-aof-rewrite-percentage:指定百分比,当 AOF 文件大小超过原来的 x% 时进行重写,默认值为 100%。指定百分比为 0 时会禁用 AOF 自动重写。
  • auto-aof-rewrite-min-size:当 AOF 文件的大小超过设定值(默认 64MB)时进行重写。
  • aof-use-rdb-preamble:是否开启 AOF 混合持久化,默认值为 yes

类 Unix 系统的 I/O 与缓存

在正式介绍 AOF 生成机制之前,我们先来了解一些前置技术。

内核缓冲区

与内存操作相比,磁盘 I/O 的代价非常昂贵,因此类 Unix 系统的内核通常会定义内核缓冲区,只有当缓冲区已满时才将数据写入磁盘。

以 POSIX 标准定义的 I/O 函数 readwrite 为例,它们本身并不带有任何缓冲,而是直接使用内核提供的缓冲。当我们调用 write 函数写入文件时,数据会先被存入内核缓冲区。当缓冲区已满,或者手动调用 fsync 函数时,再将内核缓冲区中的数据同步到磁盘上。

fdatasync

POSIX 标准定义的 fsync 函数在文件元数据(metadata,例如 st_sizest_atimest_mtime 等)变脏时,会将所有元数据同步到磁盘。由于每次同步都必定导致时间戳的改变,而且文件内容和文件元数据通常存储在磁盘上的不同位置,因此每次调用 fsync 至少需要两次随机磁盘 I/O,其代价十分昂贵。

为此,Linux 平台提供了一个 fdatasync 函数。该函数仅在必要时才将元数据同步到磁盘(文件读写时间戳、文件大小等信息的改变不会实时落盘),大大降低了元数据同步的频率。

Redis 通过条件编译,将 Linux 平台的 redis_fsync 定义成了 fdatasync,而在其它类 Unix 平台上依旧是 fsync

#ifdef __linux__
#define redis_fsync fdatasync
#else
#define redis_fsync fsync
#endif

I/O 缓冲区

上面介绍的 POSIX 标准的 I/O 函数虽然可以利用内核缓冲来加速读写,但由于缓存数据时需要切换到内核态,因此开销比较大。

实际上,缓存操作最好在用户态进行。因此,C 语言标准库提供了位于用户态的 I/O 缓冲区,并定义了一系列基于该缓冲区的 I/O 函数,例如 fopenfwritefreadfflush 等。与 POSIX 标准函数相比,这些函数更适合处理大文件的读写。因此,在 Redis 中,AOF 重写过程是通过 C 标准库函数完成的。而对于数据量较小的 AOF 文件追加操作,则直接调用了 POSIX 标准函数。

C 标准库提供的 I/O 函数本质上是对 POSIX 标准函数的封装,因此当我们通过 fwrite 将数据写入 I/O 缓冲后:

  • 首先需要调用 fflush 将数据写入内核缓冲。由于 fflush 通常比内核缓冲区大,因此这个调用可能会触发多次落盘操作(顺序写,性能并不差)。
  • fflush 完毕后,内核缓冲区中往往还会有剩余数据。因此,还需要调用 POSIX 标准的 fsync 兜底。

AOF 持久化过程

AOF 持久化过程分为以下三个步骤:命令传播、刷新 AOF 缓冲区和同步磁盘。示意图如下:

AOF 持久化过程

命令传播至 AOF 缓冲

配置中启用 AOF 后,Redis 会将执行的每个写命令都传播到 AOF 缓冲区 server.aof_buf。该过程由 propagate() 函数触发(propagate 由 call 函数触发):

void propagate(struct redisCommand *cmd, int dbid, robj **argv, int argc,
               int flags) {
    ...
    if (server.aof_state != AOF_OFF && flags & PROPAGATE_AOF)
        feedAppendOnlyFile(cmd,dbid,argv,argc);
    ...
}

处理 AOF 的实际函数为 feedAppendOnlyFile()

void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
    sds buf = sdsempty();
    ...
    // [1] EXPIRE/PEXPIRE/EXPIREAT 转换为 PEXPIREAT 处理
    if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
        cmd->proc == expireatCommand) {
        /* Translate EXPIRE/PEXPIRE/EXPIREAT into PEXPIREAT */
        buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
    } else if (cmd->proc == setCommand && argc > 3) {
        // [2] 带 EX、PX 参数的 SET 命令,特殊处理
        ...
    } else {
        // [3] 按照 RESP 协议组装命令
        buf = catAppendOnlyGenericCommand(buf,argc,argv);
    }
    // [4] 将组装好的命令追加到 aof_buf
    if (server.aof_state == AOF_ON)
        server.aof_buf = sdscatlen(server.aof_buf,buf,sdslen(buf));
    // [5] 如果存在子进程在重写 AOF,则将命令追加到重写缓冲区
    if (server.child_type == CHILD_TYPE_AOF)
        aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));

    sdsfree(buf);
}
  • [1]:对于 EXPIREEXPIREATPEXPIRE 将其转换为 PEXPIREAT 特殊处理;
  • [2]:对于带 EXPX 参数的 SET 命令特殊处理,主要涉及过期时间的处理;
  • [3]:对于其它命令,调用 catAppendOnlyGenericCommand 按照 RESP 协议组装命令,并将其暂存至 buf;
  • [4]:如果启用 AOF 日志,则将 buf 中暂存的命令追加到 AOF 缓冲区 server.aof_buf
  • [5]如果存在正在重写 AOF 的子进程,则将命令追加到 AOF 重写缓冲区 server.aof_rewrite_buf_blocks。在允许发送增量数据的情况下,触发可写事件,将增量数据写入 pipe 管道。

刷新 AOF 缓冲区 & 同步磁盘

AOF 数据的落盘操作由 serverCron() 定时任务触发:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    ...
    // [1] 存在延迟的 AOF 落盘操作,则在这里完成
    if (server.aof_state == AOF_ON && server.aof_flush_postponed_start)
        flushAppendOnlyFile(0);
    run_with_period(1000) {
        // [2] 上次 AOF 落盘失败,这里需要再次落盘
        if (server.aof_state == AOF_ON && server.aof_last_write_status == C_ERR)
            flushAppendOnlyFile(0);
    }
    ...
}
  • [1]:如果 server.aof_flush_postponed_start 不为 0,表示存在延迟的 AOF 缓冲区落盘操作,在这里落盘;

  • [2]:如果 server.aof_last_write_status == C_ERR,表示上次 AOF 落盘失败,这里需要再次落盘。

    这里会通过 run_with_period 宏控制执行周期,宏的实现如下:

          #define run_with_period(_ms_) 
             if ((_ms_ <= 1000/server.hz) 
             || !(server.cronloops%((_ms_)/(1000/server.hz))))
          
    以默认情况为例,1000/server.hz 等于 100,_ms_ 参数值为 1000,那么最终 (_ms_)/(1000/server.hz) 的值等于 10。也就是随着 server.cronloops 的自增,每执行十次 serverCron(),该宏返回一次 true。

核心实现为 flushAppendOnlyFile()

void flushAppendOnlyFile(int force) {
    ssize_t nwritten;
    int sync_in_progress = 0;
    mstime_t latency;
    // [1] 如果 AOF 缓冲为空,则特殊处理
    if (sdslen(server.aof_buf) == 0) {
        if (server.aof_fsync == AOF_FSYNC_EVERYSEC &&
            server.aof_fsync_offset != server.aof_current_size &&
            server.unixtime > server.aof_last_fsync &&
            !(sync_in_progress = aofFsyncInProgress())) {
            goto try_fsync;
        } else {
            return;
        }
    }
    // [2] 判断是否有后台的 bio 线程
    if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
        sync_in_progress = aofFsyncInProgress();
    if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) {
        if (sync_in_progress) {
            if (server.aof_flush_postponed_start == 0) {
                server.aof_flush_postponed_start = server.unixtime;
                return;
            } else if (server.unixtime - server.aof_flush_postponed_start < 2) {
                return;
            }
            server.aof_delayed_fsync++;
        }
    }
    ...
    // [3] 刷新 aof_buf 缓冲区的数据到 AOF 文件
    nwritten = aofWrite(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));
    ...
    server.aof_flush_postponed_start = 0;
    ...
    // [4] 处理用完的 AOF 缓冲
    if ((sdslen(server.aof_buf)+sdsavail(server.aof_buf)) < 4000) {
        sdsclear(server.aof_buf);
    } else {
        sdsfree(server.aof_buf);
        server.aof_buf = sdsempty();
    }
  • [1]:如果 AOF 缓冲为空,并且 AOF 策略配置为每秒同步 (everysec),距离上次磁盘同步已经过去 1 秒,同时当前没有正在运行的 bio 后台任务,则跳转至 [7] 执行后台磁盘同步。否则,直接退出函数;
  • [2]:如果 AOF 配置的策略为每秒同步 (everysec),且后台存在正在同步磁盘的 bio 线程,则判断 aof_flush_postponed_start 是否为 0:
    • 如果这个值为 0,表示之前没有推迟过落盘任务,所以记录当前的时间戳到 aof_flush_postponed_start 并退出;
    • 如果这个值不为 0,表示之前已经推迟过了。所以判断当前距离 aof_flush_postponed_start 是否已经过去了 2s。如果是,则增加 server.aof_delayed_fsync 计数,强制后续的磁盘同步流程;
  • [3]:调用 aofWrite()(即 POSIX 标志的 write() 函数)将 aof_buf 中的数据写入 AOF 文件的内核缓冲;
  • [4]:如果 aof_buf 的总空间小于 4KB,则清空 buffer 内容并重用该缓冲,否则创建一个新的 aof_buf

处理完成 aof_buf 中的数据后,接下来就该处理磁盘同步过程了:

try_fsync:
    // [5] 判断是否有运行中的 bio 线程
    if (server.aof_no_fsync_on_rewrite && hasActiveChildProcess())
        return;
    // [6] 如果 AOF 落盘策略为 always,直接同步
    if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
        ...
        if (redis_fsync(server.aof_fd) == -1)
        ...
    } else if ((server.aof_fsync == AOF_FSYNC_EVERYSEC &&
                server.unixtime > server.aof_last_fsync)) {
        // [7] 后台同步 aof
        if (!sync_in_progress) {
            aof_background_fsync(server.aof_fd);
            server.aof_fsync_offset = server.aof_current_size;
        }
        server.aof_last_fsync = server.unixtime;
    }
}
  • [5]:如果用户配置了 no-appendfsync-on-rewrite = yes,且当前后台存在处理刷盘的 bio 线程,则直接返回;
  • [6]:如果 AOF 落盘策略为 always,直接调用 redis_fsync() 完成落盘;
  • [7]:如果不存在后台 bio 线程,且用户配置的 AOF 策略为每秒同步 (everysec),则调用 aof_background_fsync() 创建 bio 异步任务。

AOF 重写过程

当 AOF 文件过大时,恢复过程可能会变得异常缓慢。因此,Redis 提供了 AOF 重写机制,以对 AOF 日志进行压缩。从 4.0 版本开始,Redis 引入了 AOF 混合持久化功能,该功能在 AOF 重写时会先将字典数据以 RDB 形式写入文件,然后再追加重写期间生成的新字符串。

AOF 重写过程分为以下几个步骤:

  1. 主进程 fork 一个 AOF 重写进程;
  2. AOF 重写进程将内存中的快照数据保存到 AOF 临时文件中,这个过程会根据 aof-use-rdb-preamble 的配置决定序列化格式;
  3. 重写期间,主进程继续将新到达的写命令追加到原 AOF 文件,同时将这些指令拷贝到重写缓冲,然后通过 pipe 管道发送给 AOF 进程的差异缓冲
  4. 子进程处理完快照数据后,追加差异缓冲中的数据到临时 AOF 文件,然后禁止主进程继续发送新数据;
  5. 主进程再将重写缓冲中的剩余数据追加到临时 AOF 文件,然后用临时 AOF 文件原子替换旧的 AOF 文件,完成收尾工作。

示意图如下:

AOF 重写过程

接下来,让我们详细介绍这个过程。

定时任务触发 fork 子进程

AOF 的重写过程由 serverCron() 定时任务触发:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    ...
    // [1] 存在延迟的 AOF 重写操作,则在这里完成
    if (!hasActiveChildProcess() &&
        server.aof_rewrite_scheduled) {
        rewriteAppendOnlyFileBackground();
    }
    if (hasActiveChildProcess() || ldbPendingChildren()) {
        run_with_period(1000) receiveChildInfo();
        // 主进程收尾
        checkChildrenDone();
    } else {
        ...
        // [2] 条件满足时,后台重写 AOF
        if (server.aof_state == AOF_ON &&
            !hasActiveChildProcess() &&
            server.aof_rewrite_perc &&
            server.aof_current_size > server.aof_rewrite_min_size)
        {
            long long base = server.aof_rewrite_base_size ?
                server.aof_rewrite_base_size : 1;
            long long growth = (server.aof_current_size*100/base) - 100;
            if (growth >= server.aof_rewrite_perc) {
                ...
                rewriteAppendOnlyFileBackground();
            }
        }
    }...
}
  • [1]:如果 server.aof_rewrite_scheduled 不为 0,表示存在延迟的 AOF 重写操作,如果当前不存在运行的子进程,则执行 AOF 后台重写;
  • [2]:如果服务器启用了 AOF 功能,并且满足以下条件,将执行 AOF 后台重写:
    • 不存在运行中的子进程;
    • 当前 AOF 文件大小大于 server.aof_rewrite_min_size 配置;
    • 当前 AOF 文件大小,对比上次重写后 AOF 文件大小的比例,超过了 auto-aof-rewrite-percentage 配置的百分比;

核心实现为 rewriteAppendOnlyFileBackground()

int rewriteAppendOnlyFileBackground(void) {
    ...
    // [1] 创建父子进程通信用的 pipe 管道
    if (aofCreatePipes() != C_OK) return C_ERR;
    // [2] 创建 AOF 子进程
    if ((childpid = redisFork(CHILD_TYPE_AOF)) == 0) {
        char tmpfile[256];
        // [3] 子进程执行这里
        redisSetProcTitle("redis-aof-rewrite");
        // 将自己绑定到某个 cpu
        redisSetCpuAffinity(server.aof_rewrite_cpulist);
        snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
        // 重写子进程
        if (rewriteAppendOnlyFile(tmpfile) == C_OK) {
            sendChildCowInfo(CHILD_INFO_TYPE_AOF_COW_SIZE, "AOF rewrite");
            exitFromChild(0);
        } else {
            exitFromChild(1);
        }
    } else {
        // 主进程执行这里
        ...
        return C_OK;
    }
    return C_OK;
}
  • [1]:调用 aofCreatePipes() 创建父子进程之间的管道:

    server.aof_pipe_write_data_to_child //主进程向子进程发送数据
    server.aof_pipe_read_data_from_parent //子进程读取主进程发来的数据
    
    server.aof_pipe_write_ack_to_parent //子进程向主进程发送停止信号
    server.aof_pipe_read_ack_from_child //主进程读取子进程发来的停止信号
    
    server.aof_pipe_write_ack_to_child //主进程收到停止信号后,向子进程回复确认
    server.aof_pipe_read_ack_from_parent //子进程读取主进程发来的回复确认
        

    这 6 个管道端口两两一对,以单工模式工作,组成了一条数据管道两条控制管道

  • [2]:创建 AOF 子进程。

  • [3]:子进程调用 rewriteAppendOnlyFile() 完成 AOF 日志的重写操作。

子进程处理过程

子进程处理 AOF 文件的核心实现是 rewriteAppendOnlyFile() 函数:

int rewriteAppendOnlyFile(char *filename) {
    ...
    // [1] 打开一个 aof 临时文件
    snprintf(tmpfile,256,"temp-rewriteaof-%d.aof", (int) getpid());
    fp = fopen(tmpfile,"w");
    ...
    // [2] 初始化差异缓冲
    server.aof_child_diff = sdsempty();
    // 初始化 rio 抽象层
    rioInitWithFile(&aof,fp);
    // 设定 fsync 触发条件
    if (server.aof_rewrite_incremental_fsync)
        rioSetAutoSync(&aof,REDIS_AUTOSYNC_BYTES);

    startSaving(RDBFLAGS_AOF_PREAMBLE);
    if (server.aof_use_rdb_preamble) {
        // [3] 混合持久化
        if (rdbSaveRio(&aof,&error,RDBFLAGS_AOF_PREAMBLE,NULL) == C_ERR) {
            ...
        }
    } else {
        // [4] 普通持久化
        if (rewriteAppendOnlyFileRio(&aof) == C_ERR) goto werr;
    }
    // [5] 将 I/O 缓冲和内核缓冲中的剩余数据落盘
    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1) goto werr;
  • [1]:打开 aof 临时文件,命名规则为 temp-rewriteaof-%d.aof
  • [2]:初始化接收增量命令的差异缓冲 server.aof_child_diff
  • [3]:如果启用了混合持久化,则调用 rdbsave 将 RDB 数据写入 aof 临时文件;
  • [4]:否则调用 rewriteAppendOnlyFileRio() 进行普通的 aof 重写。

    其内部会遍历字典快照,删除无效数据后,将其封装为 RESP 数据写入临时文件。在遍历的过程中,还会周期性地从管道中拉取增量数据到 aof_child_diff

    虽然差异缓冲中的数据在字典快照处理完后才会使用,但是为了降低管道传输压力,Redis 选择在遍历期间就分批将数据拉过来。

  • [5]:将 I/O 缓冲和内核缓冲中的剩余数据同步到磁盘;
        // [6] 从管道中拉取剩余的增量数据,持续一段时间
        int nodata = 0;
        mstime_t start = mstime();
        while(mstime()-start < 1000 && nodata < 20) {
            if (aeWait(server.aof_pipe_read_data_from_parent, AE_READABLE, 1) <= 0)
            {
                nodata++;
                continue;
            }
            nodata = 0;
            aofReadDiffFromParent();
        }
    
        // [7] 通知主进程停止发送增量数据
        if (write(server.aof_pipe_write_ack_to_parent,"!",1) != 1) goto werr;
        if (anetNonBlock(NULL,server.aof_pipe_read_ack_from_parent) != ANET_OK)
            goto werr;
        // 等待主进程的 ACK,最多等 5s
        if (syncRead(server.aof_pipe_read_ack_from_parent,&byte,1,5000) != 1 ||
            byte != '!') goto werr;
        ...
        // [8] 收到 ACK 后,再从管道拉一次数据,防止服务器收到停止信号前,又发出数据
        aofReadDiffFromParent();
        ...
        // [9] 将差异缓冲数据写入 aof 文件
        while (bytes_to_write) {
            ...
            if (rioWrite(&aof,buf,chunk_size) == 0)
                goto werr;
            ...
        }
    
        // [10] 将 aof 文件缓冲中的数据,同步到磁盘
        if (fflush(fp)) goto werr;
        if (fsync(fileno(fp))) goto werr;
        if (fclose(fp)) { fp = NULL; goto werr; }
        fp = NULL;
    
        // [11] 重命名文件,确保写入成功
        if (rename(tmpfile,filename) == -1) {
            ...
            return C_ERR;
        }
        ...
    }
    
  • [6]:最多持续 1s,继续从管道中拉取增量数据。如果超时,或连续 20ms 没有收到增量数据,就停止拉取;
  • [7]:停止拉取后,通过控制管靠发送指令,让主进程停止向管道写入数据,然后等待主进程地 ACK;
  • [8]:主进程收到指令之前,仍可能继续向管道写入数据,所以这里需要再拉取一次,确保没有数据丢失;
  • [9]:将差异缓冲中的数据追加到 aof 文件;
  • [10]:再次将 aof 文件缓冲中的数据同步到磁盘;
  • [11]:修改临时文件名,确认文件写入成功。

    这里的重命名是将 temp-rewriteaof-%d.aof 修改为 temp-rewriteaof-bg-%d.aof,修改后的文件依旧是临时文件,重命名只是为了确认文件内容、元数据等信息落盘成功。

主进程收尾工作

当主进程收到子进程停止发送增量数据的请求后,会删除数据管道上的回调函数,从而停止发送增量数据。然后在下一次运行 serverCron() 定时任务时,调用 checkChildrenDone() 完成 AOF 收尾工作。其核心方法为 backgroundRewriteDoneHandler()

void backgroundRewriteDoneHandler(int exitcode, int bysignal) {
    if (!bysignal && exitcode == 0) {
        ...
        // [1] 打开子进程刚刚处理完的 aof 临时文件
        snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof",
            (int)server.child_pid);
        newfd = open(tmpfile,O_WRONLY|O_APPEND);
        ...
        // [2] 将停止发送增量数据期间积累的数据追加到文件
        if (aofRewriteBufferWrite(newfd) == -1) {
            ...
        }
        ...
        // [3] 替换旧的 aof 文件
        if (rename(tmpfile,server.aof_filename) == -1) {
            ...
        }
        ...
    }
cleanup:
    // [4] 删除管道,清空 aof_buf,和所有临时文件
    aofClosePipes();
    aofRewriteBufferReset();
    aofRemoveTempFile(server.child_pid);
    ...
}
  • [1]:打开子进程刚刚处理完的 aof 临时文件;
  • [2]:将停止发送增量数据期间,重写缓冲中积累的数据追加到临时文件;
  • [3]:用临时文件替换旧的 aof 文件;
  • [4]:删除管道,清空 aof_buf,和所有临时文件;

可见,为了重写过程的安全性(一旦重写失败,原 AOF 文件依然可用),重写期间的增量命令会被写入磁盘两次(主进程继续写到旧 AOF,子进程最终追加到新 AOF),会浪费一定的磁盘资源。

此外,在重写过程中,新到达的写命令会被全部存储到子进程的差异缓冲区中,这可能导致较高的内存占用。

AOF 文件加载过程

Redis 在 启动过程 中调用 loadDataFromDisk() 来完成 AOF 的加载,调用链路如下:loadDataFromDisk -> loadAppendOnlyFile。最终在 loadAppendOnlyFile() 函数中完成对 AOF 文件的加载:

int loadAppendOnlyFile(char *filename) {
    struct client *fakeClient;
    // [1] 打开 AOF 文件
    FILE *fp = fopen(filename,"r");
    ...
    // [2] 创建虚拟客户端
    fakeClient = createAOFClient();
    startLoadingFile(fp, filename, RDBFLAGS_AOF_PREAMBLE);
    // [3] 根据是否有 RDB 前导码,确定处理方式
    char sig[5]; /* "REDIS" */
    if (fread(sig,1,5,fp) != 5 || memcmp(sig,"REDIS",5) != 0) {
        /* No RDB preamble, seek back at 0 offset. */
        if (fseek(fp,0,SEEK_SET) == -1) goto readerr;
    } else {
        /* RDB preamble. Pass loading the RDB functions. */
        rio rdb;
        if (fseek(fp,0,SEEK_SET) == -1) goto readerr;
        rioInitWithFile(&rdb,fp);
        // 加载 RDB 内容
        if (rdbLoadRio(&rdb,RDBFLAGS_AOF_PREAMBLE,NULL) != C_OK) ...
    }
    // [4] 循环处理剩下的所有指令
    while(1) {
        ...
        // [5] 按照 RESP 协议读取命令的参数数量
        argc = atoi(buf+1);
        if (argc < 1) goto fmterr;
        argv = zmalloc(sizeof(robj*)*argc);
        fakeClient->argc = argc;
        fakeClient->argv = argv;
        // [6] 开始读取每一个参数
        for (j = 0; j < argc; j++) {
            ...
        }
        // [7] 根据第一个参数,查询命令表
        cmd = lookupCommand(argv[0]->ptr);
        ...
        // [8] 执行命令
        fakeClient->cmd = fakeClient->lastcmd = cmd;
        if (fakeClient->flags & CLIENT_MULTI &&
            fakeClient->cmd->proc != execCommand) {
            queueMultiCommand(fakeClient);
        } else {
            cmd->proc(fakeClient);
        }
        ...
    }
    ...
}
  • [1]:打开 AOF 文件;
  • [2]:创建一个虚拟客户端,用于执行 AOF 文件中的命令;
  • [3]:如果 AOF 文件以 REDIS 字符串开头,则调用 rdbLoadRio 加载 RDB 类型的数据。否则将文件指针归零;
  • [4]:正式开始处理 RESP 格式的字符串:
    • [5]:按照 RESP 协议读取命令的参数数量;
    • [6]:读取每一个参数;
    • [7]:根据第一个参数,去查询命令表中的指令类型;
    • [8]:调用 redisCommand.proc 函数执行命令。

至此,Redis AOF 生成机制的主要内容就介绍完了。