Redis设计与实现 笔记 第十一章 AOF持久化

AOF 持久化

APPEND ONLY FILE
AOF持久化

相对于 RDB 持久化不同, RDB 发起初始化时,会一次性将当前数据库中的对象都进行一次持久化操作,即使有 BGSAVE 命令这样的保存方式,在进行保存时,还是会占用一定的资源.且可能大部分的数据也根本没有变化.

在面对上述描写的情况, AOF 持久化模式就出现了, 其记录了命令的输入,用增量的方式进行数据的持久化.

11.1 AOF 持久化的实现

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

11.1.1 命令追加

在 redisServer 结构中 有一个

//动态字符串 AOF缓冲区
sds aof_buf;

的属性,用来记录当前客户端发来的命令.
以追加的方式放置在 aof_buf 的末尾

11.1.2 AOF 文件的写入和同步

Redis 的服务器进程是一个大循环(任何服务器都是),在每一次循环的最后,通过 flushAppendOnlyFile() 函数来决定是否进行文件的写入和同步.

#define AOF_WRITE_LOG_ERROR_RATE 30 /* Seconds between errors logging. */
void flushAppendOnlyFile(int force) {
    ssize_t nwritten;
    int sync_in_progress = 0;

    // 缓冲区中没有任何内容,直接返回
    if (sdslen(server.aof_buf) == 0) return;

    // 策略为每秒 FSYNC 
    if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
        // 是否有 SYNC 正在后台进行?
        sync_in_progress = bioPendingJobsOfType(REDIS_BIO_AOF_FSYNC) != 0;

    // 每秒 fsync ,并且强制写入为假
    if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) {

        /* With this append fsync policy we do background fsyncing.
         *
         * 当 fsync 策略为每秒钟一次时, fsync 在后台执行。
         *
         * If the fsync is still in progress we can try to delay
         * the write for a couple of seconds. 
         *
         * 如果后台仍在执行 FSYNC ,那么我们可以延迟写操作一两秒
         * (如果强制执行 write 的话,服务器主线程将阻塞在 write 上面)
         */
        if (sync_in_progress) {

            // 有 fsync 正在后台进行 。。。

            if (server.aof_flush_postponed_start == 0) {
                /* No previous write postponinig, remember that we are
                 * postponing the flush and return. 
                 *
                 * 前面没有推迟过 write 操作,这里将推迟写操作的时间记录下来
                 * 然后就返回,不执行 write 或者 fsync
                 */
                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. 
                 *
                 * 如果之前已经因为 fsync 而推迟了 write 操作
                 * 但是推迟的时间不超过 2 秒,那么直接返回
                 * 不执行 write 或者 fsync
                 */
                return;

            }

            /* Otherwise fall trough, and go write since we can't wait
             * over two seconds. 
             *
             * 如果后台还有 fsync 在执行,并且 write 已经推迟 >= 2 秒
             * 那么执行写操作(write 将被阻塞)
             */
            server.aof_delayed_fsync++;
            redisLog(REDIS_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 you are following this code path, then we are going to write so
     * set reset the postponed flush sentinel to zero. 
     *
     * 执行到这里,程序会对 AOF 文件进行写入。
     *
     * 清零延迟 write 的时间记录
     */
    server.aof_flush_postponed_start = 0;

    /* We want to perform a single write. This should be guaranteed atomic
     * at least if the filesystem we are writing is a real physical one.
     *
     * 执行单个 write 操作,如果写入设备是物理的话,那么这个操作应该是原子的
     *
     * While this will save us against the server being killed I don't think
     * there is much to do about the whole server stopping for power problems
     * or alike 
     *
     * 当然,如果出现像电源中断这样的不可抗现象,那么 AOF 文件也是可能会出现问题的
     * 这时就要用 redis-check-aof 程序来进行修复。
     */
    nwritten = write(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));
    if (nwritten != (signed)sdslen(server.aof_buf)) {

        static time_t last_write_error_log = 0;
        int can_log = 0;

        /* Limit logging rate to 1 line per AOF_WRITE_LOG_ERROR_RATE seconds. */
        // 将日志的记录频率限制在每行 AOF_WRITE_LOG_ERROR_RATE 秒
        if ((server.unixtime - last_write_error_log) > AOF_WRITE_LOG_ERROR_RATE) {
            can_log = 1;
            last_write_error_log = server.unixtime;
        }

        /* Lof the AOF write error and record the error code. */
        // 如果写入出错,那么尝试将该情况写入到日志里面
        if (nwritten == -1) {
            if (can_log) {
                redisLog(REDIS_WARNING,"Error writing to the AOF file: %s",
                    strerror(errno));
                server.aof_last_write_errno = errno;
            }
        } else {
            if (can_log) {
                redisLog(REDIS_WARNING,"Short write while writing to "
                                       "the AOF file: (nwritten=%lld, "
                                       "expected=%lld)",
                                       (long long)nwritten,
                                       (long long)sdslen(server.aof_buf));
            }

            // 尝试移除新追加的不完整内容
            if (ftruncate(server.aof_fd, server.aof_current_size) == -1) {
                if (can_log) {
                    redisLog(REDIS_WARNING, "Could not remove short write "
                             "from the append-only file.  Redis may refuse "
                             "to load the AOF the next time it starts.  "
                             "ftruncate: %s", strerror(errno));
                }
            } else {
                /* If the ftrunacate() succeeded we can set nwritten to
                 * -1 since there is no longer partial data into the AOF. */
                nwritten = -1;
            }
            server.aof_last_write_errno = ENOSPC;
        }

        /* Handle the AOF write error. */
        // 处理写入 AOF 文件时出现的错误
        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 synched on disk. */
            redisLog(REDIS_WARNING,"Can't recover from AOF write error when the AOF fsync policy is 'always'. Exiting...");
            exit(1);
        } else {
            /* Recover from failed write leaving data into the buffer. However
             * set an error to stop accepting writes as long as the error
             * condition is not cleared. */
            server.aof_last_write_status = REDIS_ERR;

            /* Trim the sds buffer if there was a partial write, and there
             * was no way to undo it with ftruncate(2). */
            if (nwritten > 0) {
                server.aof_current_size += nwritten;
                sdsrange(server.aof_buf,nwritten,-1);
            }
            return; /* We'll try again on the next call... */
        }
    } else {
        /* Successful write(2). If AOF was in error state, restore the
         * OK state and log the event. */
        // 写入成功,更新最后写入状态
        if (server.aof_last_write_status == REDIS_ERR) {
            redisLog(REDIS_WARNING,
                "AOF write error looks solved, Redis can write again.");
            server.aof_last_write_status = REDIS_OK;
        }
    }

    // 更新写入后的 AOF 文件大小
    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). 
     *
     * 如果 AOF 缓存的大小足够小的话,那么重用这个缓存,
     * 否则的话,释放 AOF 缓存。
     */
    if ((sdslen(server.aof_buf)+sdsavail(server.aof_buf)) < 4000) {
        // 清空缓存中的内容,等待重用
        sdsclear(server.aof_buf);
    } else {
        // 释放缓存
        sdsfree(server.aof_buf);
        server.aof_buf = sdsempty();
    }

    /* Don't fsync if no-appendfsync-on-rewrite is set to yes and there are
     * children doing I/O in the background. 
     *
     * 如果 no-appendfsync-on-rewrite 选项为开启状态,
     * 并且有 BGSAVE 或者 BGREWRITEAOF 正在进行的话,
     * 那么不执行 fsync 
     */
    if (server.aof_no_fsync_on_rewrite &&
        (server.aof_child_pid != -1 || server.rdb_child_pid != -1))
            return;

    /* Perform the fsync if needed. */

    // 总是执行 fsnyc
    if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
        /* aof_fsync is defined as fdatasync() for Linux in order to avoid
         * flushing metadata. */
        aof_fsync(server.aof_fd); /* Let's try to get this data on the disk */

        // 更新最后一次执行 fsnyc 的时间
        server.aof_last_fsync = server.unixtime;

    // 策略为每秒 fsnyc ,并且距离上次 fsync 已经超过 1 秒
    } else if ((server.aof_fsync == AOF_FSYNC_EVERYSEC &&
                server.unixtime > server.aof_last_fsync)) {
        // 放到后台执行
        if (!sync_in_progress) aof_background_fsync(server.aof_fd);
        // 更新最后一次执行 fsync 的时间
        server.aof_last_fsync = server.unixtime;
    }

    // 其实上面无论执行 if 部分还是 else 部分都要更新 fsync 的时间
    // 可以将代码挪到下面来
    // server.aof_last_fsync = server.unixtime;
}

