Redis之RDB持久化

一、前言

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;
}



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值