redis 源码系列(14):1,2,3茄子 --- 数据库快照 rdb

Redis 是一个内存数据库,但是通过使用 rdb 和 aof 机制,redis 也支持数据持久化。今天我们来看一下 rdb 相关代码。

redis 的 *.rdb 文件是对 redis 内存数据的二进制表示(快照),通过 *.rdb 文件,我们可以恢复 redis 的所有状态。

rdb

Rdb Format

我们来看一下一个 rdb 文件的格式,大家千万注意,虽然我们在下面的例子中将 rdb 文件的内容分割成了多个部分,但是一定要记住,rdb 底层就是一个连续的字节流表示,并不存在任何显示的划分

----------------------------# Rdb 文件是二进制表示,是紧凑的,不会有类似空格等符号出现
52 45 44 49 53              # Magic String "REDIS"
30 30 30 37                 # 版本号,ASCCI 字符串,此处为 "0007"
----------------------------
FE 00                       # FE 代表后面是 database 段,00 代表 database 号
----------------------------# 从此开始后面每个都是一个 key->value 对,我们会展示几种不同的格式
FD $unsigned int            # FD 代表过期时间是以秒为单位,后面跟着一个 4bytes 的非负整数表示过期时间
$value-type                 # value type,一字节,表示 value 的类型
$string-encoded-key         # key,string 编码
$encoded-value              # value 的编码结果,不同 value type 的编码方式不同
----------------------------
FC $unsigned long           # FC 代表过期时间是以ms为单位,后面跟着一个 8bytes 非负整数代表过期时间,后面已经解释过的字段不再重复
$value-type                
$string-encoded-key         
$encoded-value             
----------------------------
$value-type                 # 这个 key 不会过期,value type 保证不等于 FD, FC, FE and FF
$string-encoded-key
$encoded-value
----------------------------
FE $length-encoding         # 新 database 开始,databaesnumber 由 $length-encoding 读取
----------------------------
...                         # 新 databaes 的 key->value 对
                            
FF                          ## FF 表示文件结束
8 byte checksum             ## 文件的 CRC64 checksum 

Magic Number

rdb 文件开始是固定的 magic number 是 “REDIS” 字符串。通过检查 magic number 我们可以快速确定这个是不是一个 rdb 文件。

Rdb Version Number

接下来的 4 个字节是 rdb 文件格式的版本号的字符串表示。

Database Selector

之前介绍 database 的文章已经说过 redis 内部有多个 database。FE 开头代表后面的内容表示的是一个 database,其 database number 按照 length-encoding 编码。

Key Value Pairs

database 段开始之后就是若干个 kv 对,每个 kv 对包含 4 部分内容:

  1. key 过期时间,可选信息
  2. 一个字节的 value type
  3. key 的字符串编码,编码格式见 Redis String Encoding
  4. value,根据 value type 进行编码,见 Redis Value Encoding

Rdb load

下面我们来结合代码,看一下具体类型的 encoding 逻辑。rdb 在 redis 中使用 rio 类型表示:

struct _rio {
    // 具体实现的函数指针
    size_t (*read)(struct _rio *, void *buf, size_t len);
    size_t (*write)(struct _rio *, const void *buf, size_t len);
    off_t (*tell)(struct _rio *);
    // 校验和计算函数,每次有写入/读取新数据时都要计算一次
    void (*update_cksum)(struct _rio *, const void *buf, size_t len);
    // 当前校验和
    uint64_t cksum;
		// 已经读写的字节数
    size_t processed_bytes;
    // 单次读写的最大字节数
  	size_t max_processing_chunk;
    // 具体实现的底层数据,rio 底层可能是一个内存字符串,或者是一个文件
    union {
        struct {
            // 缓存指针
            sds ptr;
            // 偏移量
            off_t pos;
        } buffer;
        struct {
            // 被打开文件的指针
            FILE *fp;
            // 最近一次 fsync() 以来,写入的字节量
            off_t buffered; /* Bytes written since last fsync. */
            // 写入多少字节之后,才会自动执行一次 fsync()
            off_t autosync; /* fsync after 'autosync' bytes written. */
        } file;
    } io;
};
typedef struct _rio rio;

redis 将 rio 抽象成一个字节流对象,其底层可以构建在一个字符串上,或者文件上,这个方式也算是用 c 实现面向对象编程的一个典型实现方式了。

