Redis的AOF持久化深入解析

Redis提供两种持久化方式,RDB和AOF;与RDB不同,AOF可以完整的记录整个数据库,而不像RDB只是数据库某一时刻的快照; 

那么AOF模式为什么可以完整的记录整个数据库呢?  

原理 :在AOF模式下,Redis会把执行过的每一条更新命令记录下来,保存到AOF文件中;当Redis需要恢复数据库数据时,只需要从之前保存的AOF文件中依次读取命令,执行即可 eg. 

Shell代码   收藏代码
  1. 我们执行了以下命令:  
  2. redis 127.0.0.1:6379> set name diaocow  
  3. OK  
  4. redis 127.0.0.1:6379> lpush country china usa  
  5. (integer) 4  
  6.   
  7. 这时候在AOF文件中的类容类似下面:  
  8. *3\r\n$3\r\nset\r\n$4\r\nname\r\n$7\r\ndiaocow\r\n  
  9. *4\r\n$5\r\nlpush\r\n$7\r\ncountry\r\n$5\r\nchina\r\n$3\r\nusa\r\n  

看了上面的内容,我想不用我过多解释,你也能大致猜出AOF协议格式,因为它实在太简单明了了

协议格式为:

*<count>\r\n<element1>…<elementN>

每个参数element格式:$<length>\r\n<content>\r\n

其中,不包含<>字符,count表示参数个数,length表示参数的字节长度,content表示参数的内容

(1)对于一般的写入命令(SET、SADD、ZADD、RPUSH等)+PEXPIREAT命令

sds catAppendOnlyGenericCommand(sds dst, int argc, robj **argv) {
    char buf[32];
    int len, j;
    robj *o;

    // 重建命令的个数,格式为 *<count>\r\n
    // 例如 *3\r\n
    buf[0] = '*';
    len = 1+ll2string(buf+1,sizeof(buf)-1,argc);
    buf[len++] = '\r';
    buf[len++] = '\n';
    dst = sdscatlen(dst,buf,len);

    // 重建命令和命令参数,格式为 $<length>\r\n<content>\r\n
    // 例如 $3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
    for (j = 0; j < argc; j++) {
        o = getDecodedObject(argv[j]);

        // 组合 $<length>\r\n
        buf[0] = '$';
        len = 1+ll2string(buf+1,sizeof(buf)-1,sdslen(o->ptr));
        buf[len++] = '\r';
        buf[len++] = '\n';
        dst = sdscatlen(dst,buf,len);

        // 组合 <content>\r\n
        dst = sdscatlen(dst,o->ptr,sdslen(o->ptr));
        dst = sdscatlen(dst,"\r\n",2);

        decrRefCount(o);
    }

    // 返回重建后的协议内容
    return dst;
}

(2)设置键的过期时间命令:EXPIRE 、 PEXPIRE 和 EXPIREAT。

处理这三种命令,最终会转换为PEXPIREAT来执行。

sds catAppendOnlyExpireAtCommand(sds buf, struct redisCommand *cmd, robj *key, robj *seconds) {
    long long when;
    robj *argv[3];

    /* Make sure we can use strtol 
     *
     * 取出过期值
     */
    seconds = getDecodedObject(seconds);
    when = strtoll(seconds->ptr,NULL,10);

    /* Convert argument into milliseconds for EXPIRE, SETEX, EXPIREAT 
     *
     * 如果过期值的格式为秒,那么将它转换为毫秒
     */
    if (cmd->proc == expireCommand || cmd->proc == setexCommand ||
        cmd->proc == expireatCommand)
    {
        when *= 1000;
    }

    /* Convert into absolute time for EXPIRE, PEXPIRE, SETEX, PSETEX 
     *
     * 如果过期值的格式为相对值,那么将它转换为绝对值
     */
    if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
        cmd->proc == setexCommand || cmd->proc == psetexCommand)
    {
        when += mstime();
    }

    decrRefCount(seconds);

    // 构建 PEXPIREAT 命令
    argv[0] = createStringObject("PEXPIREAT",9);
    argv[1] = key;
    argv[2] = createStringObjectFromLongLong(when);

    // 追加到 AOF 缓存中
    buf = catAppendOnlyGenericCommand(buf, 3, argv);

    decrRefCount(argv[0]);
    decrRefCount(argv[2]);

    return buf;
}

