redis源码浅析--十-AOF(append only file)持久化

环境说明:redis源码版本 5.0.3;我在阅读源码过程做了注释,git地址:https://gitee.com/xiaoangg/redis_annotation
参考书籍:《redis的设计与实现》

除了RDB持久化功能外,redis还提供了AOF持久化功能;

RDB持计划功能是将键值编码后保存到RDB文件键中,而AOF是将执行的命令保存到AOF文件中;

一. AOF功能的实现

aof功能可以分为 命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load);

1.命令的追加

当aof处于打开状态时,服务器在执行一个写命令后,会以协议格式写入aof缓存;

aof文件追击的入口位于service.c/call() 调用的propagate();

aof文件格式协议位于aof.c/catAppendOnlyGenericCommand:


/*
创建aof文本格式
argc 命令的个数
argv robj命令数组  
(
set abc 1
则 argc =3, argv =["set", "abc", "1"] 
)
*/
sds catAppendOnlyGenericCommand(sds dst, int argc, robj **argv) {
    char buf[32];
    int len, j;
    robj *o;

    buf[0] = '*'; //每个备份命令都是以*开始;  当前 buf="*"
    len = 1 + ll2string(buf + 1, sizeof(buf) - 1, argc); //这时候buf= "*"+命令参数的个数;(如argc=3 这时候buf=“*3”)
    buf[len++] = '\r';                                   //buf=“*3\r“
    buf[len++] = '\n';                                   //buf=“*3\r\n“
    dst = sdscatlen(dst, buf, len);                      //将buf内字符串追加到 dst

    //开始拼接命令信息 这里假设命令信息是 set abc 123 
    for (j = 0; j < argc; j++) {
        o = getDecodedObject(argv[j]);
        buf[0] = '$';   //buf ="$"
        len = 1+ll2string(buf+1,sizeof(buf)-1,sdslen(o->ptr)); //buf="$3" ;   加上了set 字符的长度
        buf[len++] = '\r';                                    
        buf[len++] = '\n';                                     //buf=“buf="$3\r\n"
        dst = sdscatlen(dst, buf, len);                        //buf 拼接到dst后
        dst = sdscatlen(dst,o->ptr,sdslen(o->ptr));
        dst = sdscatlen(dst,"\r\n",2);
        decrRefCount(o);
    }
    return dst;
}

2.AOF文件的写入与同步

redis的服务器进程就是一个事件循环,这个循环中文件事件负责接受客户端的命令请求,以及向客户端发送命令回复;

时间事件则服务执行需要定时执行的函数;

服务器每次结束一次事件循环之前,都会调用aof.c/flushAppendOnlyFile 函数,考虑是否要将AOF缓存区的内容保存到AOF文件;

flushAppendOnlyFile的行为由服务器配置的appendfsync来决定:

#define AOF_FSYNC_NO 0
#define AOF_FSYNC_ALWAYS 1
#define AOF_FSYNC_EVERYSEC 2

  • AOF_FSYNC_ALWAYS = 1 将aof缓冲区所有的内容写入并同步到AOF文件
  •  AOF_FSYNC_EVERYSEC =2 将aof缓冲区的所有内容写入到AOF文件,如果上次同步aof文件的时间距现在超过了一秒,那么再对aof文件进行同步操作; 并且这个操作是由一个线程专门负责执行的;
  • AOF_FSYNC_NO = 0 将aof缓冲区中的所有内容写入到AOF文件,但并不对aof文件进行同步,何时同步由操作系统解决

文件的写入与同步:
为了提高文件的写入效率,在现代操作系统中,当用户调用write函数写文件时,操作系统通常会将数据咱还保存在内存中一个缓冲区里面,当缓冲区被填满或者超过来指定的时限,才将缓冲区中的数据写入磁盘;

这种做法提高了文件的写入效率,但也带来了安全问题,如果计算机发生宕机,那么保存缓冲区的数据将会丢失;

为此系统提供了fsync和fdatasync两个函数,他们可以强制将缓冲区中的数据写入磁盘;


二. AOF文件的载入和数据还原

aof文件中包含了所有重建数据库的写命令,所以只需要读入aof文件内容,并重新执行一边,就可以还原数据库状态;

