【redis源码分析】RDB持久化机制

本文深入解析Redis RDB的实现原理,包括快照保存与导入机制、数据格式等关键内容。

        rdb是redis保存内存数据到磁盘数据的其中一种方式(另一种是AOF)。Rdb的主要原理就是在某个时间点把内存中的所有数据的快照保存一份到磁盘上。在条件达到时通过fork一个子进程把内存中的数据写到一个临时文件中来实现保存数据快照。在所有数据写完后再把这个临时文件用原子函数rename(2)重命名为目标rdb文件。这种实现方式充分利用fork的copy on write。

  另外一种是通过save命令主动触发保存数据快照,这种是阻塞式的,即不会通过生成子进程(就在当前进程完成)来进行数据集快照的保存。

  相关配置

save <seconds> <changes>

  经过多少秒且多少个key有改变就进行,可以配置多个,只要有一个满足就进行保存数据快照到磁盘

rdbcompression yes

  保存数据到rdb文件时是否进行压缩,如果不想可以配置成’no’,默认是’yes’,因为压缩可以减少I/O,当然,压缩需要消耗一些cpu资源。

dbfilename dump.rdb

  快照文件名

dir ./

  快照文件所在的目录,同时也是AOF文件所在的目录

  Rdb文件格式

  [注:本节所说的类型,值在没有特别标明的情况下都是针对rdb文件来说的]

  Rdb文件的整体格式

  文件签名 | 版本号 | 类型 | 值 | 类型 | 值 | … | 类型 | 值

  [注:竖线和空格是为了便于阅读而加入的,rdb文件中是没有竖线和空格分隔的]

  • 文件签名是字符串:REDIS
  • 版本号是字符串:0006
  • 类型是指值的类型,redis值的类型有很多种,下边一一介绍
  • 值是对应的类型下的值,不同类型的值格式不一样。这里的值包含了redis中的key与val。而不是单指redis中val。

  REDIS_SELECTDB类型与REDIS_EOF类型

  • REDIS_SELECTDB类型:对应的值是redis db的编号,从0开始到比db数小1的数值。redis中可以配置db数,每个key只属于一个db。
  • 存储redis db的编号时使用的是存储长度时使用的格式,为了尽量压缩rdb文件,存储长度使用的字节数是不一样的,会进行重新编码
  • REDIS_EOF类型:没有对应的值。rdb文件的结束符。

  把这REDIS_SELECTDB类型和REDIS_EOF类型代入到上边的rdb文件的格式中,那么rdb文件的整体格式变成为:

  文件签名 | 版本号 | REDIS_SELECTDB类型 | db编号 | 类型 | 值 | … | REDIS_SELECTD 类型 | db编号 | 类型 | 值 | … | REDIS_EOF类型

  • 两个REDIS_SELECTDB类型之间的数据都是该db下边的key和value的数据

  相关代码

  Rdb.c

int rdbSave(char *filename) { 
    … 
      // 以 "temp-<pid>.rdb" 格式创建临时文件名
    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;
    }

    // 初始化 rio 文件
    rioInitWithFile(&rdb,fp);
    // 如果有需要的话,设置校验和计算函数
    if (server.rdb_checksum)
        rdb.update_cksum = rioGenericUpdateChecksum;
    // 以 "REDIS <VERSION>" 格式写入文件头,以及 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;
        // 指向数据库 key space
        dict *d = db->dict;
        // 数据库为空, pass ,处理下个数据库
        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 */
        // 将数据库中的所有节点保存到 RDB 文件
        while((de = dictNext(di)) != NULL) {
            // 取出键
            sds keystr = dictGetKey(de);
            // 取出值
            robj key, 
                 *o = dictGetVal(de);
            long long expire;
            
            initStaticStringObject(key,keystr);
            // 取出过期时间
            expire = getExpire(db,&key);
             //保存所有的键值对
            if (rdbSaveKeyValuePair(&rdb,&key,o,expire,now) == -1) goto werr;
        }
        dictReleaseIterator(di);
    }
    … 
}


  Rdb中长度的存储

  Redis为了尽量压缩rdb文件真是费尽心思,先来看看redis为了压缩使用的长度存储。长度主要用在字符串长度,链表长度,hash表的大小存储上。

  Redis把长度的存储分为四种,最左边字节的从左到右的前两位用于区分长度的存储类型。

    相关代码   

    Rdb.c:31

