当前位置: 首页 > news >正文

redis中AOF 重写机制解析

AOF(Append 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" OK

AOF 文件会记录三条命令。但实际上,只有最后一条SET user:1000:name "Charlie"是恢复数据所需要的。

1.2 膨胀带来的问题

问题影响
磁盘空间浪费大量冗余命令占用存储
数据恢复缓慢启动时需回放所有历史命令
主从复制压力传输大文件占用网络带宽

二、核心设计思想

AOF 重写与直接处理旧 AOF 文件不同,它采用了一种巧妙的方式:不是“重写旧文件”,而是“基于当前内存数据生成新文件”这种方式相当于定期对数据库做一个“快照”,用一系列能够重建当前数据状态的命令替换旧的、冗长的命令序列。

2.1 核心原则

  1. 最小命令集原则:只生成重建当前数据集所需的最少命令。

  2. 后台执行原则:通过子进程完成,不阻塞主进程。

  3. 原子替换原则:新旧文件切换是原子的,不会丢失数据。

三、重写工作的三阶段

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 收尾阶段:父进程处理增量数据

在子进程工作期间,父进程继续处理客户端请求,新的写命令被同时写入两个地方:

  1. 旧的 AOF 缓冲区(保持旧文件持续更新)

  2. 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-Write)

fork()创建子进程时,子进程与父进程共享同一份物理内存,只有当某一方修改时才会复制内存页。这使得 AOF 重写几乎不消耗额外内存,是高效的异步处理基础。

4.2 重写缓冲区的必要性

为什么需要单独的重写缓冲区,而不能用现有的 AOF 缓冲区?

因为子进程生成的新 AOF 文件是基于fork()时刻的数据快照。如果在子进程运行期间,父进程的新命令只写入旧 AOF 缓冲区,而子进程生成的临时文件没有这些命令,替换后就会导致增量数据丢失。

4.3 为什么重写能减少文件体积?

场景旧 AOF 记录重写后记录
键反复修改SET k 1SET k 2→ ... →SET k 100SET k 100(只保留最终值)
集合频繁增删SADD s a bSREM s aSADD s cSADD s b c(只保留最终成员)
列表大量增删RPUSH l aLPUSH l bRPOP lRPUSH l b a(只保留最终元素)
过期键键已过期,但旧文件中仍有记录直接忽略,不写入新文件
键被删除SET k 1DEL k直接忽略,最终状态是"不存在"

4.4 自动触发的条件

Redis 配置了两个参数控制自动重写:

# redis.conf
auto-aof-rewrite-min-size 64mb # AOF 文件至少达到 64MB
auto-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_buf
4. 追加增量主进程重写缓冲区数据追加到临时文件
5. 原子替换主进程rename()替换旧 AOF 文件

AOF重写和生成RDB快照在执行机制上确实很相似,都利用了Linux的“写时复制”(Copy-On-Write, COW)技术来创建一个子进程处理任务,避免阻塞主进程。不过,它们诞生的目的和最终的结果完全不同。你可以这样理解它们的关系:

它们就像同一位厨师(Redis主进程)使用的两种不同菜谱:

  • RDB快照是每隔一段时间,拍一张厨房全貌的照片存起来。这张照片(RDB文件)记录的是某个瞬间所有食材(数据)的最终样子,是全量备份

  • AOF重写则是当记录做菜步骤的本子(AOF文件)变得太厚时,厨师会回顾本子上的所有步骤,重新梳理成一份精简版的“最终操作指南”。它只记录让食材变成当前状态所需的最小必要步骤,目的是给AOF文件“减肥”。

对比维度RDB 快照AOF 重写
核心目的全量备份:生成一个压缩的二进制文件,用于快速恢复数据和灾难备份。日志瘦身:压缩AOF文件体积,避免其无限膨胀,同时不影响持久化的实时性。
产出内容数据本身:某个时间点的所有键值对,按Redis的二进制格式存储。写命令集合:能还原当前数据集的最小、最精简的命令集合。
数据完整性可能丢失数据:因为是定时备份,两次快照间的数据在故障时可能会丢失。保证完整性:重写期间的新命令会被额外记录,重写完成后会追加到新文件中,确保数据不丢。
恢复速度:直接加载数据文件到内存,不执行任何命令。:需要逐条重新执行文件中的所有写命令来重建数据

推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习:链接

http://www.cnnetsun.cn/news/3927534.html

相关文章:

  • 从数据获取到量化分析:AKShare开源财经数据接口库技术解析
  • Java字符串验证器设计与实现:从规则定义到生产实践
  • 终极多视频同步播放指南:为什么GridPlayer能改变你的工作方式?
  • Spark性能优化:RDD宽窄依赖原理与数据倾斜实战
  • MySQL事务与MVCC核心原理及实战优化
  • JumpServer堡垒机核心功能与安全配置实战
  • PHP定时任务时间错乱问题排查与解决方案
  • 从美工到策略传播:海报设计的认知升级与实践
  • SQL多表查询:核心语法、优化技巧与实战应用
  • 企业官网网址错误收录问题分析与解决方案
  • 西安建设银行网站使用全解析与本地金融服务深度指南
  • 生成式AI在网络攻击中的滥用与防御策略
  • Unity URP渲染管线中的Gamma矫正原理与实践
  • 构建Agent系统存储层:从Store协议到Postgres词法检索的工程实践
  • CFD云仿真中的许可证管理技术演进与实践
  • VRM插件终极指南:5分钟在Blender中搞定虚拟角色创作 [特殊字符]
  • 2024年深度解析:为什么您的清远企业网站建设需要告别模板化选择定制化开发策略
  • 3步解锁Steam游戏清单管理:Onekey工具完全实战指南
  • 开源信息简报系统BriefingAutoFlow:从信息焦虑到工程化解决方案
  • 虚幻引擎分辨率设置:SetScreenResolution与控制台命令的底层差异与实战避坑指南
  • AI Agent如何实现电脑自动化操作:从原理到工程实践
  • 图片元数据管理神器:ExifToolGui图形化工具终极指南
  • MVI69-DFNT工业以太网模块:协议转换与工业通信实践
  • UE5 Lyra项目角色换装:动画蓝图接口与模块化动画系统实战
  • 计算机操作系统31,32,33(完结)
  • 揭秘中国建设银行内部网站:揭秘其功能与价值,探索中国建设银行内部网站如何赋能员工高效办公
  • 虚幻引擎C++开发入门:从环境搭建到创建可交互Actor
  • 技术视角测评:网传乘路资讯AI培训割韭菜?付费学员谈技术落地体验
  • Windows下MySQL 8.0安装配置与优化指南
  • GPUStack v2.1.0深度评测:生产级GPU资源池化与任务调度平台部署实战