redis读取读取aof文件并还原的具体步骤如下:

  1. 创建一个不带网络连接的伪客户端(fake client):因为redis命令只能在客户端上下文中执行,所有为了指定aof文件中的命令,创建了一个伪客户端(aof.c/createFakeClient)
  2. 在aof中读取并解析出命令;
  3. 使用伪客户端执行写命令;
  4. 循环 2、3直到aof文件处理完毕;(aof.c/loadAppendOnlyFile)

 

三. AOF重写

aof持久化是通过保存被执行的写命令,来记录数据库状态的,所以aof文件的体积会随着服务器运行的时长, 变得越来越大;

为了解决这一问题,redis提供来AOF重写功能;
 

1.AOF重写的实现

AOF的重写并不需要对现有的AOF文件读取、分析或者写入操作,这个功能是通过读取当前数据库状态来实现的

AOF重写的入口位于aof.c/rewriteAppendOnlyFileRio


/*
 * aof文件重写
 */
int rewriteAppendOnlyFileRio(rio *aof) {
    dictIterator *di = NULL;
    dictEntry *de;
    size_t processed = 0;
    int j;

    //遍历所有数据库 
    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);

        //写入select 命令
        /* SELECT the new DB */
        if (rioWrite(aof,selectcmd,sizeof(selectcmd)-1) == 0) goto werr;
        if (rioWriteBulkLongLong(aof,j) == 0) goto werr;

        //遍历所有数据中的键
        /* Iterate this DB writing every entry */
        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);

            /* Save the key and associated value */
            if (o->type == OBJ_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 */
                //写入key
                if (rioWriteBulkObject(aof,&key) == 0) goto werr;
                //写入value
                if (rioWriteBulkObject(aof,o) == 0) goto werr;
            } else if (o->type == OBJ_LIST) { //list 
                if (rewriteListObject(aof,&key,o) == 0) goto werr;
            } else if (o->type == OBJ_SET) {
                if (rewriteSetObject(aof,&key,o) == 0) goto werr;
            } else if (o->type == OBJ_ZSET) {
                if (rewriteSortedSetObject(aof,&key,o) == 0) goto werr;
            } else if (o->type == OBJ_HASH) {
                if (rewriteHashObject(aof,&key,o) == 0) goto werr;
            } else if (o->type == OBJ_STREAM) {
                if (rewriteStreamObject(aof,&key,o) == 0) goto werr;
            } else if (o->type == OBJ_MODULE) {
                if (rewriteModuleObject(aof,&key,o) == 0) goto werr;
            } else {
                serverPanic("Unknown object type");
            }
            /* Save the expire time */
            if (expiretime != -1) {
                char cmd[]="*3\r\n$9\r\nPEXPIREAT\r\n";
                if (rioWrite(aof,cmd,sizeof(cmd)-1) == 0) goto werr;
                if (rioWriteBulkObject(aof,&key) == 0) goto werr;
                if (rioWriteBulkLongLong(aof,expiretime) == 0) goto werr;
            }
            /* Read some diff from the parent process from time to time. */
            if (aof->processed_bytes > processed+AOF_READ_DIFF_INTERVAL_BYTES) {
                processed = aof->processed_bytes;
                aofReadDiffFromParent();
            }
        }
        dictReleaseIterator(di);
        di = NULL;
    }
    return C_OK;

werr:
    if (di) dictReleaseIterator(di);
    return C_ERR;
}


2.AOF后台重写

问题: AOF重写会进行大量的写入操作,所以调用这个函数的进程会被长时间的堵塞;因为redis服务器是用单线程来处理命令请求,如果由服务器来调用重写,那么服务器将无法处理客户端的命令请求;

解决:为了解决这一问题,redis将aof的重写程序放到来子进程中执行;

 

使用子进程带来的新的问题:子进程在aof重写期间,服务器进程还在继续处理命令请求,新的命令会对现有的数据库状态进行修改,导致 数据库当前的数据状态和aof文件中的数据不一致

解决:redis服务器设置来一个重写缓冲区,这个缓冲区在服务器创建了子进程后开始使用。
服务器会将写命令同时发送给AOF缓冲区和AOF重写缓冲区;

当子进程完成了AOF重写工作后,会向父进程发送一个信号,父进程接收到信号后,会调用一个处理函数:

  1. 将aof重写缓冲区中所有内容写入AOF文件;
  2. 用新的AOF替换旧的AOF文件;

整个aof后台的重写过程,只有信号处理函数 对服务器造成了堵塞,这将AOF重写对服务器的影响降到了最低;

你可能感兴趣的:(redis)