Redis源码剖析(十)--RDB持久化

RDB触发机制

命令触发

  • SAVE:SAVE命令会阻塞Redis服务进程,知道RDB文件创建完毕为止。
  • BGSAVE:BGSAVE会创建子进程,子进程负责创建RDB文件,父进程继续处理命令请求

自动间隔性保存

当配置文件中save选项的条件满足时,服务器自动执行BGSAVE命令。

// 满足以上三个条件中的任意一个,则自动触发 BGSAVE 操作 
save 900 1       // 服务器在900秒之内,对数据库执行了至少1次修改 
save 300 10      // 服务器在300秒之内,对数据库执行了至少10修改 
save 60  1000    // 服务器在60秒之内,对数据库执行了至少1000修改 

自动执行 BGSAVE 命令的条件保存在 redisServer 结构中的 savaparams 属性。当服务器成功执行一个修改命令后,dirty 计数会加一,而 lastsave属性记录了最后一次完成 SAVE 的 UNIX 时间戳。Redis的周期性操作函数 serverCron 会定时检查 save 选项的条件是否满足,如果满足,就会执行BGSAVE命令。

struct redisServer{

    // 记录了BGSAVE自动执行的条件
    struct saveparam *saveparams;

    // 自从上次 SAVE 执行以来,数据库被修改的次数
    long long dirty;      

    // 最后一次完成 SAVE 的时间
    time_t lastsave;  
    // .......
}

// 服务器的保存条件(BGSAVE 自动执行的条件)
struct saveparam {
    // 多少秒之内
    time_t seconds;

    // 发生多少次修改
    int changes;

};

 

RDB持久化源码

BGSAVE 命令底层实现函数 dbSaveBackground 会先 fork 出子进程,由子进程执行 rdbSave 函数。整个持久化函数 rdbSave 的核心在于通过 rdbSaveKeyValuePair 函数保存数据库的键值对,rdbSaveKeyValuePair 函数底层的 rdbSaveObject 函数会针对不用的对象类型采用不用的编码格式来保存数据。

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

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

    // 设置校验和函数
    if (server.rdb_checksum)
        rdb.update_cksum = rioGenericUpdateChecksum;

    // 写入 RDB 版本号
    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;
        }

        /* 
         * 写入 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; 

     // 写入 EOF 代码
    if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;

    /*
     * CRC64 校验和。
     * 如果校验和功能已关闭,那么 rdb.cksum 将为 0 ,
     * 在这种情况下, RDB 载入时会跳过校验和检查。
     */
    cksum = rdb.cksum;
    memrev64ifbe(&cksum);
    rioWrite(&rdb,&cksum,8);

    // 冲洗缓存,确保数据已写入磁盘
    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1) goto werr;
    if (fclose(fp) == EOF) goto werr;

    /* 
     * 使用 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;
}

 

RDB文件结构

  • REDIS:5字节,保存着 "REDIS" 五个字符
  • db_version:4字节,RDB文件的版本号
  • database 0:数据库中的键值对
    • SELECTDB:1字节常量
    • db_number:数据库号码
    • key_value_pairs:键值对
      • 含过期时间的键值对会带有 EXPIRETIME_MS 和过期时间
  • EOF:RDB文件的结束标志
  • check_sum:校验和(CRC64),用来检查RDB文件是否出错

key_value_pairs键值对中的 TYPE 属性:记录类对象的编码类型,程序会根据 TYPE 属性来决定如何读入和解释value数据。

 

posted @ 2018-12-29 10:45  不学习就没有梦想  阅读(488)  评论(0编辑  收藏  举报
levels of contents