Redis数据结构之AOF方式

机会只留给那些准备充分的人

Posted by irving.gx on April 17, 2020

在上一篇当中我们分析了redis持久化的其中一种方式,也就是RDB的方式,这种方式的实现和我们的直观想法非常类似,也就是内存当中有什么, 就把这些存在的数据经过编码以及压缩之后存储一份到硬盘当中,恢复的时候再从硬盘中读取。关于这种方式一个能够想象得到的问题就是如果redis当中的数据量很大 ,每次持久化的时候就要把这么多数据备份一份,那么对于资源的消耗是比较大的,那么能否有一种比较简单巧妙的方式呢,有的,也就是我们这篇将要说的AOF方式。

  AOF(Append Only File),从这种方式的命名上也可以看到这种方式采取的是一种追加的方法,基本思想就是把对于redis数据库操作的写命令存储一份到文件当中, 如果要恢复的话只需要再重新执行一次这些命令就可以了。因为对于数据库的操作命令会非常多,如果数据量比较大的话仅仅存储命令的文件也是相当庞大的,为了解 决这个问题,redis提供了一种AOF重写的机制,也就是每过一段时间会将文件中的命令进行整理,将那些冗余的命令删掉,比如存储一个键值对又删除了该键值对, 数据库当中这个数据现在是不存在的,但是旧的AOF文件存储了添加以及删除这两个命令,AOF重写就是用一个新的AOF文件来覆盖旧的文件,新的文件中这两条冗余 命令是不存在的,以这种方式来减少AOF文件占用的大小,防止文件无限膨胀。可以看到,AOF的重写机制是只关心数据库的当前状态,对于数据库的历史状态是不关 心的。

  AOF持久化主要分为以下步骤:命令追加,文件写入以及文件同步三个。AOF过程的一些变量都存储在src/server.h当中的struct redisServer当中, 具体代码如下,源代码中有每个变量的英文注释,对于变量的含义在原注释后面加入了中文注释

/* AOF persistence */
    int aof_state;                  /* AOF_(ON|OFF|WAIT_REWRITE) 打开/关闭/等待重写*/
    int aof_fsync;                  /* Kind of fsync() policy 数据同步策略*/
    char *aof_filename;             /* Name of the AOF file AOF文件名称*/
    int aof_no_fsync_on_rewrite;    /* Don't fsync if a rewrite is in prog. 如果重写在执行不进行同步*/
    int aof_rewrite_perc;           /* Rewrite AOF if % growth is > M and... */
    off_t aof_rewrite_min_size;     /* the AOF file is at least N bytes. AOF文件至少有N字节大小*/
    off_t aof_rewrite_base_size;    /* AOF size on latest startup or rewrite. 最近一次启动AOF或者重写时AOF文件大小*/
    off_t aof_current_size;         /* AOF current size. AOF当前大小*/
    int aof_rewrite_scheduled;      /* Rewrite once BGSAVE terminates. 一旦BGSAVE中止就进行重写*/
    pid_t aof_child_pid;            /* PID if rewriting process AOF重写子进程id*/
    list *aof_rewrite_buf_blocks;   /* Hold changes during an AOF rewrite. AOF重写缓存链表,每个节点是一个缓存块*/
    sds aof_buf;      /* AOF buffer, written before entering the event loop AOF缓冲区*/
    int aof_fd;       /* File descriptor of currently selected AOF file AOF文件的描述*/
    int aof_selected_db; /* Currently selected DB in AOF AOF当前选择的数据库*/
    time_t aof_flush_postponed_start; /* UNIX time of postponed AOF flush 推迟写入操作的时间*/
    time_t aof_last_fsync;            /* UNIX time of last fsync() 上一次数据同步时间*/
    time_t aof_rewrite_time_last;   /* Time used by last AOF rewrite run. 上一次AOF重写操作消耗的时间*/
    time_t aof_rewrite_time_start;  /* Current AOF rewrite start time. 当前AOF重写开始时间*/
    int aof_lastbgrewrite_status;   /* C_OK or C_ERR 执行bgrewrite的状态*/
    unsigned long aof_delayed_fsync;  /* delayed AOF fsync() counter AOF数据同步被推迟了多少次*/
    int aof_rewrite_incremental_fsync;/* fsync incrementally while aof rewriting? */
    int rdb_save_incremental_fsync;   /* fsync incrementally while rdb saving? */
    int aof_last_write_status;      /* C_OK or C_ERR AOF上一次写入成功还是失败的状态*/
    int aof_last_write_errno;       /* Valid if aof_last_write_status is ERR AOF上一次写入错误码*/
    int aof_load_truncated;         /* Don't stop on unexpected AOF EOF. */
    int aof_use_rdb_preamble;       /* Use RDB preamble on AOF rewrites. */

