Redis持久化--RDB

1 RDB持久化简介

  Redis是一个键值对数据库服务器,服务器中通常包含任意个非空的数据库,每个非空的数据库又包含任意个键值对。为方便起见,我们将非空数据库及它们的键值对统称为数据库状态。RDB是Redis持久化方式的一种,可以将Redis在内存中的数据库状态保存到磁盘中,避免意外丢失。

  RDB持久化既可以手动执行,也可以根据服务器的配置选项自动执行,该功能可以将某个时间点上的数据库状态保存到一个RDB文件中----一个经过压缩的二进制文件,通过该文件可以将数据库还原到生成该文件时的状态。

1.1 RDB文件的结构

RDB文件的基本结构如下:

  • REDIS              文件最开头保存着REDIS五个字符,标志rdb文件的开始
  • db_version      一个四字节的以字符表示的整数,记录了文件使用的RDB版本号
  • databases       存放了服务器上所有非空数据库的所有数据
  • EOF                 标志着数据库内容的结尾(不是文件的结尾)
  • CHECK_SUM  RDB 文件所有内容的校验和, 一个 uint_64t 类型值。REDIS 在写入 RDB 文件时将校验和保存在 RDB 文件的末尾,当读取时, 根据它的值对内容进行校验

RDB文件中的数据库(database)的结构如下:

  • SELECTDB    一字节的常量,标示接下来要读的是一个数据库号码。
  • db_number    保存了一个数据库号码,使后面读入的键值对可以载入到正确的数据库中。
  • key_value_pairs   保存了键值对,如果键值对带有过期时间也保存在内。

不带过期时间的键值对结构

 

带过期时间的键值对结构

2 RDB持久化的实现

2.1 RDB文件的创建

有两个命令可以生成RDB文件:

  • SAVE: 阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器阻塞期间,不能处理任何命令请求;
  • BGSAVE: 该命令会派生出一个子进程,由子进程负责RDB文件的创建;服务器进程(父进程)继续处理命令请求;

RDB的创建工作由rdb.c/rdbSave()完成,SAVE和BGSAVE通过不同的方式调用该函数,如下面的伪代码:

def SAVE():
    #创建RDB文件
    rdbSave()
    
def BGSAVE():
    #创建子进程
    pid = fork()
    if pid == 0:
        #子进程创建RDB文件
        rdbSave()
        #完成之后向父进程发送信号
        signal_parent()
    elif pid > 0:
        #父进程处理命令请求并通过轮询等待子进程的信号
        handle_request_and_wait_signal()
    else:
        #处理出错情况
        handle_fork_error()

 

2.1.1 SAVE/BGSAVE执行时数据库的状态

  (1)SAVE命令执行时,服务器被阻塞,拒绝所有客户端命令请求;

  (2)BGSAVE命令执行时,RDB文件的创建工作在子进程中执行,父进程仍可以接受客户端命令请求。但是服务器对SAVE, BGSAVE, BGREWRITEAOF的处理方式和平时有所不同。

  • BGSAVE执行期间,客户端发送的SAVE, BGSAVE命令会被拒绝,因为两个进程同时调用rdbSave(),可能产生竞争条件;
  • BGSAVEBGREWRITEAOF同样不能同时执行。如果BGSAVE正在执行,则BGREWRITEAOF将会延迟到BGSAVE执行完之后再执行;如果BGREWRITEAOF正在执行,则客户端发送的BGSAVE命令将会被拒绝。(这两个命令不同时执行,主要是性能方面的考虑,俩子进程同时执行大量磁盘写入操作不会是个好主意)

2.1.2 rdbSave()函数的实现