可以看出上述函数中经过处理后,调用的依然是catAppendOnlyGenericCommand函数。

(3)对于SETEX和PSETEX命令

因为常见格式为:SETEXkey ttl value

所以对于命令参数来说,arg[1]为key,arg[2]为过期时间,argv[3]为value

在后面的feedAppendOnlyFile函数中会看到:

对于这两种命令,先翻译为SETkey value命令,接着翻译为PEXPIREAT key ttl_ms命令,依次执行上述两个逻辑函数:

// EXPIRE 、 PEXPIRE 和 EXPIREAT 命令
    if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
        cmd->proc == expireatCommand) {
        /* Translate EXPIRE/PEXPIRE/EXPIREAT into PEXPIREAT 
         *
         * 将 EXPIRE 、 PEXPIRE 和 EXPIREAT 都翻译成 PEXPIREAT
         */
        buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);

    // SETEX 和 PSETEX 命令
    } else if (cmd->proc == setexCommand || cmd->proc == psetexCommand) {
        /* Translate SETEX/PSETEX to SET and PEXPIREAT 
         *
         * 将两个命令都翻译成 SET 和 PEXPIREAT
         */

        // SET
        tmpargv[0] = createStringObject("SET",3);
        tmpargv[1] = argv[1];
        tmpargv[2] = argv[3];
        buf = catAppendOnlyGenericCommand(buf,3,tmpargv);

        // PEXPIREAT
        decrRefCount(tmpargv[0]);
        buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);

    // 其他命令
    } else {
        /* All the other commands don't need translation or need the
         * same translation already operated in the command vector
         * for the replication itself. */
        buf = catAppendOnlyGenericCommand(buf,argc,argv);
    }
Redis把更新命令记录到AOF文件,分为两个阶段:  

阶段1:把更新命令写入aof缓存 




在每次调用执行更新命令后,根据设置选项(开启AOF、重写子进程)来判断是否执行追加到AOF缓存区(aof_buf)和重写缓

存区(aof_rewrite_buf_blocks)。

这里提一下,AOF缓冲区aof_buf为sds格式,重写缓存区(aof_rewrite_buf_blocks)则是一个list链表集合,具体类型定义为

aofrwblock,这样做的原因是考虑到分配到一个非常大的空间并不总是可能的,也可能产生大量的复制工作,所以这里采用多个

大小为AOF_RW_BUF_BLOCK_SIZE(10M)的空间来保存命令协议格式。aofrwblock定义为:

typedef struct aofrwblock {
    
    // 缓存块已使用字节数和可用字节数
    unsigned long used, free;

    // 缓存块
    char buf[AOF_RW_BUF_BLOCK_SIZE];

} aofrwblock;

设计到该类型的API有:

void aofRewriteBufferReset(void):释放旧的链表,初始化新的链表,用于AOF重写缓存的初始化
unsigned long aofRewriteBufferSize(void):返回AOF重写缓存当前已使用的大小
void aofRewriteBufferAppend(unsigned char *s, unsigned long len):将字符数组s追加到AOF重写缓存的末尾,不够的话继续分配新的缓存块
ssize_t aofRewriteBufferWrite(int fd):将重写缓存中的所有内容(可能有多个块组成)写入到给定fd中,返回写入的字节数量,错误返回-1

具体的缓存区追加函数为feedAppendOnlyFile,下面给出简约的伪代码。