int rdbSaveLen(rio *rdb, uint32_t len) {
    unsigned char buf[2];
    size_t nwritten;

    if (len < (1<<6)) {
        /* Save a 6 bit len */
		//00 xxxxxx
        buf[0] = (len&0xFF)|(REDIS_RDB_6BITLEN<<6);
        if (rdbWriteRaw(rdb,buf,1) == -1) return -1;
        nwritten = 1;
    } else if (len < (1<<14)) {
        /* Save a 14 bit len */
		//01 xxxxxx xxxxxxxx
        buf[0] = ((len>>8)&0xFF)|(REDIS_RDB_14BITLEN<<6);
        buf[1] = len&0xFF;
        if (rdbWriteRaw(rdb,buf,2) == -1) return -1;
        nwritten = 2;
    } else {
        /* Save a 32 bit len */
		//10 xxxxxx
        buf[0] = (REDIS_RDB_32BITLEN<<6);
        if (rdbWriteRaw(rdb,buf,1) == -1) return -1;
        len = htonl(len);
		//转换为网络字节序写入,
		if (rdbWriteRaw(rdb,&len,4) == -4) return -1;
        nwritten = 1+4;
    }

    // 返回编码所使用的长度
    return nwritten;
}


  也许你发现了,代码中只有上边的表格中只有3种,还有一种哪去了呢?

  另外一种比较特殊,如下:

    是不是觉得这种长度类型很奇怪,为什么要这样做?

  Redis在两种情况下需要对存储的内容进行编码

  1.把字符串转成整数存储

  比如:‘-100’需要4个字节存储,转换整数只需要一个字节

  相关函数rdbTryIntegerEncoding(rdb.c:88)

  2.使用lzf算法压缩字符串

  相关函数lzf_compress(lzf_c.c:99),lzf的算法解释见lzf字符串压缩算法

  当redis使用这两种编码对字符串进行编码时,在读取时需要区分该字符串有没有被编码过,对编码过的字符串需要特别处理,因为长度信息是存储在字符串的前面的,所以可以通过在存储长度的位置上加入编码类型的信息。

  我们来看看相关代码

  Rdb.c:557

 //从rdb文件中中读取长度
uint32_t rdbLoadLen(rio *rdb, int *isencoded) {
    unsigned char buf[2];
    uint32_t len;
    int type;

    if (isencoded) *isencoded = 0;
    if (rioRead(rdb,buf,1) == 0) return REDIS_RDB_LENERR;
    type = (buf[0]&0xC0)>>6;
    if (type == REDIS_RDB_ENCVAL) {
        //进行了特殊的编码,置编码标志,并返回编码类型
        if (isencoded) *isencoded = 1;
        return buf[0]&0x3F;
    } else if (type == REDIS_RDB_6BITLEN) {
        /* Read a 6 bit len. */
        return buf[0]&0x3F;
    } else if (type == REDIS_RDB_14BITLEN) {
        /* Read a 14 bit len. */
        if (rioRead(rdb,buf+1,1) == 0) return REDIS_RDB_LENERR;
        return ((buf[0]&0x3F)<<8)|buf[1];
    } else {
        /* Read a 32 bit len. */
        if (rioRead(rdb,&len,4) == 0) return REDIS_RDB_LENERR;
        return ntohl(len);
    }
}


  我们可以看到,在读取rdb文件时,当发现长度类型是REDIS_RDB_ENCVAL,把编码类型返回。

  我们来看看知道编码类型后的处理

  Rdb.c

/*
 * 根据编码 encode ,从 rdb 文件中读取字符串,并返回字符串对象
 */
robj *rdbGenericLoadStringObject(rio *rdb, int encode) {
    int isencoded;
    uint32_t len;
    sds val;

    // 获取字符串的长度
    len = rdbLoadLen(rdb,&isencoded);
    if (isencoded) {
    	//如果被编码了,则返回的是编码类型
        switch(len) {
        case REDIS_RDB_ENC_INT8:
        case REDIS_RDB_ENC_INT16:
        case REDIS_RDB_ENC_INT32:
            // 字节串是整数,创建整数对象并返回
            return rdbLoadIntegerObject(rdb,len,encode);
        case REDIS_RDB_ENC_LZF:
            // 字节串是被 lzf 算法压缩的字符串
            return rdbLoadLzfStringObject(rdb);
        default:
            redisPanic("Unknown RDB encoding type");
        }
    }

    if (len == REDIS_RDB_LENERR) return NULL;

    // 创建一个指定长度的 sds
    val = sdsnewlen(NULL,len);
    if (len && rioRead(rdb,val,len) == 0) {
        sdsfree(val);
        return NULL;
    }
    // 根据 sds ,创建字符串对象
    return createObject(REDIS_STRING,val);
}

整个流程是:

  • 读取长度
  • 如果长度类型是有编码信息的,则根据编码类型进行读取
  • 如果长度类型是有效长度,则根据长度信息读取字符串

  REDIS_EXPIRETIME类型

  • 如果一个key被expire设置过,那么在该key与value的前面会有一个REDIS_EXPIRETIME类型与其对应的值。
  • REDIS_EXPIRETIME类型对应的值是过期时间点的timestamp
  • REDIS_EXPIRETIME类型与其值是可选的,不是必须的,只有被expire设置过的key才有这个值

  假设有一个key被expire命令设置过,把这REDIS_EXPIRETIME类型代入到上边的rdb文件的格式中,那么rdb文件的整体格式变成为:

  文件签名 | 版本号 | REDIS_SELECTDB类型 | db编号 | REDIS_EXPIRETIME类型 | timestamp | 类型 | 值 | … | REDIS_SELECTD 类型 | db编号 | 类型 | 值 | … | REDIS_EOF类型

  数据类型

  数据类型主要有以下类型:

/* Dup object types to RDB object types. Only reason is readability (are we
 * dealing with RDB types or with in-memory object types?).
 *
 * 对象类型在 RDB 文件中的类型
 */
#define REDIS_RDB_TYPE_STRING 0
#define REDIS_RDB_TYPE_LIST   1
#define REDIS_RDB_TYPE_SET    2
#define REDIS_RDB_TYPE_ZSET   3
#define REDIS_RDB_TYPE_HASH   4

/* Object types for encoded objects. */
/*
 * 编码对象的方式
 */
#define REDIS_RDB_TYPE_HASH_ZIPMAP    9
#define REDIS_RDB_TYPE_LIST_ZIPLIST  10
#define REDIS_RDB_TYPE_SET_INTSET    11
#define REDIS_RDB_TYPE_ZSET_ZIPLIST  12
#define REDIS_RDB_TYPE_HASH_ZIPLIST  13


  下边以REDIS_RDB_TYPE_STRING和REDIS_RDB_TYPE_LIST类型为例进行详解,其他的类型都类似

        类型

        REDIS_RDB_TYPE_STRING | 值

       假设rdb文件由一个值是REDIS_RDB_TYPE_STRING类型,比如执行了一个set mykey mevalue的命令,则在rdb文件中表示为

       REDIS_RDB_TYPE_STRING类型 | 值

其中值包含了key的长度,key的值,val的长度和val的值,把REDIS_RDB_TYPE_STRING类型值的格式代入得:

  REDIS_RDB_TYPE_STRING类型 | keylen | mykey | vallen | myval

  类型用一个字节表示,长度的存储格式见rdb中长度的存储

相关的代码:

/* Save a key-value pair, with expire time, type, key, value.
 * 保存键值对,值的类型,以及它的过期时间(如果有的话)。
 *
 * On error -1 is returned.
 * 出错返回 -1 。
 *
 * On success if the key was actaully saved 1 is returned, otherwise 0
 * is returned (the key was already expired). 
 *
 * 如果 key 已经过期,放弃保存,返回 0 。
 * 如果 key 保存成功,返回 1 。
 */
int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val,
                        long long expiretime, long long now)
{
    /* Save the expire time */
    // 保存过期时间
    if (expiretime != -1) {
        /* If this key is already expired skip it */
        // key 已过期,直接跳过
        if (expiretime < now) return 0;

        if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EXPIRETIME_MS) == -1) return -1;
        if (rdbSaveMillisecondTime(rdb,expiretime) == -1) return -1;
    }

    /* Save type, key, value */
    // 保存值类型
    if (rdbSaveObjectType(rdb,val) == -1) return -1;
    // 保存 key
    if (rdbSaveStringObject(rdb,key) == -1) return -1;
    // 保存 value
    if (rdbSaveObject(rdb,val) == -1) return -1;

    return 1;
}

  REDIS_LIST类型

  1.List

如果以linkedlist实现list

  REDIS_LIST | keylen | key | listlen | len | value | len | value

  Listlen是链表长度

  Len是链表结点的值value的长度

  Value是链表结点的值

  2.Ziplist

  REDIS_ENCODING_ZIPLIST| keylen | key   | ziplist

  Ziplist就是通过字符串来实现的,直接将其存储于rdb文件中即可

       相关代码:

/* Save a Redis object. Returns -1 on error, 0 on success. */
/*
 * 将 Redis 对象写入到 rdb 。
 *
 * 出错返回 -1 ,写入成功返回 0 。
 */