int rdbSave(char *filename) {
    dictIterator *di = NULL;
    dictEntry *de;
    FILE *fp;
    rio rdb;
    uint64_t cksum;
    ...

    // 创建临时文件
    snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
    fp = fopen(tmpfile,"w");

    // 初始化 I/O
    rioInitWithFile(&rdb,fp);

    // 写入 RDB 版本号
    snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION);
    if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr;

    // 遍历所有数据库,将有效数据库状态保存到rdb文件中
    for (j = 0; j < server.dbnum; j++) {

        // 指向数据库
        redisDb *db = server.db+j;

        // 指向数据库键空间
        dict *d = db->dict;

        // 跳过空数据库
        if (dictSize(d) == 0) continue;

        // 创建键空间迭代器
        di = dictGetSafeIterator(d);

        /* 
        Write the SELECT DB opcode, 写入 DB 选择器
         */
        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;
            
            // 根据 keystr ,在栈中创建一个 key 对象
            initStaticStringObject(key,keystr);

            // 获取键的过期时间
            expire = getExpire(db,&key);

            // 保存键值对数据
            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 
     *
     * 写入 EOF 代码
     */
    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. 
     *
     * CRC64 校验和。
     *
     * 如果校验和功能已关闭,那么 rdb.cksum 将为 0 ,
     * 在这种情况下, RDB 载入时会跳过校验和检查。
     */
    cksum = rdb.cksum;
    memrev64ifbe(&cksum);
    rioWrite(&rdb,&cksum,8);

    /* Make sure data will not remain on the OS's output buffers */
    // 冲洗缓存,确保数据已写入磁盘
    // c库缓冲—–fflush———〉内核缓冲——–fsync—–〉磁盘
    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. 
     *
     * 使用 RENAME ,原子性地对临时文件进行改名,覆盖原来的 RDB 文件。
     */
    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;

    // 记录最后一次完成 SAVE 的时间
    server.lastsave = time(NULL);

    // 记录最后一次执行 SAVE 的状态
    server.lastbgsave_status = REDIS_OK;

    return REDIS_OK;

werr:
    // 关闭文件
    fclose(fp);
    // 删除文件
    unlink(tmpfile);

    redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));

    if (di) dictReleaseIterator(di);

    return REDIS_ERR;
}

2.1.3 RDB持久化--自动间隔保存 

  redis允许用户设置服务器配置的save选项,让服务器每隔一段时间执行一次bgsave。用户可以设置多个保存条件,只要一个条件满足,服务器就会执行bgsave。

(1)设置保存条件

save 900 1   服务器在900秒之内,对数据库进行至少1次修改
save 300 10 服务器在300秒之内,对数据库进行至少10次修改
save 60 10000 服务器在60秒之内,对数据库进行至少10000次修改
保存条件保存在redisServer结构的savaparams属性
struct redisServer {  
    ...
    struct saveparam *saveparams;   /* Save points array for RDB */  
}  
struct saveparam {  
    time_t seconds;  
    int changes;  
};  

(2)dirty计数器和lastsave属性

dirty计数器记录上一次rdb之后,服务器进行的数据库修改次数;
lastsave属性是记录上次执行rdb的时间戳;
struct redisServer {  
    ...
    time_t lastsave;                /* Unix time of last successful save */  
    time_t lastbgsave_try;          /* Unix time of last attempted bgsave */  
}

(3)检查保存条件

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {  
   if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||  
        ldbPendingChildren())  
    {……}   
    else {//遍历设置的保存条件  
         for (j = 0; j < server.saveparamslen; j++) {  
            struct saveparam *sp = server.saveparams+j;  
            if (server.dirty >= sp->changes &&  
                server.unixtime-server.lastsave > sp->seconds &&  
                (server.unixtime-server.lastbgsave_try >  
                 CONFIG_BGSAVE_RETRY_DELAY ||  
                 server.lastbgsave_status == C_OK))  
            {//满足保存条件,进行rdb  
                rdbSaveBackground(server.rdb_filename,NULL);  
                break;  
            }  
         }  
    }  
}

2.2 RDB文件的加载

 Redis的有两种持久化方式:RDB, AOF,而且AOF文件的更新频率通常比RDB文件高,所以,如果开启了AOF持久化功能,则会就先采用AOF文件还原数据库状态:

rdb文件重载入内存还原数据库时,采用的是rdbLoad函数: 

int rdbLoad(char *filename, rdbSaveInfo *rsi) {  
    ……  
    if ((fp = fopen(filename,"r")) == NULL) return C_ERR;  
    startLoading(fp);  
    rioInitWithFile(&rdb,fp);  
    retval = rdbLoadRio(&rdb,rsi);//载入rdb文件  
    ……  
    return retval;  
}  
int rdbLoadRio(rio *rdb, rdbSaveInfo *rsi) {  
    ……  
    rdb->update_cksum = rdbLoadProgressCallback;  
    rdb->max_processing_chunk = server.loading_process_events_interval_bytes;  
    if (rioRead(rdb,buf,9) == 0) goto eoferr;  
    buf[9] = '\0';  
    //检查rdb文件开头的redis RDB_VERSION  
    if (memcmp(buf,"REDIS",5) != 0)   
    rdbver = atoi(buf+5);  
    if (rdbver < 1 || rdbver > RDB_VERSION)   
    //解析rdb文件,rdb存储方式为type+object  
    while(1) {  
        //根据type,进行object解析  
        if ((type = rdbLoadType(rdb)) == -1) goto eoferr;  
        /* Handle special types. */  
        if (type == RDB_OPCODE_EXPIRETIME) {  
            //读取key-value的过期时间,时间单位为秒  
            if ((expiretime = rdbLoadTime(rdb)) == -1) goto eoferr;  
            // 获取key-value的value类型,用于接下去解析key-value  
            if ((type = rdbLoadType(rdb)) == -1) goto eoferr;  
            expiretime *= 1000;  
        } else if (type == RDB_OPCODE_EXPIRETIME_MS) {  
            //读取key-value的过期时间,时间单位为毫秒  
            if ((expiretime = rdbLoadMillisecondTime(rdb)) == -1) goto eoferr;  
            // 获取key-value的value类型,用于接下去解析key-value  
            if ((type = rdbLoadType(rdb)) == -1) goto eoferr;  
        } else if (type == RDB_OPCODE_EOF) {  
            //解析道EOF,载入完毕  
            break;  
        } else if (type == RDB_OPCODE_SELECTDB) {  
            //解析数据库id  
            if ((dbid = rdbLoadLen(rdb,NULL)) == RDB_LENERR)  
                goto eoferr;  
            db = server.db+dbid;  
            continue; /* Read type again. */  
        } else if (type == RDB_OPCODE_RESIZEDB) {  
            //解析dict和expires的size,并创建对应大小的字典  
            uint64_t db_size, expires_size;  
            if ((db_size = rdbLoadLen(rdb,NULL)) == RDB_LENERR)  
            if ((expires_size = rdbLoadLen(rdb,NULL)) == RDB_LENERR)  
            dictExpand(db->dict,db_size);  
            dictExpand(db->expires,expires_size);  
            continue; /* Read type again. */  
        } else if (type == RDB_OPCODE_AUX) {        
            ……//解析rdb文件的默认字段信息  
            continue; /* Read type again. */  
        }  
        //解析key-value  
        if ((key = rdbLoadStringObject(rdb)) == NULL) goto eoferr;  
        if ((val = rdbLoadObject(type,rdb)) == NULL) goto eoferr;  
        //添加key-value到数据库  
        dbAdd(db,key,val);  
        //设置过期时间  
        if (expiretime != -1) setExpire(NULL,db,key,expiretime);  
        decrRefCount(key);  
    }  
    /* Verify the checksum if RDB version is >= 5 */  
    if (rdbver >= 5 && server.rdb_checksum)   
        ……  
    return C_OK;  
  
}  

 

参考:
redis rdb持久化的源码分析

Redis persistence demystified

 

posted @ 2017-09-16 10:23  harvyxu  阅读(1036)  评论(0编辑  收藏  举报