Python代码  

  1. def processCommand(cmd, argc, argv):  
  2.     # 执行命令  
  3.     call(cmd, argc, argv)  
  4.     # 该命令变更了键空间并且AOF模式打开  
  5.     if redisServer.update_key_space and redisServer.aof_state & REDIS_AOF_ON:  
  6.         feedAppendOnlyFile(cmd, argc, argv)   
  7.   
  8. def feedAppendOnlyFile(cmd, argc, argv):  
  9.     # 把命令转换成AOF协议格式  
  10.     aofCmdStr = getAofProtocolStr(cmd, argc, argv)  
  11.     redisServer.aof_buf.append(aofCmdStr )  
  12.   
  13.     # 存在一个子进程正在进行AOF_REWRITE(关于AOF_REWRITE,稍后详说)  
  14.     if redisServer.aof_child_pid != -1:  
  15.         redisServer.aof_rewrite_buf_blocks.append(aofCmdStr )  

阶段2: 把aof缓存写入文件

当我们开始下一次 事件循环 (eventLoop)之前,redis会把AOF缓存中的内容写入到文件: 
Python代码   收藏代码
  1. def flushAppendOnlyFile(force):    
  2.     if len(redisServer.aof_buf) == 0:  
  3.         return  
  4.     # 把缓存数据写入文件    
  5.     if writeByPolicy(force, redisServer.aof_fsync):     
  6.         write(redisServer.aof_fd, redisServer.aof_buf, len(redisServer.aof_buf))     
  7.     # 同步数据到硬盘    
  8.     if fsyncByPolicy(force, redisServer.aof_fsync):    
  9.         fsync(redisServer.aof_fd)   

更多细节请看: aof.c/flushAppendOnlyFile函数 (ps: 这个函数代码看起来比较晦涩) 

看到这里,你也许会有两个疑问:  
1. 为什么要调用fsync函数,不是已经调用write把数据写入到文件了吗? 
2. 伪代码中aof_fsync是什么,它有几种类型? 

首先回答问题1,为什么写入文件后,还要调用fsync函数: 
大多数unix系统为了减少磁盘IO,采用了“延迟写”技术,也就是说当我们执行完write调用后,数据并不一定立马被写入磁盘(可能还是保留在系统的buffer cache或者page cache中),这样当主机突然断电,这些我们本以为已经写入到磁盘文件的数据可能就会丢失;所以当我们需要确保数据被完整正确的写入磁盘(譬如数据库的持久化),则需要调用同步函数fsync,它会一直阻塞直到数据全部被写入到硬盘 

问题2,aof_fysnc是什么: 
aof_fsync用来指定flush策略,也就是调用fsync函数的策略,它一共有三种: 
a. AOF_FSYNC_NO :每次都会把aof_buf中的内容写入到磁盘,但是不会调用fsync函数; 
b. AOF_FSYNC_ALWAYS :每次都会把aof_buf中的内容写入到磁盘,同时调用fsync函数; (主进程负责)
c. AOF_FSYNC_EVERYSEC :每次都会把aof_buf中的内容写入到磁盘,如果距离上次同步超过一秒则调用fsync函数,由子线程负责。(默认值)

由于AOF_FSYNC_ALWAYS每次都写入文件都会调用fsync,所以这种flush策略可以保证数据的完整性,缺点就是性能太差(因为fysnc是个同步调用,会阻塞主进程对客户端请求的处理),而AOF_FSYNC_NO由于依赖于操作系统自动sync,因此不能保证数据的完整性; 

那有没有一种折中的方式:既能不过分降低系统的性能,又能最大程度上的保证数据的完整性,答案就是:AOF_FSYNC_EVERYSEC,AOF_FSYNC_EVERYSEC的flush策略是:定期(至少1s)去调用fsync,并且该操作是放到一个异步队列中(线程)去执行,因此不会阻塞主进程 


AOF 后台执行的方式和 RDB 有类似的地方,fork 一个子进程,主进程仍进行服务,子进程执行 AOF 持久化,数据被 dump 到磁盘上。与 RDB 不同的是,后台子进程持久化过程中,主进程会记录期间的所有数据变更(主进程还在服务),并存储在 server.aof_rewrite_buf_blocks 中;后台子进程结束后,redis 更新缓存追加到 AOF 文件中,是 RDB 持久化所不具备的.