int rdbSaveObject(rio *rdb, robj *o) {
    int n, nwritten = 0;

    if (o->type == REDIS_STRING) {
        /* Save a string value */
        // 字符串直接保存
        if ((n = rdbSaveStringObject(rdb,o)) == -1) return -1;
        nwritten += n;
    } else if (o->type == REDIS_LIST) {
        /* Save a list value */
        if (o->encoding == REDIS_ENCODING_ZIPLIST) {
            // 保存 ziplist 占用的字节数量
            size_t l = ziplistBlobLen((unsigned char*)o->ptr);

            // 以字符串形式保存整个 ziplist
            if ((n = rdbSaveRawString(rdb,o->ptr,l)) == -1) return -1;
            nwritten += n;
        } else if (o->encoding == REDIS_ENCODING_LINKEDLIST) {
            list *list = o->ptr;
            listIter li;
            listNode *ln;

            // </span><strong><span style="color:#ff0000;">保存入节点数量</span></strong><span style="color:#333333;">
            if ((n = rdbSaveLen(rdb,listLength(list))) == -1) return -1;
            nwritten += n;

            // </span><strong><span style="color:#ff0000;">遍历所有链表节点,取出值,并以字符形式保存它们</span></strong><span style="color:#333333;">
            listRewind(list,&li);
            while((ln = listNext(&li))) {
                robj *eleobj = listNodeValue(ln);
                if ((n = </span><strong><span style="color:#ff0000;">rdbSaveStringObject</span></strong><span style="color:#333333;">(rdb,eleobj)) == -1) return -1;
                nwritten += n;
            }
        } else {
            redisPanic("Unknown list encoding");
        }
    } else if (o->type == </span><strong><span style="color:#ff0000;">REDIS_SET</span></strong><span style="color:#333333;">) {
        /* Save a set value */
        if (o->encoding == </span><strong><span style="color:#ff0000;">REDIS_ENCODING_HT</span></strong><span style="color:#333333;">) {
            dict *set = o->ptr;
            dictIterator *di = dictGetIterator(set);
            dictEntry *de;

            //</span><strong><span style="color:#ff0000;"> 保存集合的基数</span></strong><span style="color:#333333;">
            if ((n = rdbSaveLen(rdb,dictSize(set))) == -1) return -1;
            nwritten += n;

            // 取出所有成员的值,并以字符串形式保存它们
            while((de = dictNext(di)) != NULL) {
                robj *eleobj = dictGetKey(de);
                if ((n = </span><span style="color:#ff0000;"><strong>rdbSaveStringObjec</strong></span><span style="color:#333333;">t(rdb,eleobj)) == -1) return -1;
                nwritten += n;
            }
            dictReleaseIterator(di);
        } else if (o->encoding == </span><strong><span style="color:#ff0000;">REDIS_ENCODING_INTSET</span></strong><span style="color:#333333;">) {
            // 保存 intset 占用的字节数量
            size_t l = intsetBlobLen((intset*)o->ptr);

            </span><strong><span style="color:#ff0000;">// 以字符串形式保存整个 intset</span></strong><span style="color:#333333;">
            if ((n = rdbSaveRawString(rdb,o->ptr,l)) == -1) return -1;
            nwritten += n;
        } else {
            redisPanic("Unknown set encoding");
        }
    } else if (o->type == </span><strong><span style="color:#ff0000;">REDIS_ZSET</span></strong><span style="color:#333333;">) {
        /* Save a sorted set value */
        if (o->encoding == </span><span style="color:#ff0000;"><strong>REDIS_ENCODING_ZIPLIST</strong></span><span style="color:#333333;">) {
            // 保存 ziplist 占用的字节数
            size_t l = ziplistBlobLen((unsigned char*)o->ptr);
            
            </span><span style="color:#ff0000;"><strong>// 将整个 ziplist 以字符串形式保存</strong></span><span style="color:#333333;">
            if ((n = rdbSaveRawString(rdb,o->ptr,l)) == -1) return -1;
            nwritten += n;
        } else if (o->encoding == </span><span style="color:#ff0000;"><strong>REDIS_ENCODING_SKIPLIST</strong></span><span style="color:#333333;">) {
            zset *zs = o->ptr;
            dictIterator *di = dictGetIterator(zs->dict);
            dictEntry *de;

            </span><span style="color:#ff0000;">// 保存有序集成员的基数</span><span style="color:#333333;">
            if ((n = rdbSaveLen(rdb,dictSize(zs->dict))) == -1) return -1;
            nwritten += n;

            // 遍历整个字典,保存所有有序集成员
            while((de = dictNext(di)) != NULL) {
                robj *eleobj = dictGetKey(de);
                double *score = dictGetVal(de);

                </span><span style="color:#ff0000;"><strong>// 保存 member</strong></span><span style="color:#333333;">
                if ((n = rdbSaveStringObject(rdb,eleobj)) == -1) return -1;
                nwritten += n;

                </span><span style="color:#ff0000;"><strong>// 保存 score</strong></span><span style="color:#333333;">
                if ((n = rdbSaveDoubleValue(rdb,*score)) == -1) return -1;
                nwritten += n;
            }
            dictReleaseIterator(di);
        } else {
            redisPanic("Unknown sorted set encoding");
        }
    } else if (o->type == </span><strong><span style="color:#ff0000;">REDIS_HASH</span></strong><span style="color:#333333;">) {
        /* Save a hash value */
        if (o->encoding == </span><span style="color:#ff0000;"><strong>REDIS_ENCODING_ZIPLIST</strong></span><span style="color:#333333;">) {
            // 保存 ziplist 占用的字节数
            size_t l = ziplistBlobLen((unsigned char*)o->ptr);
            
            </span><span style="color:#ff0000;"><strong>// 将整个 ziplist 保存为字符串</strong></span><span style="color:#333333;">
            if ((n = rdbSaveRawString(rdb,o->ptr,l)) == -1) return -1;
            nwritten += n;

        } else if (o->encoding == </span><span style="color:#ff0000;"><strong>REDIS_ENCODING_HT</strong></span><span style="color:#333333;">) {
            dictIterator *di = dictGetIterator(o->ptr);
            dictEntry *de;
            
            </span><span style="color:#ff0000;"><strong>// 记录字典的键值对数量</strong></span><span style="color:#333333;">
            if ((n = rdbSaveLen(rdb,dictSize((dict*)o->ptr))) == -1) return -1;
            nwritten += n;

            // 遍历整个字典,将所有键值对保存到 rdb 文件
            while((de = dictNext(di)) != NULL) {
                robj *key = dictGetKey(de);
                robj *val = dictGetVal(de);

                </span><span style="color:#ff0000;"><strong>// 保存 key</strong></span><span style="color:#333333;">
                if ((n = rdbSaveStringObject(rdb,key)) == -1) return -1;
                nwritten += n;

                </span><span style="color:#ff0000;"><strong>// 保存 value</strong></span><span style="color:#333333;">
                if ((n = rdbSaveStringObject(rdb,val)) == -1) return -1;
                nwritten += n;
            }
            dictReleaseIterator(di);

        } else {
            redisPanic("Unknown hash encoding");
        }

    } else {
        redisPanic("Unknown object type");
    }

    return nwritten;
}</span>

  快照保存

  我们接下来看看具体实现细节

  不管是触发条件满足后通过fork子进程来保存快照还是通过save命令来触发,其实都是调用的同一个函数rdbSave()。

  先来看看触发条件满足后通过fork子进程的实现保存快照的的实现

  在每100ms调用一次的serverCron函数中会对快照保存的条件进行检查,如果满足了则进行快照保存

  如果后端有写rdb的子进程或者写aof的子进程,则检查rdb子进程是否退出了,如果退出了则进行一些收尾处理,比如更新脏数据计数server.dirty和最近快照保存时间server.lastsave。

  如果后端没有写rdb的子进程且没有写aof的子进程,则判断下是否有触发写rdb的条件满足了,如果有条件满足,则通过调用rdbSaveBackground函数进行快照保存。

  跟着进rdbSaveBackground函数里边看看

  

