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 部分内容:
- key 过期时间,可选信息
- 一个字节的 value type
- key 的字符串编码,编码格式见 Redis String Encoding
- 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 在数据持久性方面很容易丢失数据。
总结
- rdb 是一个保存数据库快照的机制
- redis 可以通过 save(阻塞)和 bgsave(非阻塞)地保存数据库快照
- rdb 粒度太粗,还不足以很好的解决数据持久化问题
- 我很不喜欢 rdb load 部分代码,个人认为写的很乱,在后面的版本好像好了很多,笔者看的是 3.0