AOF模式至此我们已经基本说完,但是随着Redis运行,AOF文件会变得越来越大(在业务高分期增长的更快),原因有两个 : 
a. AOF协议本身是文本协议,比较占空间; 
b. Redis需要记录从开始到现在的所有更新命令; 

这两个原因导致了AOF文件容易变得很大,那有什么方式可以优化吗?譬如用户执行了三个命令:lpush name diaocow; lpush name jack; lpush name jobs 
AOF文件会记录以下数据: 
Aof_file代码   收藏代码
  1. *3\r\n$5\r\nlpush\r\n$4\r\nname\r\n$7\r\ndiaocow  
  2. *3\r\n$5\r\nlpush\r\n$4\r\nname\r\n$4\r\njack  
  3. *3\r\n$5\r\nlpush\r\n$4\r\nname\r\n$4\r\njobs  

但其实只需要记录一条:lpush name diaocow jack jbos 命令即可: 
Aof_file代码   收藏代码
  1. *5\r\n$5\r\nlpush\r\n$4\r\nname\r\n$7\r\ndiaocow\r\n$4\r\njack\r\n$4\r\njobs  

所以当AOF文件达到 REDIS_AOF_REWRITE_MIN_SIZE(1M)时,Redis就会执行AOF_REWRITE来优化AOF文件; 

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------  

AOF_REWRITE触发条件  
1. 被动: 当AOF文件尺寸超过REDIS_AOF_REWRITE_MIN_SIZE & 达到一定增长比; 
2. 主动: 调用BGREWRITEAOF命令; 

主动和被动方式的AOF_REWRITE过程基本相同,唯一的区别就是,通过BGREWRITEAOF命令执行的AOF_REWRITE(主动)是在一个子进程中进行,因此它不会阻塞主进程对客户端请求的处理,而被动方式由于是在主进程中进行,所以在AOF_REWRITE过程中redis是无法响应客户端请求的; 

下面我就以BGREWRITEAOF命令为例,具体看下AOF_REWRITE过程:


上述执行命令过程的代码为:

void bgrewriteaofCommand(redisClient *c) {

    // 不能重复运行 BGREWRITEAOF
    if (server.aof_child_pid != -1) {
        addReplyError(c,"Background append only file rewriting already in progress");

    // 如果正在执行 BGSAVE ,那么预定 BGREWRITEAOF
    // 等 BGSAVE 完成之后, BGREWRITEAOF 就会开始执行
    } else if (server.rdb_child_pid != -1) {
        server.aof_rewrite_scheduled = 1;
        addReplyStatus(c,"Background append only file rewriting scheduled");

    // 执行 BGREWRITEAOF
    } else if (rewriteAppendOnlyFileBackground() == REDIS_OK) {
        addReplyStatus(c,"Background append only file rewriting started");

    } else {
        addReply(c,shared.err);
    }
}

执行rewriteAppendOnlyFileBackground的代码为:

/* This is how rewriting of the append only file in background works:
 * 
 * 以下是后台重写 AOF 文件(BGREWRITEAOF)的工作步骤:
 *
 * 1) The user calls BGREWRITEAOF
 *    用户调用 BGREWRITEAOF
 *
 * 2) Redis calls this function, that forks():
 *    Redis 调用这个函数,它执行 fork() :
 *
 *    2a) the child rewrite the append only file in a temp file.
 *        子进程在临时文件中对 AOF 文件进行重写
 *
 *    2b) the parent accumulates differences in server.aof_rewrite_buf.
 *        父进程将新输入的写命令追加到 server.aof_rewrite_buf 中
 *
 * 3) When the child finished '2a' exists.
 *    当步骤 2a 执行完之后,子进程结束
 *
 * 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!
 *
 *    父进程会捕捉子进程的退出信号,
 *    如果子进程的退出状态是 OK 的话,
 *    那么父进程将新输入命令的缓存追加到临时文件,
 *    然后使用 rename(2) 对临时文件改名,用它代替旧的 AOF 文件,
 *    至此,后台 AOF 重写完成。
 */
