Redis之RDB持久化

来源:互联网 发布:追星必备软件 编辑:程序博客网 时间:2024/05/29 11:59

一、前言

Redis是key-value型的内存数据库,所有的数据都保存在内存中,如果一段断电或者服务器宕机那么数据将会全部丢失,这很显然满足不了很多业务场景的需求,比如我的Redis数据库中存放很多待执行的URL爬取任务,如果在运行过程中服务器突然断电,所有的数据及没有处理的任务都会丢失,因此为了解决这个问题Redis引入了RDB持久化功能,此外Redis还提供另外一种更高级的持久化功能AOF持久化,先来看下RDB持久化的实现机制等。



二、RDB持久化

2.1 RDB文件格式

在介绍持久化过程之前,我们来先看下RDB文件的格式



再看下key_value_pairs部分的结构,RDB文件中的key_value_pairs部分都保存了一个或以上数量的键值对,包括键值对的过期信息等,


2.2 RDB文件的创建与载入

Redis中有两个命令可以生成RDB文件,一个是SAVE,另外一个是BGSAVE

SAVE命令:SAVE命令会阻塞Redis服务器进程,指导RDB文件创建完毕,在这个期间服务器进程不能处理任何来自客户端的命令请求。


BGSAVE命令:BGSAVE命令会从服务器进程中派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程继续处理命令请求

创建RDB文件过程由rdb.c/rdbSave函数完成