看一下 rio 的构造代码:

static const rio rioBufferIO = {
    // 读函数
    rioBufferRead,
    // 写函数
    rioBufferWrite,
    // 偏移量函数
    rioBufferTell,
    NULL,           /* update_checksum */
    0,              /* current checksum */
    0,              /* bytes read or written */
    0,              /* read/write chunk size */
    { { NULL, 0 } } /* union for io-specific vars */
};

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

void rioInitWithFile(rio *r, FILE *fp) {
    *r = rioFileIO;
    r->io.file.fp = fp;
    r->io.file.buffered = 0;
    r->io.file.autosync = 0;
}

void rioInitWithBuffer(rio *r, sds s) {
    *r = rioBufferIO;
    r->io.buffer.ptr = s;
    r->io.buffer.pos = 0;
}

rio 的初始化代码很有意思,定义了两个模块内常量对象,然后初始化的时候,分别用这两个对象对 rio 进行复制赋值。

读写相关 API:

// 向 rio 写入数据
static inline size_t rioWrite(rio *r, const void *buf, size_t len) {
    while (len) {
        // 单次写入不可以超过 max_processing_chunk
        size_t bytes_to_write = (r->max_processing_chunk && r->max_processing_chunk < len) ? r->max_processing_chunk : len;
        // 如果需要的话更新 checksum 
      	if (r->update_cksum) r->update_cksum(r,buf,bytes_to_write);
        // 写入返回 0 代表出错,返回 0
        if (r->write(r,buf,bytes_to_write) == 0)
            return 0;
        // 写入成功,更新指针和 rio 状态
        buf = (char*)buf + bytes_to_write;
        len -= bytes_to_write;
        r->processed_bytes += bytes_to_write;
    }
    return 1;
}

// 从 rio 读取数据,与写入逻辑基本一样,不再啰嗦
static inline size_t rioRead(rio *r, void *buf, size_t len) {
    while (len) {
        size_t bytes_to_read = (r->max_processing_chunk && r->max_processing_chunk < len) ? r->max_processing_chunk : len;
      	if (r->read(r,buf,bytes_to_read) == 0)
            return 0;
        if (r->update_cksum) r->update_cksum(r,buf,bytes_to_read);
        buf = (char*)buf + bytes_to_read;
        len -= bytes_to_read;
        r->processed_bytes += bytes_to_read;
    }
    return 1;
}
// 转调用底层实现的 tell 
static inline off_t rioTell(rio *r) {
    return r->tell(r);
}

rio 底层实现的函数就是对 sds 和 FILE * 的操作,并没有什么值得细讲的地方,我们就不赘述了。

来看一下 rdb 的加载代码:

#define RDB_OPCODE_MODULE_AUX 247   /* Module auxiliary data. */
#define RDB_OPCODE_IDLE       248   /* LRU idle time. */
#define RDB_OPCODE_FREQ       249   /* LFU frequency. */
#define RDB_OPCODE_AUX        250   /* RDB aux field. */
#define RDB_OPCODE_RESIZEDB   251   /* Hash table resize hint. */
#define RDB_OPCODE_EXPIRETIME_MS 252    /* Expire time in milliseconds. */
#define RDB_OPCODE_EXPIRETIME 253       /* Old expire time in seconds. */
#define RDB_OPCODE_SELECTDB   254   /* DB number of the following keys. */
#define RDB_OPCODE_EOF        255   /* End of the RDB file. */

#define REDIS_RDB_6BITLEN 0
#define REDIS_RDB_14BITLEN 1
#define REDIS_RDB_32BITLEN 2
#define REDIS_RDB_ENCVAL 3

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

    if (len < (1<<6)) {
				/*
						小于 2^6 ,编码为:
size        2bits|  6bits | 
            +---------+----
component   | 00 | len    |
            +---------+----
            占用 1byte
				*/
        buf[0] = (len&0xFF)|(REDIS_RDB_6BITLEN<<6);
        if (rdbWriteRaw(rdb,buf,1) == -1) return -1;
        nwritten = 1;
    } else if (len < (1<<14)) {
        /*
						小于 2^14,编码为:
size        2bits|  6bits | 8bits. |
            +---------+-------------
component   | 01 | len1   | len2.  |
            +---------+-------------
            占用 2bytes
				*/
        buf[0] = ((len>>8)&0xFF)|(REDIS_RDB_14BITLEN<<6);
        buf[1] = len&0xFF;
        if (rdbWriteRaw(rdb,buf,2) == -1) return -1;
        nwritten = 2;

    } else {
        /*
						编码为:
size        2bits|  6bits | 4bytes       |
            +---------+------------------
component   | 01 | 无用   |  len          |
            +---------+------------------
            占用 5 bytes
				*/        
        buf[0] = (REDIS_RDB_32BITLEN<<6);
        if (rdbWriteRaw(rdb,buf,1) == -1) return -1;
        len = htonl(len);
        if (rdbWriteRaw(rdb,&len,4) == -1) return -1;
        nwritten = 1+4;
    }

    return nwritten;
}
// 从 rdb 读取 length
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) {
        // length 被额外编码,返回编码值
        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);
    }
}