不同 appendfsync 值产生不同的持久化行为

appendfsync 选项的值flushAppendOnlyFile 函数的行为
always每次循环结束都将 aof_buf 缓冲区中的所有内容写入并同步到 AOF 文件
everysec如果距离上次保存时间超过1秒,那么将 aof_buf 缓冲区中的所有内容写入并同步到 AOF 文件
no数据库将不进行擅自保存,只有当客户端主动发起 BGWRITEAOF 命令时,才进行持久化操作

从这里可以看出,Redis 在出现抉择的时候,往往会给出3个选项,两个极端,一个折中,供我们进行选择. 这个思路可以在以后代码编写中应用.

简要分析一下三种模式
always 既然是每次都进行 AOF 持久化追加,那么首先会消耗一部分效率,但是换来了安全性上的保障,最多只会丢失当前这一个循环产生的命令

no 只有当客户端需求持久化时,才进行持久化的执行,极致的效率,但是安全性完全的丢失.

everysec 折中的方案,在一定程度上保证了 效率和安全性,发生意外也就丢失一秒内的命令.

11.2 AOF 文件的载入与数据还原

AOF 文件载入流程很简单粗暴, 并没有额外的代码进行 AOF 文件的载入,而直接说用一个虚假的客户端直接进行 AOF 文件的还原,相当于数据库开启后,将全部命令进行输入,且这套流程可以完美的替代单元测试.