<span style="color:#333333;">/*
 * 使用子进程保存数据库数据,不阻塞主进程
 */
int rdbSaveBackground(char *filename) {
    pid_t childpid;
    long long start;
     //已经在运行
    if (server.rdb_child_pid != -1) return REDIS_ERR;
    
    // 修改服务器状态
    server.dirty_before_bgsave = server.dirty;

    // 开始时间
    start = ustime();
    // 创建子进程
    if ((childpid = fork()) == 0) {
        int retval;

        /* Child */
        // 子进程不接收网络数据
        if (server.ipfd > 0) close(server.ipfd);
        if (server.sofd > 0) close(server.sofd);

        // 保存数据
       </span><strong><span style="color:#ff0000;"> retval = rdbSave(filename);</span></strong><span style="color:#333333;">
       // .......
        // 退出子进程
        exitFromChild((retval == REDIS_OK) ? 0 : 1);
    } else {
        /* Parent */
        // 记录最后一次 fork 的时间
        server.stat_fork_time = ustime()-start;

        // 创建子进程失败时进行错误报告
        if (childpid == -1) {..... }
        redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid);

        // 记录保存开始的时间
        server.rdb_save_time_start = time(NULL);
        // 记录子进程的 id
        server.rdb_child_pid = childpid;
        // 在执行时关闭对数据库的 rehash
        // 避免 copy-on-write
        updateDictResizePolicy();
        return REDIS_OK;
    }
    return REDIS_OK; /* unreached */
}</span>

  rdb的快照保存是通过函数rdbSave函数(rdb.c:394)来实现的。其实save命令也是通过调用这个函数来实现的。我们来简单看看

  rdb.c

/* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success */
/*
 * 将数据库保存到磁盘上。成功返回 REDIS_OK ,失败返回 REDIS_ERR 。
 */
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;

    // 以 "temp-<pid>.rdb" 格式创建临时文件名
    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;
    }

    // 初始化 rio 文件
    rioInitWithFile(&rdb,fp);
    // 如果有需要的话,设置校验和计算函数
    if (server.rdb_checksum)
        rdb.update_cksum = rioGenericUpdateChecksum;
    // 以 "REDIS <VERSION>" 格式写入文件头,以及 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;
        // 指向数据库 key space
        dict *d = db->dict;
        // 数据库为空, pass ,处理下个数据库
        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 */
        // 将数据库中的所有节点保存到 RDB 文件
        while((de = dictNext(di)) != NULL) {
            // 取出键
            sds keystr = dictGetKey(de);
            // 取出值
            robj key, 
                 *o = dictGetVal(de);
            long long expire;
            
            initStaticStringObject(key,keystr);
            // 取出过期时间
            expire = getExpire(db,&key);
            if (<span style="background-color: rgb(255, 0, 0);"><strong>rdbSaveKeyValuePair</strong></span>(&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);
    rioWrite(&rdb,&cksum,8);

    /* Make sure data will not remain on the OS's output buffers */
    fflush(fp);
    fsync(fileno(fp));
    fclose(fp);

    /* Use RENAME to make sure the DB file is changed atomically only
     * if the generate DB file is ok. */
    // 将临时文件 tmpfile 改名为 filename 
    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:
    fclose(fp);
    unlink(tmpfile);
    redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));
    if (di) dictReleaseIterator(di);
    return REDIS_ERR;
}


  创建并打开临时文件

  写入文件签名“REDIS”和版本号

  遍历所有db中的所有key

  对每个key,先判断是否设置了expireTime, 如果设置了,则保存expireTime到rdb文件中。

rdbSaveKeyValuePair函数保存没个键值对,这个函数在上面介绍过了

  不同类型有有不同的存储格式,详细见rdb文件格式

  最后写入rdb文件的结束符

  关闭文件并重命名临时文件名到正式文件名

  更新脏数据计数server.dirty为0和最近写rdb文件的时间server.lastsave为当前时间,这个只是在通过save命令触发的情况下有用。因为如果是通过fork一个子进程来写rdb文件的,更新无效,因为更新的是子进程的数据。

  如果是通过fork一个子进程来写rdb文件(即不是通过save命令触发的),在写rdb文件的过程中,可能又有一些数据被更改了,那此时的脏数据计数server.dirty怎么更新呢? redis是怎样处理的呢?

  我们来看看写rdb的子进程退出时的处理

  Redis.c(serverCron())

// 如果 BGSAVE 或者 BGREWRITEAOF 正在进行
    // 那么检查它们是否已经执行完毕
    if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {
        int statloc;
        pid_t pid;

        if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
            int exitcode = WEXITSTATUS(statloc);
            int bysignal = 0;
            
            if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);

            if (pid == server.rdb_child_pid) {
                backgroundSaveDoneHandler(exitcode,bysignal);
            } else if (pid == server.aof_child_pid) {
                backgroundRewriteDoneHandler(exitcode,bysignal);
            } else {
                redisLog(REDIS_WARNING,
                    "Warning, detected child with unmatched pid: %ld",
                    (long)pid);
            }
            // 如果 BGSAVE 和 BGREWRITEAOF 都已经完成,那么重新开始 REHASH
            updateDictResizePolicy();
        }
    } 

 如果捕捉到写rdb文件的子进程退出,则调用backgroundSaveDoneHandler进行处理

  接着看看backgroundSaveDoneHandler函数

  Rdb.c

<span style="color:#333333;">/* A background saving child (BGSAVE) terminated its work. Handle this. */
/*
 * 根据 BGSAVE 子进程的返回值,对服务器状态进行更新
 */
void backgroundSaveDoneHandler(int exitcode, int bysignal) {
    // 保存成功
    if (!bysignal && exitcode == 0) {
        redisLog(REDIS_NOTICE, "Background saving terminated with success");
       </span><strong><span style="color:#ff0000;"> server.dirty = server.dirty - server.dirty_before_bgsave;</span></strong><span style="color:#333333;">
        </span><strong><span style="color:#ff0000;">server.lastsave = time(NULL);</span></strong><span style="color:#333333;">
        server.lastbgsave_status = REDIS_OK;
    // 保存失败
    } else if (!bysignal && exitcode != 0) {
        redisLog(REDIS_WARNING, "Background saving error");
        server.lastbgsave_status = REDIS_ERR;
    // 子进程被终结
    } else {
        redisLog(REDIS_WARNING,
            "Background saving terminated by signal %d", bysignal);
        rdbRemoveTempFile(server.rdb_child_pid);
        server.lastbgsave_status = REDIS_ERR;
    }

    // 更新服务器状态
    server.rdb_child_pid = -1;
    server.rdb_save_time_last = time(NULL)-server.rdb_save_time_start;
    server.rdb_save_time_start = -1;

    /* Possibly there are slaves waiting for a BGSAVE in order to be served
     * (the first stage of SYNC is a bulk transfer of dump.rdb) */
    // 将 rdb 文件保存完毕的消息报告可能正在等待复制的附属节点
    updateSlavesWaitingBgsave(exitcode == 0 ? REDIS_OK : REDIS_ERR);
}</span>

  更新脏数据计数server.dirty和最近写rdb文件的时间server.lastsave为当前时间

       注意这里AOF的处理方式的不同,对于AOF,他是将快照期间的数据同时写入了一个buf和AOF日志文件,当rewrite结束后,将buf中的数据追加到AOF中就可以保证数据的完整,但是在RDB中,没有必要这样做,因为RDB本来就不需要保证数据的完全完整,所以只需要更新脏数据,将脏数据设置为这期间被修改的数量就可以了。

  快照导入

  当redis因为停电或者某些原因挂掉了,此时重启redis时,我们就需要从rdb文件中读取快照文件,把保存到rdb文件中的数据重新导入到内存中。

  先来看看启动时对快照导入的处理

  Redis.c