int rewriteAppendOnlyFileBackground(void) {
    pid_t childpid;
    long long start;

    // 已经有进程在进行 AOF 重写了
    if (server.aof_child_pid != -1) return REDIS_ERR;

    // 记录 fork 开始前的时间,计算 fork 耗时用
    start = ustime();

	//fork函数执行时,创建一个子进程,复制一份副本给子进程,若fork成功执行,子进程返回0,父进程返回子进程的ID,失败则返回-1
	//fork只调用一次,有两个返回值,父子进程将同时拥有以下代码,执行。
	//子进程
    if ((childpid = fork()) == 0) {
        char tmpfile[256];

        /* Child */

        // 关闭网络连接 fd
        closeListeningSockets(0);

        // 为进程设置名字,方便记认
        redisSetProcTitle("redis-aof-rewrite");

        // 创建临时文件,临时文件中的名字是由获得进程的ID来设置,防止临时文件重名,并进行 AOF 重写
        snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
        if (rewriteAppendOnlyFile(tmpfile) == REDIS_OK) {
            size_t private_dirty = zmalloc_get_private_dirty();

            if (private_dirty) {
                redisLog(REDIS_NOTICE,
                    "AOF rewrite: %zu MB of memory used by copy-on-write",
                    private_dirty/(1024*1024));
            }
            // 发送重写成功信号
            exitFromChild(0);
        } else {
            // 发送重写失败信号
            exitFromChild(1);
        }
    } else {
        /* Parent父进程 */
        // 记录执行 fork 所消耗的时间
        server.stat_fork_time = ustime()-start;

        if (childpid == -1) {
            redisLog(REDIS_WARNING,
                "Can't rewrite append only file in background: fork: %s",
                strerror(errno));
            return REDIS_ERR;
        }

        redisLog(REDIS_NOTICE,
            "Background append only file rewriting started by pid %d",childpid);

        // 记录 AOF 重写的信息
        server.aof_rewrite_scheduled = 0;
        server.aof_rewrite_time_start = time(NULL);
        server.aof_child_pid = childpid;

        // 关闭字典自动 rehash
        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. 
         *
         * 将 aof_selected_db 设为 -1 ,
         * 强制让 feedAppendOnlyFile() 下次执行时引发一个 SELECT 命令,
         * 从而确保之后新添加的命令会设置到正确的数据库中
         */
        server.aof_selected_db = -1;
        replicationScriptCacheFlush();
        return REDIS_OK;
    }
    return REDIS_OK; /* unreached */
}

整个AOF_WRITE过程,最重要的一个函数是: rewriteAppendOnlyFile,它主要做了下面事情: 

a. 创建一个临时文件temp-rewriteaof-pid.aof; 
b. 循环所有数据库,把每一个数据库中的键值对(过期键不写入),按照aof协议写入到临时文件; 
c. 重命名临时文件; 

注意的是,为了避免在执行命令时造成客户端输入缓冲区溢出,重写程序在处理列表、哈希表、集合、有序集合这四种可能会带有多个元素的键时,会先检查键所包含的元素数量,当超过了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那么重写程序将使用多条命令来记录键的值,不单单使用一个命令,每条命令所恢复的元素个数最多不能超过上述常量值。

