Redis AOF重写机制详解

 

Redis AOF 重写

+--------------------------------------------------------+
|                     Redis主进程                         |
|                                                        |
|  +---------------+   写命令   +----------------------+  |
|  |               |----------->|                      |  |
|  | 命令处理程序   |            | AOF缓冲区(aof_buf)   |  |
|  |               |            |                      |  |
|  +---------------+            +----------------------+  |
|         |                              |                |
|         | 写命令                        | fsync          |
|         v                              v                |
|  +---------------+             +----------------------+ |
|  |               |             |                      | |
|  | AOF重写缓冲区  |             | AOF文件              | |
|  | (aof_rewrite  |             |                      | |
|  |  _buf)        |             +----------------------+ |
|  +---------------+                                      |
|         |                                               |
|         | fork                                          |
|         v                                               |
|  +---------------+                                      |
|  | 发送信号给     |                                     |
|  | 父进程         |                                     |
|  +---------------+                                      |
|                                                        |
+--------------------------------------------------------+
                |
                | fork
                v
+--------------------------------------------------------+
|                   AOF重写子进程                         |
|                                                        |
|  +---------------+   读取    +----------------------+  |
|  |               |<----------|                      |  |
|  | AOF重写程序    |           | 内存数据库副本        |  |
|  | (rewrite      |           |                      |  |
|  |  AppendOnly   |           |                      |  |
|  |  File)        |---------->|                      |  |
|  |               |   写入    |                      |  |
|  +---------------+           +----------------------+  |
|         |                                              |
|         | 写入                                         |
|         v                                              |
|  +------------------+                                  |
|  |                  |                                  |
|  | 临时AOF文件       |                                  |
|  | (temp-rewriteaof)|                                  |
|  |                  |                                  |
|  +------------------+                                  |
|                                                        |
+--------------------------------------------------------+

完成后:
1. 子进程通知父进程
2. 父进程将AOF重写缓冲区内容追加到新AOF文件
3. 父进程原子性地重命名临时文件,替换旧AOF文件

AOF重写的必要性

AOF(Append Only File)持久化通过记录所有修改数据库的写命令来保存数据状态。随着Redis运行时间的增长,AOF文件会不断膨胀,导致以下问题:

  1. 磁盘空间占用过大:大量冗余命令会占用不必要的存储空间
  2. 恢复速度变慢:Redis重启时需要执行AOF文件中的所有命令,文件越大恢复越慢
  3. 潜在的性能影响:过大的AOF文件可能影响Redis整体性能

AOF重写的实现原理

尽管名为”重写”,Redis并不会读取、分析现有的AOF文件,而是通过以下步骤创建一个新的AOF文件:

  1. 读取当前数据库状态:直接从内存中的数据库读取所有键值对
  2. 生成恢复命令:为每个键值对生成能够恢复其当前状态的最少命令
  3. 优化命令结构:尽可能使用能处理多个元素的命令(如RPUSH、SADD、ZADD等)
  4. 原子性替换:通过rename()系统调用原子性地用新文件替换旧文件

这种实现方式确保了新AOF文件的紧凑性,且包含恢复数据库状态所需的最小命令集。

重写过程的核心代码

rewriteAppendOnlyFile函数是AOF重写的核心实现:

int rewriteAppendOnlyFile(char *filename) {
    // 创建临时文件
    snprintf(tmpfile, 256, "temp-rewriteaof-%d.aof", (int) getpid());
    fp = fopen(tmpfile, "w");
    
    // 遍历所有数据库
    for (j = 0; j < server.dbnum; j++) {
        redisDb *db = server.db+j;
        dict *d = db->dict;
        if (dictSize(d) == 0) continue;
        
        // 写入SELECT命令切换数据库
        if (rioWrite(&aof, selectcmd, sizeof(selectcmd)-1) == 0) goto werr;
        if (rioWriteBulkLongLong(&aof, j) == 0) goto werr;
        
        // 遍历数据库中的所有键
        while((de = dictNext(di)) != NULL) {
            // 获取键、值和过期时间
            keystr = dictGetKey(de);
            o = dictGetVal(de);
            expiretime = getExpire(db, &key);
            
            // 跳过已过期的键
            if (expiretime != -1 && expiretime < now) continue;
            
            // 根据值类型生成相应命令
            if (o->type == REDIS_STRING) {
                // 生成SET命令
                // ...
            } else if (o->type == REDIS_LIST) {
                // 生成LIST相关命令
                // ...
            }
            // 其他数据类型...
            
            // 保存过期时间
            if (expiretime != -1) {
                // 生成PEXPIREAT命令
                // ...
            }
        }
    }
    
    // 原子性替换旧文件
    if (rename(tmpfile, filename) == -1) {
        // 处理错误...
        return REDIS_ERR;
    }
    
    return REDIS_OK;
}

后台重写机制

为了避免阻塞主进程,Redis采用子进程进行AOF重写:

子进程重写:

  1. 不阻塞主服务:子进程进行重写时,父进程继续处理客户端请求
  2. 内存安全:子进程拥有父进程数据的副本,无需加锁即可安全访问
  3. 写时复制:由于Linux的写时复制(Copy-On-Write)机制,子进程创建时不会实际复制所有内存

数据一致性问题

子进程重写期间,父进程仍在处理写命令,可能导致新AOF文件与当前数据库状态不一致。

AOF重写缓冲区

Redis巧妙地解决了这个问题,具体流程如下:

  1. 创建重写缓冲区:Redis创建子进程开始AOF重写时,同时创建一个AOF重写缓冲区
  2. 双向写入:每次执行写命令后,父进程同时将命令写入:
    • 普通AOF缓冲区(用于常规AOF追加)
    • AOF重写缓冲区(用于后续合并到新AOF文件)
  3. 合并过程:当子进程完成重写后:
    • 向父进程发送信号
    • 父进程调用信号处理函数backgroundRewriteDoneHandler
    • 将AOF重写缓冲区中积累的命令追加到新AOF文件末尾
    • 原子性地重命名新AOF文件,替换旧文件

重写触发机制

Redis提供了两种触发AOF重写的方式:

  1. 手动触发:通过BGREWRITEAOF命令
  2. 自动触发:基于配置参数:
    • auto-aof-rewrite-percentage:当前AOF文件大小相对上次重写时的增长百分比
    • auto-aof-rewrite-min-size:触发重写所需的最小AOF文件大小

例如,设置auto-aof-rewrite-percentage 100auto-aof-rewrite-min-size 64mb,表示当AOF文件体积比上次重写后增长100%且超过64MB时,自动触发重写。

重写过程中的性能影响

尽管AOF重写设计为尽量减少对主服务的影响,但仍可能出现以下性能问题:

  1. fork时的阻塞:创建子进程时,父进程会短暂暂停
  2. 内存占用增加:子进程会复制父进程的页表,写时复制机制可能导致内存用量增加
  3. 磁盘I/O压力:重写过程涉及大量磁盘写入操作

最佳实践

  1. 合理配置自动重写参数:根据实际应用场景调整触发条件
  2. 选择合适的时间执行手动重写:在业务低峰期执行BGREWRITEAOF
  3. 监控重写过程:关注INFO命令输出中的AOF相关指标
  4. 考虑硬件配置:使用SSD存储和足够的内存以减轻重写压力
  5. 合理使用RDB和AOF:在某些场景下,可以考虑RDB+AOF混合使用

AOF重写是Redis持久化的重要组成部分,通过优化AOF文件大小,既保证了数据安全性,又提高了系统恢复速度,是Redis长期稳定运行的关键机制之一。