int rdbLoad(char *filename) {
    uint32_t dbid;
    int type, rdbver;
    redisDb *db = server.db+0;
    char buf[1024];
    long long expiretime, now = mstime();
    FILE *fp;
    rio rdb;
    // 打开 rdb 文件
    if ((fp = fopen(filename,"r")) == NULL) return REDIS_ERR;
    // 初始化写入流
    rioInitWithFile(&rdb,fp);
    rdb.update_cksum = rdbLoadProgressCallback;
    rdb.max_processing_chunk = server.loading_process_events_interval_bytes;
    // 先读取 magic number 和 版本号
  	if (rioRead(&rdb,buf,9) == 0) goto eoferr;
    buf[9] = '\0';

    // 检查 magic number
    if (memcmp(buf,"REDIS",5) != 0) {
        fclose(fp);
        redisLog(REDIS_WARNING,"Wrong signature trying to load DB from file");
        errno = EINVAL;
        return REDIS_ERR;
    }
    // 检查 rdb format version,rdb format 向前兼容
    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);
    // 加载解析 rdb 文件
    while(1) {
        robj *key, *val;
        expiretime = -1;
				// 读取 type,表示后续的数据是什么类型
        if ((type = rdbLoadType(&rdb)) == -1) goto eoferr;
				// 根据不同 type 解析数据
        if (type == REDIS_RDB_OPCODE_EXPIRETIME) {
						// 以 s 为单位的过期信息。读取 expire time
            if ((expiretime = rdbLoadTime(&rdb)) == -1) goto eoferr;
  					// 后续必然是跟着 kv 信息,读取 k 的类型
            if ((type = rdbLoadType(&rdb)) == -1) goto eoferr;
						// expiretime 是 ms 为单位的,需要乘以 1000
            expiretime *= 1000;
        } else if (type == REDIS_RDB_OPCODE_EXPIRETIME_MS) {
            // 以毫秒计算的过期时间
            if ((expiretime = rdbLoadMillisecondTime(&rdb)) == -1) goto eoferr;
            if ((type = rdbLoadType(&rdb)) == -1) goto eoferr;
        }
        // rdb 文件结束
        if (type == REDIS_RDB_OPCODE_EOF)
            break;
        // 后续数据是一个 database
        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;
            // 跳过,继续解析,因为后面的代码不是 else if
            continue;
        }
        // 到这里的话,应该是开始解析 kv 对了,首先是 key
        if ((key = rdbLoadStringObject(&rdb)) == NULL) goto eoferr;
				// 读取 value
        if ((val = rdbLoadObject(type,&rdb)) == NULL) goto eoferr;
        // 如果是 master 节点,而且这个键已经过期,不需要添加到 database 中
        // 如果是 slave 则等待后续传播的 aof 指令即可,master 会来删除
        if (server.masterhost == NULL && expiretime != -1 && expiretime < now) {
            decrRefCount(key);
            decrRefCount(val);
            // 跳过
            continue;
        }
        // 添加到数据库中
        dbAdd(db,key,val);
				// 如果有过期信息,设置过期时间
        if (expiretime != -1) setExpire(db,key,expiretime);
        decrRefCount(key);
    }
		// 如果 rdb format version 大于 5 ,校验文件的 check
    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);
        }
    }
    // 关闭 RDB 
    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 */
}

加载字符串:

robj *rdbLoadStringObject(rio *rdb) {
    return rdbGenericLoadStringObject(rdb,0);
}