11.3 AOF 重写

我们了解 AOF 的工作原理后,稍加思索就能想到以下问题

RPUSH msg "hello"
RPUSH msg "world"

这两条消息先后被放入了 AOF 文件,但是我们从结果上来看, 其实使用一条命令就可以复原

RPUSH msg "hello" "world"

类似的情况有很多,如果我们找出所有类似可以总结的操作,将大大节省时间以及减少 AOF 文件的体积

11.3.1 AOF 文件重写的实现

文件的重写过程由于会有多种不同的对象类型和编码方案,而所做的就是对当前的键值对进行一个总体的命令生成,就不基于当前已经生成的 AOF 文件了,而是针对当前数据库所存储所有键值对.

指的注意的是,列表,哈希表,集合,有序集合,这些结构的元素可能会超出我们缓冲区,造成缓冲区溢出的情况,所以在命令生成的时候有对应的限制,每条命令最多生成64个元素.

//元素上限
REDIS_AOF_REWRITE_ITEMS_PER_CMD
11.3.2 AOF 的后台重写

跟 RDB 模式一样,重写也可以这工作当然也可以放在后台进行,不用阻塞当前进程.

但是会有一个问题,子进程虽然拷贝了当前进程的所有数据,但是,在进行重写过程中,过来的命令需要怎么处理?

在没看下面内容之前,我想的是,不处理好像问题也不大,在开始重写后就开始将当前新的命令存入 aof_buf 中,完成之后再将 aof_buf 的内容追加到 AOF 文件中.

不过在看到这一小节有两页多时,就知道不会这么简单,

Redis 的处理为,在进行 AOF 后,新建一个重写新增队列,将新的命令加入该队列中,aof 缓存也正常接收命令,也就是在子进程进行 AOF 重写时, 一条新命令会被同时写入 aof_buf 和 重写 aof_buf 中,在子进程完毕后,将重写aof_buf的命令追加到 AOF 文件中,此时 后台 AOF 才算完成,当然在 重写aof_buf 进行追加时,主进程是处于阻塞状态的. 该种行为和我之前预想的更为强壮,可以应对宕机等情况,我的方案由于是直接覆盖,在宕机时,就无法进行恢复了.

总结

AOF 是通过记录命令来进行数据库的持久化行为.
AOF 的所有命令都是以 Redis 命令请求协议的格式保存.
AOF 命令会先被 aof_buf 进行缓存,后续再持久化到文件.
AOF appendfsync 选项的不同值对 AOF 持久化功能的安全性以及整体性能又有很大的影响.
AOF 只要重载所有的命令,就能恢复到AOF 存储的状态
AOF 可以重写一个文件,用来减少 AOF 文件体积,以及提高载入效率
AOF 重写,实际上是对当前数据库所有键值对的一个整合,并不是对现有的 AOF 文件进行处理
AOF 在进行 BGREWRITEAOF 命令期间,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程完成重写后,阻塞主进程,完成子进程重写时的命令,此时会有两个大小不一,命令不一,但是执行结果相同的 AOF 文件,此时用旧文件替换新文件,即完成了 AOF 后台重写

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值