Redis 持久化机制 —— AOF 持久化

220 阅读4分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

Redis 持久化方式除了之前说过的 RDB 持久化以外,还提供了 AOF (Append Only File) 持久化功能。与 RDB 持久化方式通过保存数据库中的键值对来记录数据库状态不同, AOF 持久化是通过保存 Redis 服务器所执行的写命令来记录数据库状态。

AOF (Append Only File) 持久化功能

在 AOF 文件中,除了用于指定数据库的SELECT命令是服务器自动添加以外,其他的都是我们通过客户端发送的命令。

AOF 持久化的实现

AOF 持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤.

命令追加

当打开 AOF 持久化功能,服务器在执行完一个写命令之后,会将被执行的写命令追加到服务器的aof_buf缓存区的末尾。

struct redisServer {
    // ...
    
    sds aof_buf;      /* AOF buffer, written before entering the event loop */
    
    // ...
}

AOF 文件的写入和同步

Redis 的服务器进程就是一个事件循环

  • 文件事件负责接收客服端的命令请求,向客户端发送命令回复
  • 时间事件负责执行需要定时运行的函数

因为服务在执行写命令时,会将内容追加到 aof_buf 缓存区里面,所以在服务器每次结束循环前,会调用 flushAppendOnlyFile() ,考虑是否要将 aof_buf 缓存区中的数据写入和保存到 AOF 文件中。

void flushAppendOnlyFile(int force) {
   
    //...
    
    if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
        sync_in_progress = aofFsyncInProgress();
        
    if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) {
        /* With this append fsync policy we do background fsyncing.
         * If the fsync is still in progress we can try to delay
         * the write for a couple of seconds. */
        if (sync_in_progress) {
            if (server.aof_flush_postponed_start == 0) {
                /* No previous write postponing, remember that we are
                 * postponing the flush and return. */
                server.aof_flush_postponed_start = server.unixtime;
                return;
            } else if (server.unixtime - server.aof_flush_postponed_start < 2) {
                /* We were already waiting for fsync to finish, but for less
                 * than two seconds this is still ok. Postpone again. */
                return;
            }
            /* Otherwise fall trough, and go write since we can't wait
             * over two seconds. */
            server.aof_delayed_fsync++;
            serverLog(LL_NOTICE,"Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.");
        }
    }
    
    //...
    
    if (nwritten != (ssize_t)sdslen(server.aof_buf)) {
        /* Handle the AOF write error. */
        if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
            /* We can't recover when the fsync policy is ALWAYS since the
             * reply for the client is already in the output buffers, and we
             * have the contract with the user that on acknowledged write data
             * is synced on disk. */
            serverLog(LL_WARNING,"Can't recover from AOF write error when the AOF fsync policy is 'always'. Exiting...");
            exit(1);
        } else {
            // ...
        }
    } else {
        /* Successful write(2). If AOF was in error state, restore the
         * OK state and log the event. */
        if (server.aof_last_write_status == C_ERR) {
            serverLog(LL_WARNING,
                "AOF write error looks solved, Redis can write again.");
            server.aof_last_write_status = C_OK;
        }
    }
    
    server.aof_current_size += nwritten;

    /* Re-use AOF buffer when it is small enough. The maximum comes from the
     * arena size of 4k minus some overhead (but is otherwise arbitrary). */
    if ((sdslen(server.aof_buf)+sdsavail(server.aof_buf)) < 4000) {
        sdsclear(server.aof_buf);
    } else {
        sdsfree(server.aof_buf);
        server.aof_buf = sdsempty();
    }

appendfsync 选项的值flushAppendOnlyFile 函数的行为
always将 aof_buf 缓存区中的所有内容写入并同步到 AOF 文件
everysec将 aof_buf 缓存区中的所有内容写入到 AOF 文件,如果上次同步 AOF 文件距离现在超过一秒钟,那么再次对 AOF 进行同步。该判断逻辑有一个线程专门执行
no将 aof_buf 缓存区中的所有内容写入并同步到 AOF 文件,但并不对 AOF 文件进行同步,何时同步由操作系统决定

文件的写入和同步

为了提高文件的写入效率,在现在操作系统中,当用户调用 write 函数,将数据写入到文件的时候,操作系统会将数据暂时写入到内存缓冲区中,等到缓存区的空间被填满或者超过了指定的时限后,才真正的将缓冲区中的数据写入到磁盘中。

系统提供了 fsync 和 fdatasync 两个同步函数,可以强制让操作系统将缓冲区中的数据立即写入到磁盘中,从而保证数据的可靠性。

AOF 文件的载入与数据还原

Redis 读取 AOF 文件并还原数据库状态的详细步骤

  1. 创建一个不带网络连接的伪客户端
  2. 从 AOF 文件中分析并读取出一条写命令
  3. 使用伪客户端执行被读出来的写命令
  4. 循环执行步骤2和步骤3,直到 AOF 文件中的数据全部读取完毕
AOF (Append Only File) 持久化功能

AOF 重写

因为 AOF 持久化是通过保存被执行的写命令来记录数据库状态的,所以随着服务器运行时间的流逝,AOF 文件的内容会越来越多,文件体积会越来越大。为了解决 AOF 文件体积膨胀问题,Redis 提供了 AOF 文件重写(rewrite) 功能。

AOF 文件重写的实现

AOF 文件重写不会对现有的 AOF 文件进行任何读取、分析或者写入操作,而是通过读取服务器当前的数据库状态来实现的。

  • 从数据库中读取键现在的值
  • 用一条命令去记录键值对

为了避免在执行命令时造成客服端输入缓冲区溢出, 重写程序在处理会带有多个元素的键时,会先检查所包含的元素个数,如果超过 AOF_REWRITE_ITEMS_PER_CMD 64 时,改用多条命令进行记录。

AOF 后台重写

Redis 为了不造成服务器无法请求处理,决定将 AOF 重写程序放到子进程里执行。目的有两个:

  • 子进程进行 AOF 重写期间,服务器进程可以继续处理命令请求
  • 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性

但是会存在一个问题

  • 服务器继续处理命令请求,而新的命令可能会对现有数据库状态进行修改,从而使数据不一致

解决方案

为了解决数据不一致的问题,Redis 服务器设置了一个 AOF 重写缓冲区, 这个缓冲区在服务器创建子进程之后开始使用,当 Redis 服务器执行完一个写命令后,会同时发送到 AOF 缓冲区和 AOF 重写缓冲区。

  1. 执行客户端发来的指令
  2. 将执行后的写命令追加到 AOF 缓冲区中
  3. 将执行后的写命令追加到 AOF 重写缓冲区中

当子进程完成 AOF 重写工作之后,他会向父进程发送一个信号,父进程在接到信号后,会调用处理函数,执行以下步骤

  • 将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中,这时新 AOF 文件所保存的数据库状态将和服务器当前的数据库状态保持一致。
  • 对新 AOF 文件进行改名,覆盖现有的 AOF 文件。