上一章讲的RDB方式的变量也声明在这个地方,感兴趣的可以去看一下。

从上面的变量声明我们可以看到AOF缓冲区aof_buf是一个sds变量,当向数据库进行写入操作的时候,比如RPUSH或者SET的时候会在aof_buf 当中将写入的命令进行追加。在default.conf文件当中有对于aof的配置,如下所示

appendonly no
appendfsync everysec

默认AOF方式是关闭的,如果开启的话同步数据默认采用的策略是everysec方式,一共有如下三种同步策略

appendfsync always表示每次收到写入命令将AOF缓冲区当中的所有内容同步到AOF文件当中,这种方式能够保证完全同步,但是速度会比较慢,效率比较低。

appendfsync everysec表示每一秒钟强制写入一次硬盘。

appendfsync no依赖操作系统对于写入的频率进行控制。

  我们之前说过为了防止AOF文件过大,采用了AOF重写的方式对于AOF文件进行精简。下面我们看下AOF功能的代码,通过注释以及代码逻辑我们可以看到, 主要的逻辑是用户调用BGREWRITEAOF,然后redis会生成一个子进程专门来进行rewrite的操作,将做的改动写入到缓存区当中,接下来子进程退出, 主进程会将缓存区当中的数据写入到文件当中,并且重新命名新的文件。

/* This is how rewriting of the append only file in background works:
 *
 * 1) The user calls BGREWRITEAOF
 * 2) Redis calls this function, that forks():
 *    2a) the child rewrite the append only file in a temp file.
 *    2b) the parent accumulates differences in server.aof_rewrite_buf.
 * 3) When the child finished '2a' exists.
 * 4) The parent will trap the exit code, if it's OK, will append the
 *    data accumulated into server.aof_rewrite_buf into the temp file, and
 *    finally will rename(2) the temp file in the actual file name.
 *    The the new file is reopened as the new append only file. Profit!
 */
int rewriteAppendOnlyFileBackground(void) {
    pid_t childpid;
    long long start;
    //如果已经有aof或者rdb进程在执行就停止
    if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
    //新建父子进程间通讯管道
    if (aofCreatePipes() != C_OK) return C_ERR;
    //打开子进程管道
    openChildInfoPipe();
    start = ustime();
    //生成子进程
    if ((childpid = fork()) == 0) {
        char tmpfile[256];
/* Child */
       //关闭子进程的socket监听
        closeListeningSockets(0);
        redisSetProcTitle("redis-aof-rewrite");
        //aof临时文件命名
        snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
        //aof文件重写
        if (rewriteAppendOnlyFile(tmpfile) == C_OK) {
            size_t private_dirty = zmalloc_get_private_dirty(-1);
if (private_dirty) {
                serverLog(LL_NOTICE,
                    "AOF rewrite: %zu MB of memory used by copy-on-write",
                    private_dirty/(1024*1024));
            }
server.child_info_data.cow_size = private_dirty;
            //子进程发送信号
            sendChildInfo(CHILD_INFO_TYPE_AOF);
            //退出子进程
            exitFromChild(0);
        } else {
            exitFromChild(1);
        }
    } else {
        /* Parent */
        //父进程
        //统计时间
        server.stat_fork_time = ustime()-start;
        server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
        latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
        //如果子进程创建失败
        if (childpid == -1) {
            //关闭子进程管道
            closeChildInfoPipe();
            serverLog(LL_WARNING,
                "Can't rewrite append only file in background: fork: %s",
                strerror(errno));
            //关闭之前打开的AOF通讯管道
            aofClosePipes();
            return C_ERR;
        }
        serverLog(LL_NOTICE,
            "Background append only file rewriting started by pid %d",childpid);
        server.aof_rewrite_scheduled = 0;
        server.aof_rewrite_time_start = time(NULL);
        server.aof_child_pid = childpid;
        //针对hash表关闭resize功能
        updateDictResizePolicy();
        /* We set appendseldb to -1 in order to force the next call to the
         * feedAppendOnlyFile() to issue a SELECT command, so the differences
         * accumulated by the parent into server.aof_rewrite_buf will start
         * with a SELECT statement and it will be safe to merge. */
        server.aof_selected_db = -1;
        //清空脚本缓存列表
        replicationScriptCacheFlush();
        return C_OK;
    }
    return C_OK; /* unreached */
}

  以上就是对AOF方式的一点解读。


如果对你有帮助,请作者喝一杯牛奶吧