/* 后台进行rbd保存操作 */int rdbSaveBackground(char *filename) {    pid_t childpid;    long long start;    if (server.rdb_child_pid != -1) return REDIS_ERR;// 距离上一次成功执行SAVE或者BGSAVE命令之后,服务器对数据库状态进行了多少次修改    server.dirty_before_bgsave = server.dirty;    server.lastbgsave_try = time(NULL);    // 获取当前系统时间    start = ustime();    //利用fork()创建子进程用来实现rdb的保存操作    //此时有2个进程在执行这段函数的代码,在子进行程返回的pid为0,    //所以会执行下面的代码,在父进程中返回的代码为孩子的pid,不为0,所以执行else分支的代码    //在父进程中放返回-1代表创建子进程失败    if ((childpid = fork()) == 0) {    //在这个if判断的代码就是在子线程中后执行的操作        int retval;        /* Child */// 由于子进程会和父进程共享打开的Socket文件描述句柄,所以要在子进程中关闭其        closeListeningSockets(0);        redisSetProcTitle("redis-rdb-bgsave");        //这个就是刚刚说的rdbSave()操作        // 开始执行save过程        retval = rdbSave(filename);        if (retval == REDIS_OK) {            size_t private_dirty = zmalloc_get_private_dirty();            if (private_dirty) {                redisLog(REDIS_NOTICE,                    "RDB: %zu MB of memory used by copy-on-write",                    private_dirty/(1024*1024));            }        }        exitFromChild((retval == REDIS_OK) ? 0 : 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) {            server.lastbgsave_status = REDIS_ERR;            redisLog(REDIS_WARNING,"Can't save in background: fork: %s",                strerror(errno));            return REDIS_ERR;        }        redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid);        server.rdb_save_time_start = time(NULL);        server.rdb_child_pid = childpid;        updateDictResizePolicy();        return REDIS_OK;    }    return REDIS_OK; /* unreached */}



/* 保存rdb数据库的内容到磁盘中 */int rdbSave(char *filename) {    dictIterator *di = NULL;    dictEntry *de;    char tmpfile[256];    char magic[10];    int j;    long long now = mstime();    FILE *fp;    rio rdb;    uint64_t cksum;    snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());    fp = fopen(tmpfile,"w");    if (!fp) {    //随时记录错误日志,此时为文件无法打开的时候        redisLog(REDIS_WARNING, "Failed opening .rdb for saving: %s",            strerror(errno));        return REDIS_ERR;    }//初始化rbd和fp的初始操作,据此判断,后面关于rdb的操作都存入到fp这个文件中// 使用rio进行健壮的I/O读写,将文件句柄与读缓冲区联系起来    rioInitWithFile(&rdb,fp);    if (server.rdb_checksum)        rdb.update_cksum = rioGenericUpdateChecksum;    snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION);    if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr;//循环服务端下的所有数据库    for (j = 0; j < server.dbnum; j++) {    //获取单个数据库        redisDb *db = server.db+j;        dict *d = db->dict;        if (dictSize(d) == 0) continue;        di = dictGetSafeIterator(d);        if (!di) {            fclose(fp);            return REDIS_ERR;        }        /* Write the SELECT DB opcode */        if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr;        if (rdbSaveLen(&rdb,j) == -1) goto werr;        /* Iterate this DB writing every entry */        while((de = dictNext(di)) != NULL) {            sds keystr = dictGetKey(de);            robj key, *o = dictGetVal(de);            long long expire;            initStaticStringObject(key,keystr);            expire = getExpire(db,&key);            //将里面的键值存入rdb中            if (rdbSaveKeyValuePair(&rdb,&key,o,expire,now) == -1) goto werr;        }        dictReleaseIterator(di);    }    di = NULL; /* So that we don't release it again on error. */    /* EOF opcode */    if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;    /* CRC64 checksum. It will be zero if checksum computation is disabled, the     * loading code skips the check in this case. */    cksum = rdb.cksum;    memrev64ifbe(&cksum);    if (rioWrite(&rdb,&cksum,8) == 0) goto werr;    /* Make sure data will not remain on the OS's output buffers */    if (fflush(fp) == EOF) goto werr;    if (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. */    if (rename(tmpfile,filename) == -1) {        redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno));        unlink(tmpfile);        return REDIS_ERR;    }    redisLog(REDIS_NOTICE,"DB saved on disk");    server.dirty = 0;    server.lastsave = time(NULL);    server.lastbgsave_status = REDIS_OK;    return REDIS_OK;werr://这里又用到了goto处理异常操作的代码    fclose(fp);    unlink(tmpfile);    redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));    if (di) dictReleaseIterator(di);    return REDIS_ERR;}

在具体的文件写入过程中,作者使用了自己包装的rio包,实现带缓冲、健壮的读写文件,


首先使用rio进行健壮的I/O读写,将文件句柄与读缓冲区联系起来


// 使用rio进行健壮的I/O读写,将文件句柄与读缓冲区联系起来    rioInitWithFile(&rdb,fp);

/* 根据上面描述的方法,定义了FileRio */static const rio rioFileIO = {    rioFileRead,    rioFileWrite,    rioFileTell,    NULL,           /* update_checksum */    0,              /* current checksum */    0,              /* bytes read or written */    0,              /* read/write chunk size */    { { NULL, 0 } } /* union for io-specific vars */};/* 初始化rio中的file变量 */void rioInitWithFile(rio *r, FILE *fp) {    *r = rioFileIO;    r->io.file.fp = fp;    r->io.file.buffered = 0;    r->io.file.autosync = 0;}

最终的实现由rio.h中的rioWrite函数实现带缓冲的读写

static inline size_t rioWrite(rio *r, const void *buf, size_t len) {    while (len) {    //判断当前操作字节长度是否超过最大长度        size_t bytes_to_write = (r->max_processing_chunk && r->max_processing_chunk < len) ? r->max_processing_chunk : len;        //写入新的数据时,更新校验和        if (r->update_cksum) r->update_cksum(r,buf,bytes_to_write);        //执行写方法        if (r->write(r,buf,bytes_to_write) == 0)            return 0;        buf = (char*)buf + bytes_to_write;        len -= bytes_to_write;        //操作字节数增加        r->processed_bytes += bytes_to_write;    }    return 1;}



原创粉丝点击