
AOFAppend Only File持久化机制通过记录所有写命令来保证数据安全但随之而来的问题是随着运行时间增长AOF 文件会不断膨胀。假设你反复对一个 key 执行INCR操作 1000 次AOF 文件中会记录 1000 条INCR命令但恢复时只需要一条SET key 1000即可。这就是 AOF 重写的价值所在。AOF 重写的核心目标就是将 AOF 文件重写为包含相同数据集的最小命令集从而减少文件体积加速数据恢复。一、问题背景AOF 文件的膨胀1.1 AOF 持久化回顾AOF 机制以日志形式记录每个写命令。当执行以下操作时127.0.0.1:6379 SET user:1000:name Alice OK 127.0.0.1:6379 SET user:1000:name Bob OK 127.0.0.1:6379 SET user:1000:name Charlie OKAOF 文件会记录三条命令。但实际上只有最后一条SET user:1000:name Charlie是恢复数据所需要的。1.2 膨胀带来的问题问题影响磁盘空间浪费大量冗余命令占用存储数据恢复缓慢启动时需回放所有历史命令主从复制压力传输大文件占用网络带宽二、核心设计思想AOF 重写与直接处理旧 AOF 文件不同它采用了一种巧妙的方式不是“重写旧文件”而是“基于当前内存数据生成新文件”。这种方式相当于定期对数据库做一个“快照”用一系列能够重建当前数据状态的命令替换旧的、冗长的命令序列。2.1 核心原则最小命令集原则只生成重建当前数据集所需的最少命令。后台执行原则通过子进程完成不阻塞主进程。原子替换原则新旧文件切换是原子的不会丢失数据。三、重写工作的三阶段3.1 准备阶段触发与状态检查在redis中无论是手动触发还是自动触发rewriteAppendOnlyFileBackground()函数是入口// 伪代码AOF重写入口 int rewriteAppendOnlyFileBackground() { // 1. 检查是否已有重写进程在运行 if (server.aof_rewrite_child_pid ! -1) { return C_ERR; // 已有重写进程忽略本次请求 } // 2. 检查是否正在执行 RDB 持久化 if (server.rdb_child_pid ! -1) { // 等待 RDB 完成后再执行 server.aof_rewrite_scheduled 1; return C_OK; } // 3. 执行 fork 创建子进程 pid_t child_pid fork(); if (child_pid 0) { // 子进程执行重写 rewriteAppendOnlyFile(); exit(0); } else { // 父进程记录子进程 PID初始化重写缓冲区 server.aof_rewrite_child_pid child_pid; server.aof_rewrite_buf sdsempty(); // 创建重写缓冲区 return C_OK; } }3.2 执行阶段子进程的核心工作这是重写的核心工作子进程遍历整个数据库生成新的 AOF 文件// 伪代码子进程重写 AOF 文件 void rewriteAppendOnlyFile() { // 1. 创建临时文件 FILE* fp createTempFile(temp-rewrite.aof); // 2. 写入 SELECT 命令选择数据库 // Redis 默认有 16 个数据库需要确保恢复时切换到正确的数据库 for (int db_id 0; db_id server.dbnum; db_id) { redisDb* db server.db[db_id]; if (dictSize(db-dict) 0) continue; // 空数据库跳过 // 写入 SELECT db_id 命令 writeSelectCommand(fp, db_id); // 3. 遍历当前数据库的所有键 dictIterator* di dictGetIterator(db-dict); dictEntry* de; while ((de dictNext(di)) ! NULL) { robj* key dictGetKey(de); robj* val dictGetVal(de); // 4. 根据键的类型生成对应的重建命令 if (val-type OBJ_STRING) { // 字符串SET key value writeSetCommand(fp, key, val); } else if (val-type OBJ_LIST) { // 列表RPUSH key value1 value2 ... writeListCommand(fp, key, val); } else if (val-type OBJ_SET) { // 集合SADD key member1 member2 ... writeSetMembersCommand(fp, key, val); } else if (val-type OBJ_ZSET) { // 有序集合ZADD key score1 member1 score2 member2 ... writeZsetCommand(fp, key, val); } else if (val-type OBJ_HASH) { // 哈希HMSET key field1 value1 field2 value2 ... writeHashCommand(fp, key, val); } } dictReleaseIterator(di); } // 5. 写入 EOF 标记和校验和可选 writeEofAndChecksum(fp); // 6. 关闭临时文件 fclose(fp); }子进程遍历的是fork()那一刻的内存快照利用了写时复制技术所以数据是一致的。生成的是RESP协议格式的命令与普通 AOF 文件格式完全兼容。3.3 收尾阶段父进程处理增量数据在子进程工作期间父进程继续处理客户端请求新的写命令被同时写入两个地方旧的 AOF 缓冲区保持旧文件持续更新AOF 重写缓冲区aof_rewrite_buf子进程完成工作后通知父进程进行最后的合并// 伪代码父进程处理重写完成信号 void handleRewriteCompletion(pid_t child_pid) { // 1. 等待子进程结束获取退出状态 int status; waitpid(child_pid, status, 0); // 2. 检查子进程是否成功 if (!WIFEXITED(status) || WEXITSTATUS(status) ! 0) { // 子进程失败清理资源 clearRewriteResources(); return; } // 3. 将重写缓冲区增量数据追加到临时文件 // 这是关键步骤保证新文件包含子进程工作期间的所有新写命令 FILE* temp_fp fopen(temp-rewrite.aof, a); fwrite(server.aof_rewrite_buf, len, 1, temp_fp); fclose(temp_fp); // 4. 重命名临时文件为目标 AOF 文件原子操作 // rename 在 Unix 中是原子的 if (rename(temp-rewrite.aof, appendonly.aof) ! 0) { // 重命名失败记录错误 logError(Failed to rename AOF file); return; } // 5. 清空重写缓冲区重置状态 sdsfree(server.aof_rewrite_buf); server.aof_rewrite_buf NULL; server.aof_rewrite_child_pid -1; // 6. 如果新的 AOF 文件大于旧文件可能需要调整自动重写阈值 updateAofRewriteThreshold(); }收尾阶段的时序图时间线 ────────────────────────────────────────────────────────────── 主进程 │ fork() │ 继续处理请求写入旧AOF 重写缓冲区 │ 信号处理 │ │ │ 子进程 │ │ 遍历内存生成新AOF │ 退出 │ │ │ │───────┤ 重写缓冲区积累增量命令 │ │ │ │ │ │ │ 追加增量 │ │ │ 原子替换四、关键机制深度解析4.1 写时复制Copy-on-Writefork()创建子进程时子进程与父进程共享同一份物理内存只有当某一方修改时才会复制内存页。这使得 AOF 重写几乎不消耗额外内存是高效的异步处理基础。4.2 重写缓冲区的必要性为什么需要单独的重写缓冲区而不能用现有的 AOF 缓冲区因为子进程生成的新 AOF 文件是基于fork()时刻的数据快照。如果在子进程运行期间父进程的新命令只写入旧 AOF 缓冲区而子进程生成的临时文件没有这些命令替换后就会导致增量数据丢失。4.3 为什么重写能减少文件体积场景旧 AOF 记录重写后记录键反复修改SET k 1→SET k 2→ ... →SET k 100SET k 100只保留最终值集合频繁增删SADD s a b→SREM s a→SADD s cSADD s b c只保留最终成员列表大量增删RPUSH l a→LPUSH l b→RPOP lRPUSH l b a只保留最终元素过期键键已过期但旧文件中仍有记录直接忽略不写入新文件键被删除SET k 1→DEL k直接忽略最终状态是不存在4.4 自动触发的条件Redis 配置了两个参数控制自动重写# redis.confauto-aof-rewrite-min-size 64mb # AOF 文件至少达到 64MBauto-aof-rewrite-percentage 100 # 比上次重写后增长 100%触发逻辑的伪代码// 伪代码检查是否需要触发自动重写 void checkAndTriggerAofRewrite() { // 获取当前 AOF 文件大小 long long current_size getAofCurrentSize(); long long base_size server.aof_rewrite_base_size; // 检查条件 if (current_size server.aof_rewrite_min_size current_size base_size * (1 server.aof_rewrite_percentage / 100.0)) { // 触发重写 rewriteAppendOnlyFileBackground(); server.aof_rewrite_base_size current_size; // 更新基准大小 } }4.5 重写对性能的影响方面影响缓解措施CPU子进程遍历数据库CPU 密集后台执行不阻塞主进程内存fork()时复制页表写时复制增加内存控制重写触发频率I/O写入新 AOF 文件可配置aof-rewrite-incremental-fsync降低磁盘压力主线程仅在信号处理时短暂阻塞毫秒级几乎无感知五、总结与最佳实践5.1 AOF 重写本质AOF 重写 后台子进程 × (遍历内存 生成命令) 重写缓冲区 × (记录增量) 原子替换5.2 核心工作清单步骤执行主体关键操作1. 触发重写主进程检查条件fork()2. 生成新 AOF子进程遍历数据库为每个键生成重建命令3. 缓存增量命令主进程新写命令存入aof_rewrite_buf4. 追加增量主进程重写缓冲区数据追加到临时文件5. 原子替换主进程rename()替换旧 AOF 文件AOF重写和生成RDB快照在执行机制上确实很相似都利用了Linux的“写时复制”Copy-On-Write, COW技术来创建一个子进程处理任务避免阻塞主进程。不过它们诞生的目的和最终的结果完全不同。你可以这样理解它们的关系它们就像同一位厨师Redis主进程使用的两种不同菜谱RDB快照是每隔一段时间拍一张厨房全貌的照片存起来。这张照片RDB文件记录的是某个瞬间所有食材数据的最终样子是全量备份。AOF重写则是当记录做菜步骤的本子AOF文件变得太厚时厨师会回顾本子上的所有步骤重新梳理成一份精简版的“最终操作指南”。它只记录让食材变成当前状态所需的最小必要步骤目的是给AOF文件“减肥”。对比维度RDB 快照AOF 重写核心目的全量备份生成一个压缩的二进制文件用于快速恢复数据和灾难备份。日志瘦身压缩AOF文件体积避免其无限膨胀同时不影响持久化的实时性。产出内容数据本身某个时间点的所有键值对按Redis的二进制格式存储。写命令集合能还原当前数据集的最小、最精简的命令集合。数据完整性可能丢失数据因为是定时备份两次快照间的数据在故障时可能会丢失。保证完整性重写期间的新命令会被额外记录重写完成后会追加到新文件中确保数据不丢。恢复速度快直接加载数据文件到内存不执行任何命令。慢需要逐条重新执行文件中的所有写命令来重建数据推荐一个零声教育学习教程个人觉得老师讲得不错分享给大家[LinuxNginxZeroMQMySQLRedisfastdfsMongoDBZK流媒体CDNP2PK8SDockerTCP/IP协程DPDK等技术内容点击立即学习:链接