<span style="color:#333333;">// 从 RDB 文件或 AOF 文件中载入数据到内存
void loadDataFromDisk(void) {
    long long start = ustime();

    // 如果开启了 AOF 功能,那么优先使用 AOF 文件来还原数据
    if (server.aof_state == REDIS_AOF_ON) {
        if (</span><strong><span style="color:#ff0000;">loadAppendOnlyFile</span></strong><span style="color:#333333;">(server.aof_filename) == REDIS_OK)
            redisLog(REDIS_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000);
    } else {
        // 在没有开启 AOF 功能时,才使用 RDB 来还原
        if (</span><strong><span style="color:#ff0000;">rdbLoad</span></strong><span style="color:#333333;">(server.rdb_filename) == REDIS_OK) {
            redisLog(REDIS_NOTICE,"DB loaded from disk: %.3f seconds",
                (float)(ustime()-start)/1000000);
        } else if (errno != ENOENT) {
            redisLog(REDIS_WARNING,"Fatal error loading the DB. Exiting.");
            exit(1);
        }
    }
}</span>

  如果保存了AOF文件,则使用AOF文件来恢复数据,AOF的具体内容见AOF

  如果没有AOF,则使用rdb文件恢复数据,调用rdbLoad函数

  接着看看rdbLoad函数

  Rdb.c

<span style="color:#333333;">/*
 * 读取 rdb 文件,并将其中的对象保存到内存中
 */
