• 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] = '';  
        //检查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

  • 相关阅读:
    DevCon 5 2019 活动照片
    区块链小册 | 必知的运营常识
    区块链小册 | 必知的运营渠道
    产品经理需求沟通的艺术
    作为产品经理要如何面对失败?
    展示亚洲金融科技状况的 15 张金融科技地图
    成为区块链行业的产品经理是什么感觉
    腾讯产品经理能力模型
    jQuery 知识点大纲
    call()与apply()区别
  • 原文地址:https://www.cnblogs.com/harvyxu/p/7530459.html
Copyright © 2020-2023  润新知