int rewriteAppendOnlyFile(char *filename) {
    dictIterator *di = NULL;
    dictEntry *de;
    rio aof;
    FILE *fp;
    char tmpfile[256];
    int j;
    long long now = mstime();

    /* Note that we have to use a different temp name here compared to the
     * one used by rewriteAppendOnlyFileBackground() function. 
     *
     * 创建临时文件
     *
     * 注意这里创建的文件名和 rewriteAppendOnlyFileBackground() 创建的文件名稍有不同
	 * 后者为snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
     */
    snprintf(tmpfile,256,"temp-rewriteaof-%d.aof", (int) getpid());
    fp = fopen(tmpfile,"w");
    if (!fp) {
        redisLog(REDIS_WARNING, "Opening the temp file for AOF rewrite in rewriteAppendOnlyFile(): %s", strerror(errno));
        return REDIS_ERR;
    }

    // 初始化文件 io
    rioInitWithFile(&aof,fp);

    // 设置每写入 REDIS_AOF_AUTOSYNC_BYTES 字节
    // 就执行一次 FSYNC 
    // 防止缓存中积累太多命令内容,造成 I/O 阻塞时间过长
    if (server.aof_rewrite_incremental_fsync)
        rioSetAutoSync(&aof,REDIS_AOF_AUTOSYNC_BYTES);

    // 遍历所有数据库
    for (j = 0; j < server.dbnum; j++) {

        char selectcmd[] = "*2\r\n$6\r\nSELECT\r\n";

        redisDb *db = server.db+j;

        // 指向键空间
        dict *d = db->dict;
        if (dictSize(d) == 0) continue;

        // 创建键空间迭代器
        di = dictGetSafeIterator(d);
        if (!di) {
            fclose(fp);
            return REDIS_ERR;
        }

        /* SELECT the new DB 
         *
         * 首先写入 SELECT 命令,确保之后的数据会被插入到正确的数据库上
         */
        if (rioWrite(&aof,selectcmd,sizeof(selectcmd)-1) == 0) goto werr;
        if (rioWriteBulkLongLong(&aof,j) == 0) goto werr;

        /* Iterate this DB writing every entry 
         *
         * 遍历数据库所有键,并通过命令将它们的当前状态(值)记录到新 AOF 文件中
         */
        while((de = dictNext(di)) != NULL) {
            sds keystr;
            robj key, *o;
            long long expiretime;

            // 取出键
            keystr = dictGetKey(de);

            // 取出值
            o = dictGetVal(de);
            initStaticStringObject(key,keystr);

            // 取出过期时间
            expiretime = getExpire(db,&key);

            /* If this key is already expired skip it 
             *
             * 如果键已经过期,那么跳过它,不保存
             */
            if (expiretime != -1 && expiretime < now) continue;

            /* Save the key and associated value 
             *
             * 根据值的类型,选择适当的命令来保存值
             */
            if (o->type == REDIS_STRING) {
                /* Emit a SET command */
                char cmd[]="*3\r\n$3\r\nSET\r\n";
                if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
                /* Key and value */
                if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
                if (rioWriteBulkObject(&aof,o) == 0) goto werr;
            } else if (o->type == REDIS_LIST) {
                if (rewriteListObject(&aof,&key,o) == 0) goto werr;
            } else if (o->type == REDIS_SET) {
                if (rewriteSetObject(&aof,&key,o) == 0) goto werr;
            } else if (o->type == REDIS_ZSET) {
                if (rewriteSortedSetObject(&aof,&key,o) == 0) goto werr;
            } else if (o->type == REDIS_HASH) {
                if (rewriteHashObject(&aof,&key,o) == 0) goto werr;
            } else {
                redisPanic("Unknown object type");
            }

            /* Save the expire time 
             *
             * 保存键的过期时间
             */
            if (expiretime != -1) {
                char cmd[]="*3\r\n$9\r\nPEXPIREAT\r\n";

                // 写入 PEXPIREAT expiretime 命令
                if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
                if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
                if (rioWriteBulkLongLong(&aof,expiretime) == 0) goto werr;
            }
        }

        // 释放迭代器
        dictReleaseIterator(di);
    }

    /* Make sure data will not remain on the OS's output buffers */
    // 冲洗并关闭新 AOF 文件
    if (fflush(fp) == EOF) goto werr;
    if (aof_fsync(fileno(fp)) == -1) goto werr;
    if (fclose(fp) == EOF) goto werr;

    /* Use RENAME to make sure the DB file is changed atomically only
     * if the generate DB file is ok. 
     *
     * 原子地改名,用重写后的新 AOF 文件覆盖旧 AOF 文件
     */
    if (rename(tmpfile,filename) == -1) {
        redisLog(REDIS_WARNING,"Error moving temp append only file on the final destination: %s", strerror(errno));
        unlink(tmpfile);
        return REDIS_ERR;
    }

    redisLog(REDIS_NOTICE,"SYNC append only file rewrite performed");

    return REDIS_OK;

werr:
    fclose(fp);
    unlink(tmpfile);
    redisLog(REDIS_WARNING,"Write error writing append only file on disk: %s", strerror(errno));
    if (di) dictReleaseIterator(di);
    return REDIS_ERR;
}