int rdbLoad(char *filename) {
    uint32_t dbid;
    int type, rdbver;
    redisDb *db = server.db+0;
    char buf[1024];
    long long expiretime, now = mstime();
    long loops = 0;
    FILE *fp;
    rio rdb;

    // 打开文件
    fp = fopen(filename,"r");
    if (!fp) {
        errno = ENOENT;
        return REDIS_ERR;
    }

    // 初始化 rdb 文件
    rioInitWithFile(&rdb,fp);
    if (server.rdb_checksum)
        rdb.update_cksum = rioGenericUpdateChecksum;

    // 检查 rdb 文件头(“REDIS”字符串,以及版本号)
    if (rioRead(&rdb,buf,9) == 0) goto eoferr;
    buf[9] = '\0';
    if (memcmp(buf,"REDIS",5) != 0) {   // "REDIS"
        fclose(fp);
        redisLog(REDIS_WARNING,"Wrong signature trying to load DB from file");
        errno = EINVAL;
        return REDIS_ERR;
    }
    rdbver = atoi(buf+5);   // 版本号
    if (rdbver < 1 || rdbver > REDIS_RDB_VERSION) {
        fclose(fp);
        redisLog(REDIS_WARNING,"Can't handle RDB format version %d",rdbver);
        errno = EINVAL;
        return REDIS_ERR;
    }

    startLoading(fp);
    while(1) {
        robj *key, *val;
        expiretime = -1;

        /* Serve the clients from time to time */
        // 间隔性服务客户端
        if (!(loops++ % 1000)) {
            // 刷新载入进程信息
            loadingProgress(rioTell(&rdb));
            // 处理事件
            aeProcessEvents(server.el, AE_FILE_EVENTS|AE_DONT_WAIT);
        }

        /* Read type. */
        // 读入类型标识符
        if ((type = rdbLoadType(&rdb)) == -1) goto eoferr;

        // 接下来的值是一个过期时间
        if (type == REDIS_RDB_OPCODE_EXPIRETIME) {
            // 读取毫秒计数的过期时间
            if ((expiretime = rdbLoadTime(&rdb)) == -1) goto eoferr;
            /* We read the time so we need to read the object type again. */
            // 读取下一个值(一个字符串 key )的类型标识符
            if ((type = rdbLoadType(&rdb)) == -1) goto eoferr;
            /* the EXPIRETIME opcode specifies time in seconds, so convert
             * into milliesconds. */
             // 将毫秒转换为秒
            expiretime *= 1000;
        } else if (type == REDIS_RDB_OPCODE_EXPIRETIME_MS) {
            /* Milliseconds precision expire times introduced with RDB
             * version 3. */
            // 读取毫秒计数的过期时间
            if ((expiretime = rdbLoadMillisecondTime(&rdb)) == -1) goto eoferr;
            /* We read the time so we need to read the object type again. */
            // 读取下一个值(一个字符串 key )的类型标识符
            if ((type = rdbLoadType(&rdb)) == -1) goto eoferr;
        }
    
        // 到达 EOF ,跳出
        if (type == REDIS_RDB_OPCODE_EOF)
            break;

        /* Handle SELECT DB opcode as a special case */
        // 数据库号码标识符
        if (type == REDIS_RDB_OPCODE_SELECTDB) {
            // 读取数据库号
            if ((dbid = rdbLoadLen(&rdb,NULL)) == REDIS_RDB_LENERR)
                goto eoferr;
            // 检查数据库号是否合法
            if (dbid >= (unsigned)server.dbnum) {
                redisLog(REDIS_WARNING,"FATAL: Data file was created with a Redis server configured to handle more than %d databases. Exiting\n", server.dbnum);
                exit(1);
            }
            db = server.db+dbid;
            continue;
        }

        /* Read key */
        // 读入 key
        if ((key = </span><strong><span style="color:#ff0000;">rdbLoadStringObject(&rdb)</span></strong><span style="color:#333333;">) == NULL) goto eoferr;

        /* Read value */
        // 读入 value
        if ((val = </span><strong><span style="color:#ff0000;">rdbLoadObject(type,&rdb)</span></strong><span style="color:#333333;">) == NULL) goto eoferr;

        /* Check if the key already expired. This function is used when loading
         * an RDB file from disk, either at startup, or when an RDB was
         * received from the master. In the latter case, the master is
         * responsible for key expiry. If we would expire keys here, the
         * snapshot taken by the master may not be reflected on the slave. */
        // 如果 key 已经过期,那么释放 key 和 value
        if (server.masterhost == NULL && expiretime != -1 && expiretime < now) {
            decrRefCount(key);
            decrRefCount(val);
            continue;
        }

        /* Add the new object in the hash table */
        // 将对象添加到数据库
        </span><strong><span style="color:#ff0000;">dbAdd(db,key,val);</span></strong><span style="color:#333333;">

        /* Set the expire time if needed */
        // 如果有过期时间,设置过期时间
        if (expiretime != -1) setExpire(db,key,expiretime);

        decrRefCount(key);
    }

    /* Verify the checksum if RDB version is >= 5 */
    // 检查校验和
    if (rdbver >= 5 && server.rdb_checksum) {
        uint64_t cksum, expected = rdb.cksum;

        if (rioRead(&rdb,&cksum,8) == 0) goto eoferr;
        memrev64ifbe(&cksum);
        if (cksum == 0) {
            redisLog(REDIS_WARNING,"RDB file was saved with checksum disabled: no check performed.");
        } else if (cksum != expected) {
            redisLog(REDIS_WARNING,"Wrong RDB checksum. Aborting now.");
            exit(1);
        }
    }

    fclose(fp);
    stopLoading();
    return REDIS_OK;

eoferr: /* unexpected end of file is handled here with a fatal exit */
    redisLog(REDIS_WARNING,"Short read or OOM loading DB. Unrecoverable error, aborting now.");
    exit(1);
    return REDIS_ERR; /* Just to avoid warning */
}</span>

  打开rdb文件

  读取rdb文件的签名和版本号

  开始进入 类型 | 值 | 类型 | 值 的循环读取,可参考rdb文件格式

  作者还做了导入的进度条,是有人反馈说rdb文件很大时导入时要很久,但又不知道进度,所以作者就加了导入的进度条,改善用户体验

  读取类型

  如果类型是过期时间类型REDIS_EXPIRETIME,则读取过期时间

  如果类型是文件结束类型REDIS_EOF,则跳出 类型 | 值 | 类型 | 值 的循环读取

  如果类型是选择db类型REDIS_SELECTDB,则读取db索引并把当前db转成该db,然后继续 类型 | 值 | 类型 | 值 的循环读取。

  如果不是以上类型,则表明该类型是数据类型,读取作为key的字符串,即读取字符串类型的值,然后接着读取作为value的字符串。不同类型的编码不一样,根据写入时得规则解释读取到的值即可

  读取到key和value后,判断下该key是否过期,如果过期则丢弃,不再导入,然后继续 类型 | 值 | 类型 | 值 的循环读取。

  如果读取成功,则导入到内存,如果有过期时间则设置过期时间

  总结

  落地存储是数据设计的一大重点也是难点。原理很简单,定义某种协议,然后按照某种协议写入读出。Redis为了节省空间和读写时的I/O操作,做了很多很细致的工作来压缩数据。另外redis的丰富的数据类型也加大了落地的实现难度。作者也曾经在他的博客说过,redis的丰富的数据类型导致了很多经典的优化办法无法在redis上实现。

参考:

http://www.searchdatabase.com.cn/showcontent_62814.htm

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值