robj *rdbLoadEncodedStringObject(rio *rdb) {
    return rdbGenericLoadStringObject(rdb,1);
}

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);
        // LZF 压缩
        case REDIS_RDB_ENC_LZF:
            return rdbLoadLzfStringObject(rdb);
        default:
            redisPanic("Unknown RDB encoding type");
        }
    }
    if (len == REDIS_RDB_LENERR) return NULL;
    // 执行到这里,说明这个字符串即没有被压缩,也不是整数,直接从加载 rdb 后续字节
    val = sdsnewlen(NULL,len);
    if (len && rioRead(rdb,val,len) == 0) {
        sdsfree(val);
        return NULL;
    }
    return createObject(REDIS_STRING,val);
}

加载 value 对象:

#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

#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

robj *rdbLoadObject(int rdbtype, rio *rdb) {
    robj *o, *ele, *dec;
    size_t len;
    unsigned int i;

    if (rdbtype == REDIS_RDB_TYPE_STRING) {
        // 载入字符串对象
        if ((o = rdbLoadEncodedStringObject(rdb)) == NULL) return NULL;
        o = tryObjectEncoding(o);
    } else if (rdbtype == REDIS_RDB_TYPE_LIST) {
				// 加载 list 对象,首先读取 list 的节点数量
        if ((len = rdbLoadLen(rdb,NULL)) == REDIS_RDB_LENERR) return NULL;
				// 如果大于 ziplist 的限定节点数,则使用一般的双向链表,否则用 ziplist
        if (len > server.list_max_ziplist_entries) {
            o = createListObject();
        } else {
            o = createZiplistObject();
        }
				// 加载所有节点
        while(len--) {
            // 载入字符串对象
            if ((ele = rdbLoadEncodedStringObject(rdb)) == NULL) return NULL;
					  // 检查是否需要将 ziplist 节点转为一般双向链表节点
            if (o->encoding == REDIS_ENCODING_ZIPLIST &&
                sdsEncodedObject(ele) &&
                sdslen(ele->ptr) > server.list_max_ziplist_value)
                    listTypeConvert(o,REDIS_ENCODING_LINKEDLIST);
            
          	if (o->encoding == REDIS_ENCODING_ZIPLIST) {
                // 节点插入到 ziplist 末尾
                dec = getDecodedObject(ele);
                o->ptr = ziplistPush(o->ptr,dec->ptr,sdslen(dec->ptr),REDIS_TAIL);
                decrRefCount(dec);
                decrRefCount(ele);
            } else {
                // 将新列表项推入到链表的末尾
                ele = tryObjectEncoding(ele);
                listAddNodeTail(o->ptr,ele);
            }
        }
    } else if (rdbtype == REDIS_RDB_TYPE_SET) {
				// 加载集合对象,先加载节点个数
        if ((len = rdbLoadLen(rdb,NULL)) == REDIS_RDB_LENERR) return NULL;
				// 根据节点个数选择编码方式,超过 set_max_intset_entries 则使用 hashtable
        if (len > server.set_max_intset_entries) {
            o = createSetObject();
					  // 既然已经知道元素数量,我们直接手动扩容,这样比插入过程中动态扩容效率更高
            if (len > DICT_HT_INITIAL_SIZE)
                dictExpand(o->ptr,len);
        } else {
            o = createIntsetObject();
        }
				// 加载所有元素
        for (i = 0; i < len; i++) {
            long long llval;
            // 载入元素
            if ((ele = rdbLoadEncodedStringObject(rdb)) == NULL) return NULL;
            ele = tryObjectEncoding(ele);
            // 将元素添加到 INTSET 集合,并在有需要的时候,转换编码为 HT
            if (o->encoding == REDIS_ENCODING_INTSET) {
                /* Fetch integer value from element */
                if (isObjectRepresentableAsLongLong(ele,&llval) == REDIS_OK) {
                    o->ptr = intsetAdd(o->ptr,llval,NULL);
                } else {
                    setTypeConvert(o,REDIS_ENCODING_HT);
                    dictExpand(o->ptr,len);
                }
            }
						// 添加元素,注意如果是用 intset 表示,在上面的判断中已经加入到 intset 了
            if (o->encoding == REDIS_ENCODING_HT) {
                dictAdd((dict*)o->ptr,ele,NULL);
            } else {
                decrRefCount(ele);
            }
        }
    } else if (rdbtype == REDIS_RDB_TYPE_ZSET) {
    		// 载入有序集合对象
        size_t zsetlen;
        size_t maxelelen = 0;
        zset *zs;
        // 载入有序集合的元素数量
        if ((zsetlen = rdbLoadLen(rdb,NULL)) == REDIS_RDB_LENERR) return NULL;
        // 创建有序集合
        o = createZsetObject();
        zs = o->ptr;
        // 加载所有元素
        while(zsetlen--) {
            robj *ele;
            double score;
            zskiplistNode *znode;
            // 载入元素成员
            if ((ele = rdbLoadEncodedStringObject(rdb)) == NULL) return NULL;
            ele = tryObjectEncoding(ele);
            // 载入元素分值
            if (rdbLoadDoubleValue(rdb,&score) == -1) return NULL;
            // 记录成员的最大长度
            if (sdsEncodedObject(ele) && sdslen(ele->ptr) > maxelelen)
                maxelelen = sdslen(ele->ptr);
            // 将元素插入到跳跃表中
            znode = zslInsert(zs->zsl,score,ele);
            // 将元素关联到字典中
            dictAdd(zs->dict,ele,&znode->score);
            incrRefCount(ele); /* added to skiplist */
        }
				// 如果可以的话,将 zset 转为 ziplist 表示
        if (zsetLength(o) <= server.zset_max_ziplist_entries &&
            maxelelen <= server.zset_max_ziplist_value)
                zsetConvert(o,REDIS_ENCODING_ZIPLIST);
    } else if (rdbtype == REDIS_RDB_TYPE_HASH) {
        // 载入哈希表对象
        size_t len;
        int ret;
        // 载入哈希表节点数量
        len = rdbLoadLen(rdb, NULL);
        if (len == REDIS_RDB_LENERR) return NULL;
        // 创建哈希表,默认为 ziplist 编码
        o = createHashObject();
				// 如果大于 hash_max_ziplist_entries 将 hash object 转为 dict 实现
        if (len > server.hash_max_ziplist_entries)
            hashTypeConvert(o, REDIS_ENCODING_HT);
				// 加载 ziplist 节点
        while (o->encoding == REDIS_ENCODING_ZIPLIST && len > 0) {
            robj *field, *value;
            len--;
            // 载入域(一个字符串)
            field = rdbLoadStringObject(rdb);
            if (field == NULL) return NULL;
            // 载入值(一个字符串)
            redisAssert(sdsEncodedObject(field));
            value = rdbLoadStringObject(rdb);
            if (value == NULL) return NULL;
            redisAssert(sdsEncodedObject(value));
   					// 向 ziplist 推入 key 和 value,key 与 value 相邻
            o->ptr = ziplistPush(o->ptr, field->ptr, sdslen(field->ptr), ZIPLIST_TAIL);
            o->ptr = ziplistPush(o->ptr, value->ptr, sdslen(value->ptr), ZIPLIST_TAIL);
						// 如果元素过多或者元素的长度超限,使用 dict 编码
            if (sdslen(field->ptr) > server.hash_max_ziplist_value ||
                sdslen(value->ptr) > server.hash_max_ziplist_value)
            {
                decrRefCount(field);
                decrRefCount(value);
                hashTypeConvert(o, REDIS_ENCODING_HT);
                break;
            }
            decrRefCount(field);
            decrRefCount(value);
        }
				// 加载到 hashtable
        while (o->encoding == REDIS_ENCODING_HT && len > 0) {
            robj *field, *value;

            len--;
            // 域和值都载入为字符串对象
            field = rdbLoadEncodedStringObject(rdb);
            if (field == NULL) return NULL;
            value = rdbLoadEncodedStringObject(rdb);
            if (value == NULL) return NULL;
            // 尝试编码
            field = tryObjectEncoding(field);
            value = tryObjectEncoding(value);
       			// 添加到底层 dict 中
            ret = dictAdd((dict*)o->ptr, field, value);
            redisAssert(ret == REDIS_OK);
        }
        redisAssert(len == 0);
    } else if (rdbtype == REDIS_RDB_TYPE_HASH_ZIPMAP  ||
               rdbtype == REDIS_RDB_TYPE_LIST_ZIPLIST ||
               rdbtype == REDIS_RDB_TYPE_SET_INTSET   ||
               rdbtype == REDIS_RDB_TYPE_ZSET_ZIPLIST ||
               rdbtype == REDIS_RDB_TYPE_HASH_ZIPLIST)
    {
        // 压缩编码对象。aux 底层数据即为 object 数据
        robj *aux = rdbLoadStringObject(rdb);
        if (aux == NULL) return NULL;
        // 这里的 string 类型没有关系,因为后续会将其修正为指定编码
        o = createObject(REDIS_STRING,NULL); /* string is just placeholder */
        o->ptr = zmalloc(sdslen(aux->ptr));
        // 复制底层数据,后续的判断只是修正 object 的 type 和 encoding,不知道为什么不直接
        // 复用 aux 的数据,一定要拷贝呢
        memcpy(o->ptr,aux->ptr,sdslen(aux->ptr));
        decrRefCount(aux);
				
        switch(rdbtype) {
            case REDIS_RDB_TYPE_HASH_ZIPMAP:
                /* Convert to ziplist encoded hash. This must be deprecated
                 * when loading dumps created by Redis 2.4 gets deprecated. */
                {
                    // 创建 ZIPLIST
                    unsigned char *zl = ziplistNew();
                    unsigned char *zi = zipmapRewind(o->ptr);
                    unsigned char *fstr, *vstr;
                    unsigned int flen, vlen;
                    unsigned int maxlen = 0;
                    // 从 2.6 开始, HASH 不再使用 ZIPMAP 来进行编码
                    // 所以遇到 ZIPMAP 编码的值时,要将它转换为 ZIPLIST
                    // 从字符串中取出 ZIPMAP 的域和值,然后推入到 ZIPLIST 中
                    while ((zi = zipmapNext(zi, &fstr, &flen, &vstr, &vlen)) != NULL) {
                        if (flen > maxlen) maxlen = flen;
                        if (vlen > maxlen) maxlen = vlen;
                        zl = ziplistPush(zl, fstr, flen, ZIPLIST_TAIL);
                        zl = ziplistPush(zl, vstr, vlen, ZIPLIST_TAIL);
                    }
                    zfree(o->ptr);
                    // 设置类型、编码和值指针
                    o->ptr = zl;
                    o->type = REDIS_HASH;
                    o->encoding = REDIS_ENCODING_ZIPLIST;
                    // 是否需要从 ZIPLIST 编码转换为 HT 编码
                    if (hashTypeLength(o) > server.hash_max_ziplist_entries ||
                        maxlen > server.hash_max_ziplist_value)
                    {
                        hashTypeConvert(o, REDIS_ENCODING_HT);
                    }
                }
                break;

            // ZIPLIST 编码的列表
            case REDIS_RDB_TYPE_LIST_ZIPLIST:
                o->type = REDIS_LIST;
                o->encoding = REDIS_ENCODING_ZIPLIST;
                // 检查是否需要转换编码
                if (ziplistLen(o->ptr) > server.list_max_ziplist_entries)
                    listTypeConvert(o,REDIS_ENCODING_LINKEDLIST);
                break;
            // INTSET 编码的集合
            case REDIS_RDB_TYPE_SET_INTSET:

                o->type = REDIS_SET;
                o->encoding = REDIS_ENCODING_INTSET;

                // 检查是否需要转换编码
                if (intsetLen(o->ptr) > server.set_max_intset_entries)
                    setTypeConvert(o,REDIS_ENCODING_HT);
                break;

            // ZIPLIST 编码的有序集合
            case REDIS_RDB_TYPE_ZSET_ZIPLIST:

                o->type = REDIS_ZSET;
                o->encoding = REDIS_ENCODING_ZIPLIST;

                // 检查是否需要转换编码
                if (zsetLength(o) > server.zset_max_ziplist_entries)
                    zsetConvert(o,REDIS_ENCODING_SKIPLIST);
                break;

            // ZIPLIST 编码的 HASH
            case REDIS_RDB_TYPE_HASH_ZIPLIST:

                o->type = REDIS_HASH;
                o->encoding = REDIS_ENCODING_ZIPLIST;

                // 检查是否需要转换编码
                if (hashTypeLength(o) > server.hash_max_ziplist_entries)
                    hashTypeConvert(o, REDIS_ENCODING_HT);
                break;

            default:
                redisPanic("Unknown encoding");
                break;
        }

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

    return o;
}

至此我们就基本介绍完了 rdb 的加载过程,至于 rdb 的存储过程,我们就不细讲了,其实就是加载的反操作。大家可以自行查看相关代码。

SAVE & BGSAVE

Redis 提供了 save (阻塞)和 bgsave(非阻塞) 命令来 dump rdb 文件:

void saveCommand(redisClient *c) {
    // BGSAVE 已经在执行中,不能再执行 SAVE
    if (server.rdb_child_pid != -1) {
        addReplyError(c,"Background save already in progress");
        return;
    }
    // 执行 
    if (rdbSave(server.rdb_filename) == REDIS_OK) {
        addReply(c,shared.ok);
    } else {
        addReply(c,shared.err);
    }
}

void bgsaveCommand(redisClient *c) {
    // 不能重复执行 BGSAVE
    if (server.rdb_child_pid != -1) {
        addReplyError(c,"Background save already in progress");
    // 不能在 BGREWRITEAOF 正在运行时执行
    } else if (server.aof_child_pid != -1) {
        addReplyError(c,"Can't BGSAVE while AOF log rewriting is in progress");
    } else if (rdbSaveBackground(server.rdb_filename) == REDIS_OK) {
        // 执行 BGSAVE
        addReplyStatus(c,"Background saving started");
    } else {
        addReply(c,shared.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;
    // 创建临时文件
    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;
        }
				// 开始写入 database
        if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr;
        if (rdbSaveLen(&rdb,j) == -1) goto werr;
				// 写入每个 kv 对
        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
    if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;
		// 写入 checksum
    cksum = rdb.cksum;
    memrev64ifbe(&cksum);
    rioWrite(&rdb,&cksum,8);
		// 手动 flush,确保数据落盘
    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1) goto werr;
    if (fclose(fp) == EOF) goto werr;
		// 将文件名字修改为正在使用的 rdb 的文件名,这个操作保证任何时候,我们的 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;
}

int rdbSaveBackground(char *filename) {
    pid_t childpid;
    long long start;
    // 如果 BGSAVE 已经在执行,那么出错
    if (server.rdb_child_pid != -1) return REDIS_ERR;
    // 记录 BGSAVE 执行前的数据库被修改次数,因为这个
    server.dirty_before_bgsave = server.dirty;
    // 最近一次尝试执行 BGSAVE 的时间
    server.lastbgsave_try = time(NULL);
    // fork() 开始前的时间,记录 fork() 返回耗时用
    start = ustime();
    if ((childpid = fork()) == 0) {
        int retval;
        // 子进程,关闭网络连接 fd
        closeListeningSockets(0);
        // 设置进程的标题,方便识别
        redisSetProcTitle("redis-rdb-bgsave");
        // 执行保存操作
        retval = rdbSave(filename);
        // 打印 copy-on-write 时使用的内存数
        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 {
        // 父进程,计算 fork() 执行的时间
        server.stat_fork_time = ustime()-start;
        // 如果 fork() 出错,那么报告错误
        if (childpid == -1) {
            server.lastbgsave_status = REDIS_ERR;
            redisLog(REDIS_WARNING,"Can't save in background: fork: %s",
                strerror(errno));
            return REDIS_ERR;
        }
        // 打印 BGSAVE 开始的日志
        redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid);
        // 记录数据库开始 BGSAVE 的时间
        server.rdb_save_time_start = time(NULL);
        // 记录负责执行 BGSAVE 的子进程 ID
        server.rdb_child_pid = childpid;
        // 关闭自动 rehash,尽量避免触发 copy-on-write
        updateDictResizePolicy();
        return REDIS_OK;
    }
    return REDIS_OK; /* unreached */
}

在生产环境中,一般不太可能执行 save 命令,因为这会导致 redis 服务在很长时间内无法响应其他客户端。

有了 rdb 文件,我们在启动数据库的时候,就可以根据 rdb 文件恢复 redis 内的数据啦。但是 rdb 本身粒度太大,是整个数据库的快照,所以 dump rdb 的操作往往只能在业务低峰期进行,这就导致了 rdb 在数据持久性方面很容易丢失数据。

总结

  1. rdb 是一个保存数据库快照的机制
  2. redis 可以通过 save(阻塞)和 bgsave(非阻塞)地保存数据库快照
  3. rdb 粒度太粗,还不足以很好的解决数据持久化问题
  4. 我很不喜欢 rdb load 部分代码,个人认为写的很乱,在后面的版本好像好了很多,笔者看的是 3.0
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值