将重建各种对象所需的命令协议格式写入到rio中:(在下列具体函数中会严格判断每条命令包含的元素数量)

int rioWriteBulkObject(rio *r, robj *obj):obj指向的整数对象或者字符串对象

int rewriteListObject(rio*r, robj *key, robj *o):列表对象

int rewriteSetObject(rio *r, robj *key, robj *o):集合对象

int rewriteSortedSetObject(rio *r, robj *key, robj *o):有序集合对象

int rewriteHashObject(rio *r, robj *key, robj *o):哈希对象
当AOF_REWRITE过程执行完毕,Redis会用新生成的文件去替换原来的AOF文件,至此我们可以说,现在AOF文件中的内容已经是最精简的了

现在还存在一个问题:如果我们是通过 主动方式 去执行AOF_REWRITE,那么在保存AOF文件期间,“键空间”是可能发生变化的(因为主进程没有被阻塞),若直接用新生成的文件去替换原来的AOF文件,就会造成数据的不一致性(丢失在AOF_REWRITE过程中更新的数据) 

那redis如何解决这个问题呢? 在文章开头讲AOF模式的时候,我列举了下面一段伪代码: 
Python代码   收藏代码
  1. def processCommand(cmd, argc, argv):  
  2.     # 执行命令  
  3.     call(cmd, argc, argv)  
  4.     # 该命令变更了键空间并且AOF模式打开  
  5.     if redisServer.update_key_space and redisServer.aof_state & REDIS_AOF_ON:  
  6.         feedAppendOnlyFile(cmd, argc, argv)   
  7.   
  8. def feedAppendOnlyFile(cmd, argc, argv):  
  9.     # 把命令转换成AOF协议格式  
  10.     aofCmdStr = getAofProtocolStr(cmd, argc, argv)  
  11.     redisServer.aof_buf.append(aofCmdStr )  
  12.   
  13.     # 存在一个子进程正在进行AOF_REWRITE  
  14.     if redisServer.aof_child_pid != -1:  
  15.         # 把变更命写写到aof重写缓存  
  16.         redisServer.aof_rewrite_buf_blocks.append(aofCmdStr )  

你会发现,如果redis检测到有一个子进程正在进行AOF_REWRITE,那么它会把这期间所有变更命令写到AOF重写缓存(aof_rewrite_buf_blocks),然后当子进程完成AOF_REWRITE后,它会向父进程发送信号,父进程接受到子进程发来的信号,会将AOF重写缓存中的内容追加到新生成文件(该过程执行函数为backgroundRewriteDoneHandler,该函数在serverCron中会被判断捕捉。AOF重写缓存的追加过程会阻塞父进程,直至完成),这样我们就可以保证数据的一致性,避免刚才说的问题发生。

AOF文件的载入和还原

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

1、创建一个不带网络连接的伪客户端(fakeClient):因为命令来源于AOF文件,非网络连接

2、循环读取每条指令,伪客户端执行,直至所有写命令处理完毕。

可参考aof.c/int loadAppendOnlyFile(char *filename)函数,该函数执行逻辑为:

打开AOF文件并检查,暂时性关闭AOF,建立伪客户端:

开始从文件中循环:

每一次读取内容到缓存,解析得到参数个数、每个参数,从命令表中寻找该命令,调用伪客户端,执行命令,最后清理命令和命令参数对象。

循环结束,关闭AOF文件,释放伪客户端,恢复AOF状态。

RDB和AOF持久化区别:

参考博客:https://blog.csdn.net/jackpk/article/details/30073097

总结:

1. 了解AOF协议

2. 了解AOF模式作用及原理

3. 了解AOF重写作用及原理

参考博客:https://blog.csdn.net/erica_1230/article/details/51305552


  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值