众所周知,Reids是一个高效的内存数据库,所有的数据都存放在内存中。这种模式的缺点就是一旦服务器关闭后会立刻丢失所有存储的数据,Redis当然要避免这种情况的发生,于是其提供了两种持久化机制:RDB和AOF。它们的功能都是将内存中存放的数据保存到磁盘文件上,等到服务器下次开启时能重载数据,以免数据丢失。今天,我们先来剖析一下RDB持久化机制。
RDB概述
看过我系列博客的应该知道我分析源码的方式是,先学会使用它,再来一步一步的深入它。我先演示一个小例子来感受一下RDB持久化。
1
2
3
4
5
6
7
|
127.0
.0
.1:
6379> flushdb
OK
127.0
.0
.1:
6379>
set hello world
OK
127.0
.0
.1:
6379> SAVE
OK
|
如下,我开启了一个Redis客户端,先清空了里面的数据,然后依次添加了一个键值对到数据库,最后通过SAVE文件将数据库中的数据保存到rdb文件中,实现数据的持久化。运行完SAVE命令之后,服务器会显示数据已经存放在磁盘文件上。
1
|
78415:M
30 Dec
10:
58:
11.445 * DB saved on disk
|
接着,我们来看看这个文件中存放着什么数据,保存到磁盘的文件名为dump.rdb
,利用od命令就能查看里面的数据。
~ od -c dump.rdb
0000000 R E D I S 0 0 0 7 372 \t r e d i s
0000020 - v e r 005 3 . 2 . 3 372 \n r e d i
0000040 s - b i t s 300 @ 372 005 c t i m e 164
0000060 t 317 e X 372 \b u s e d - m e m 302
0000100 _ 017 \0 376 \0 373 001 \0 \0 005 h e l l o 005
0000120 w o r l d 377 l 320 E e \b E \a @
|
看二进制文档实在有点费力,不过大致可以看到里面有如下信息:
- RDB文件标识和版本号:REDIS0007
- Redis版本:redis-ver 3.2.3
- Redis系统位数(32位或64位):redis-bits
- 系统时间:ctime
- 内存使用量:used-mem
- 一组键值对:hello-word
其他看不出来的信息,我们待会去源码中一一剖析出来,在源码面前,这些都不是秘密!
RDB文件结构
上面打印出来的二进制文件只能看出部分信息,Redis的RDB文件中具体包含了哪些信息,我们需要从源码中挖掘出来,不然在理解上可能会出问题,下面我画了一个表格来表示RDB的文件结构。
————————————————————————————————————————————
| 文件标识 | 辅助信息 | 数据库 | 结束符 | 校验和 |
————————————————————————————————————————————
|
文件标识
Redis在每一个RDB文件的首部都写入了如下字符,用来标识这是一个Redis的RDB文件。
—————————————————————
| REDIS | 文件版本号 |
—————————————————————
|
例如:示例中的文件以『REDIS0007』开头,0007代表RDB文件的版本号。
辅助信息
Redis在新的RDB文件版本上加入了辅助信息,其格式如下:
————————————————————————————————————————————
| redis版本 | 系统位数 | 系统时间 | 已使用的内存 |
————————————————————————————————————————————
|
例如:在上述的示例中,这些信息对应着:
372
表示是一个辅助信息\t redis-ver
表示后面的数据代表Redis的版本号005 3.2.3
表示当前Redis版本为3.2.3,005代表长度为5\n redis-bits
表示后面的数据为当前Redis服务器的位数300 @
乱码,应该是代表系统为64位005 ctime
表示后面跟着的数据为系统当前时间164 t 317 e X
当前系统时间\b used-mem
表示后面的数据为已使用的内存数302 _ 017 \0
已使用的内存数
示例中每一个信息的都是以372开头,表示这是一个辅助信息。其中,该类信息的宏定义如下:
1
2
|
#define RDB_OPCODE_AUX 250
|
数据库
Redis服务器默认有16个数据库,每个数据的信息是一次写入rdb文件中,每个数据库的信息的存放格式如下:
————————————————————————————————————————————————————
| select | dbnum | db_size | expires_size | 键值数据 |
————————————————————————————————————————————————————
|
其中,select标识当前进行切换数据库操作,后面的dnum表示当前存放的是第dbnum号数据库的数据。示例中的二进制码对应的信息如下:
376 \0
表示切换到第0号数据库;373 \1
表示当前数据库中只有一个数据;\0
表示当前没有过期键
其中,切换,数据库大小,过期键个数这些都属于一个操作信息,Redis用宏定义来表示这些数据。
1
2
3
4
|
#define RDB_OPCODE_RESIZEDB 251
#define RDB_OPCODE_SELECTDB 254
|
键值数据
键值数据的存放格式如下:
———————————————————————————————————————————————————————
| 过期键标识 | 时间戳 | 键值对类型 | 键长度 | 键 | 值长度 | 值 |
———————————————————————————————————————————————————————
|
其中,过期键标识和时间戳是可选项,如果该键设置了过期时间就需要在数据前面加上这些信息。过期键标识由以下两个宏定义给出:
1
2
3
4
|
#define RDB_OPCODE_EXPIRETIME_MS 252
#define RDB_OPCODE_EXPIRETIME 253
|
键值对类型为Redis的五个数据类型,其宏定义如下:
1
2
3
4
5
|
#define RDB_TYPE_STRING 0
#define RDB_TYPE_LIST 1
#define RDB_TYPE_SET 2
#define RDB_TYPE_ZSET 3
#define RDB_TYPE_HASH 4
|
在示例中,各二进制位代表的含义如下:
\0
标识后面是一个字符串键005 hello
长度为5的字符串hello005 world
长度为5的字符串world
结束符
每个RDB文件都以EOF结束符结尾。上述示例中对应EOF的是:
其宏定义如下:
1
|
#define RDB_OPCODE_EOF 255
|
校验和
Redis在每一个RDB文件的末尾加上了采用CRC校验的校验和,二进制中最后一串乱码标识的就是校验和,如果我们用od -cx dump.rdb
就可以更直观的看到检验和为多少。
~ od -cx dump.rdb
0000000 R E D I S 0 0 0 7 372 \t r e d i s
4552 4944 3053 3030 fa37 7209 6465 7369
0000020 - v e r 005 3 . 2 . 3 372 \n r e d i
762d 7265 3305 322e 332e 0afa 6572 6964
0000040 s - b i t s 300 @ 372 005 c t i m e 164
2d73 6962 7374 40c0 05fa 7463 6d69 c265
0000060 t 317 e X 372 \b u s e d - m e m 302
cf74 5865 08fa 7375 6465 6d2d 6d65 20c2
0000100 _ 017 \0 376 \0 373 001 \0 \0 005 h e l l o 005
0f5f fe00 fb00 0001 0500 6568 6c6c 056f
0000120 w o r l d 377 l 320 E e \b E \a @
6f77 6c72 ff64 d06c 6545 4508 4007
|
最后的0x 4007 4508 6545 d06c
就代表的是该RDB文件的校验和(校验和以小端模式存储)。
RDB编码格式
对于Redis的数据存放结构,上述分析已经很明了了。接下来,我们要具体到Redis对于每种数据结构的编码方式。
长度编码
在之前的压缩列表和整数集合中就多次见识到Redis为了节省内存做的各种措施,由于C语言中对于指针指向的内存无法计算长度,所以必须将该段内存的大小标识出来。在Redis中,有很多长度信息需要保存,如字符串的长度,链表的长度,数据库的大小等,针对不同大小的长度数据,Redis会使用不同的编码格式来节省内存。我们先来看看这些宏定义。
1
2
3
4
|
#define RDB_6BITLEN 0
#define RDB_14BITLEN 1
#define RDB_32BITLEN 2
#define RDB_ENCVAL 3
|
其具体的编码格式如下:
00|000000 // 6位长度值
01|000000 00000000 // 14位长度值
10|000000 [32位] // 后续32位表示一个32位的长度值,所以其需要5个字节来表示
11|000000 表示一个特殊编码
|
该编码方式对应的源代码函数如下,各位可以对着代码理解以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
int rdbSaveLen(rio *rdb, uint32_t len) {
unsigned
char buf[
2];
size_t nwritten;
if (len < (
1<<
6)) {
buf[
0] = (len&
0xFF)|(RDB_6BITLEN<<
6);
if (rdbWriteRaw(rdb,buf,
1) ==
-1)
return
-1;
nwritten =
1;
}
else
if (len < (
1<<
14)) {
buf[
0] = ((len>>
8)&
0xFF)|(RDB_14BITLEN<<
6);
buf[
1] = len&
0xFF;
if (rdbWriteRaw(rdb,buf,
2) ==
-1)
return
-1;
nwritten =
2;
}
else {
buf[
0] = (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;
}
|
特殊编码
特殊编码主要是将一些用字符串表示的小整数转换成整数编码,以节省内存,比如”12”,”-1”等。Redis对于这些小整数类型的字符串有以下几种不同的编码格式,用宏定义指出。
1
2
3
4
|
#define RDB_ENC_INT8 0
#define RDB_ENC_INT16 1
#define RDB_ENC_INT32 2
#define RDB_ENC_LZF 3
|
因此,其编码对应的内存布局如下:
1
2
3
4
|
11|
0000|
00
00000000
11|
0000|
01
00000000
00000000
11|
0000|
10 [
32 bits]
11|
0000|
11
|
所以,存储一个能用八字节表示字符串有符整数需要2位;存储一个能用16字节表示的有符整数需要3字节;存储一个能用32字节表示的有符整数需要5个字节。
特殊编码的实现由rdbTryIntegerEncoding和rdbEncodeInteger函数完成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
int rdbTryIntegerEncoding(char *s, size_t len, unsigned char *enc) {
long
long value;
char *endptr, buf[
32];
value = strtoll(s, &endptr,
10);
if (endptr[
0] !=
'\0')
return
0;
ll2string(buf,
32,value);
if (
strlen(buf) != len ||
memcmp(buf,s,len))
return
0;
return rdbEncodeInteger(value,enc);
}
int rdbEncodeInteger(long long value, unsigned char *enc) {
if (value >= -(
1<<
7) && value <= (
1<<
7)
-1) {
enc[
0] = (RDB_ENCVAL<<
6)|RDB_ENC_INT8;
enc[
1] = value&
0xFF;
return
2;
}
else
if (value >= -(
1<<
15) && value <= (
1<<
15)
-1) {
enc[
0] = (RDB_ENCVAL<<
6)|RDB_ENC_INT16;
enc[
1] = value&
0xFF;
enc[
2] = (value>>
8)&
0xFF;
return
3;
}
else
if (value >= -((
long
long)
1<<
31) && value <= ((
long
long)
1<<
31)
-1) {
enc[
0] = (RDB_ENCVAL<<
6)|RDB_ENC_INT32;
enc[
1] = value&
0xFF;
enc[
2] = (value>>
8)&
0xFF;
enc[
3] = (value>>
16)&
0xFF;
enc[
4] = (value>>
24)&
0xFF;
return
5;
}
else {
return
0;
}
}
|
LZF编码
当Redis开启了字符串压缩功能且字符串长度大于20bytes时,会采用LZF编码对其进行压缩,开启字符串压缩功能的变量为:
当字符串写入RDB文件时,判断上述条件成立与否,进而选择编码格式,其源码片段如下:
1
2
3
4
5
6
7
|
if (server.rdb_compression && len >
20) {
n = rdbSaveLzfStringObject(rdb,s,len);
if (n ==
-1)
return
-1;
if (n >
0)
return n;
}
|
真正执行编码操作的函数是rdbSaveLzfStringObject,其按照上述的编码格式对数据进行压缩。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
ssize_t rdbSaveLzfStringObject(rio *rdb,
unsigned
char *s,
size_t len) {
size_t comprlen, outlen;
void *out;
if (len <=
4)
return
0;
outlen = len
-4;
if ((out = zmalloc(outlen+
1)) ==
NULL)
return
0;
comprlen = lzf_compress(s, len, out, outlen);
if (comprlen ==
0) {
zfree(out);
return
0;
}
ssize_t nwritten = rdbSaveLzfBlob(rdb, out, comprlen, len);
zfree(out);
return nwritten;
}
ssize_t rdbSaveLzfBlob(rio *rdb,
void *data,
size_t compress_len,
size_t original_len) {
unsigned
char byte;
ssize_t n, nwritten =
0;
byte = (RDB_ENCVAL<<
6)|RDB_ENC_LZF;
if ((n = rdbWriteRaw(rdb,&byte,
1)) ==
-1)
goto writeerr;
nwritten += n;
if ((n = rdbSaveLen(rdb,compress_len)) ==
-1)
goto writeerr;
nwritten += n;
if ((n = rdbSaveLen(rdb,original_len)) ==
-1)
goto writeerr;
nwritten += n;
if ((n = rdbWriteRaw(rdb,data,compress_len)) ==
-1)
goto writeerr;
nwritten += n;
return nwritten;
writeerr:
return
-1;
}
|
从源码中可以看出,经过LZF算法压缩的字符串在内存中的布局如下:
——————————————————————————————————————————————————————
| LZF标识(11000011) | 压缩后的长度 | 原长度 | 压缩后的数据 |
——————————————————————————————————————————————————————
|
String对象编码
前面在Redis源码剖析–字符串t_string一文中,有介绍到string对象的底层编码有三种,分别是OBJ_ENCODING_INT
、OBJ_ENCODING_RAW
和OBJ_ENCODING_EMBSTR
,这三种编码的不同之处各位可以跳转复习一下。在写入RDB文件时,会判断String对象的编码类型,从而选择以何种编码方式写入到RDB文件中。字符串是按照如下三种格式存放在RDB文件中的。
1
2
3
4
5
6
7
8
9
10
11
12
|
————————————————
| len | data |
————————————————
——————————————————
| Encoding |
int |
——————————————————
————————————————————————————————————————————
| LZF标识 | 压缩后的长度 | 原长度 | 压缩后的数据 |
————————————————————————————————————————————
|
其实现源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
int rdbSaveStringObject(rio *rdb, robj *obj) {
* object is already integer encoded. */
if (obj->encoding == OBJ_ENCODING_INT) {
return rdbSaveLongLongAsStringObject(rdb,(
long)obj->ptr);
}
else {
serverAssertWithInfo(
NULL,obj,sdsEncodedObject(obj));
return rdbSaveRawString(rdb,obj->ptr,sdslen(obj->ptr));
}
}
ssize_t rdbSaveLongLongAsStringObject(rio *rdb,
long
long value) {
unsigned
char buf[
32];
ssize_t n, nwritten =
0;
int enclen = rdbEncodeInteger(value,buf);
if (enclen >
0) {
return rdbWriteRaw(rdb,buf,enclen);
}
else {
enclen = ll2string((
char*)buf,
32,value);
serverAssert(enclen <
32);
if ((n = rdbSaveLen(rdb,enclen)) ==
-1)
return
-1;
nwritten += n;
if ((n = rdbWriteRaw(rdb,buf,enclen)) ==
-1)
return
-1;
nwritten += n;
}
return nwritten;
}
ssize_t rdbSaveRawString(rio *rdb,
unsigned
char *s,
size_t len) {
int enclen;
ssize_t n, nwritten =
0;
if (len <=
11) {
unsigned
char buf[
5];
if ((enclen = rdbTryIntegerEncoding((
char*)s,len,buf)) >
0) {
if (rdbWriteRaw(rdb,buf,enclen) ==
-1)
return
-1;
return enclen;
}
}
if (server.rdb_compression && len >
20) {
n = rdbSaveLzfStringObject(rdb,s,len);
if (n ==
-1)
return
-1;
if (n >
0)
return n;
}
if ((n = rdbSaveLen(rdb,len)) ==
-1)
return
-1;
nwritten += n;
if (len >
0) {
if (rdbWriteRaw(rdb,s,len) ==
-1)
return
-1;
nwritten += len;
}
return nwritten;
}
|
List对象编码
在Redis源码剖析–列表t_list一文中,解释到List的底层编码只有quicklist。其存放格式如下:
1
2
3
|
————————————————————————————————————————————————————————————————————————
| listLength | len1| data1 | len2 | CompressLength| OriginLength | data2 |
————————————————————————————————————————————————————————————————————————
|
其中,第一个节点直接按照字符串的形式存放;第二个节点采用LZF压缩后存放,其源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
ssize_t rdbSaveObject(rio *rdb, robj *o) {
ssize_t n =
0, nwritten =
0;
if (o->type == OBJ_STRING) {
if ((n = rdbSaveStringObject(rdb,o)) ==
-1)
return
-1;
nwritten += n;
}
else
if (o->type == OBJ_LIST) {
if (o->encoding == OBJ_ENCODING_QUICKLIST) {
quicklist *ql = o->ptr;
quicklistNode *node = ql->head;
if ((n = rdbSaveLen(rdb,ql->len)) ==
-1)
return
-1;
nwritten += n;
do {
if (quicklistNodeIsCompressed(node)) {
void *data;
size_t compress_len = quicklistGetLzf(node, &data);
if ((n = rdbSaveLzfBlob(rdb,data,compress_len,node->sz)) ==
-1)
return
-1;
nwritten += n;
}
else {
if ((n = rdbSaveRawString(rdb,node->zl,node->sz)) ==
-1)
return
-1;
nwritten += n;
}
}
while ((node = node->next));
}
else {
serverPanic(
"Unknown list encoding");
}
}
}
|
Set对象编码
在Redis源码剖析–集合t_set一文中讲到Set的实现原理和数据存储形式,set的底层采用字典或者整数集合的编码形式。Set对象在RDB文件中的存储形式为:
—————————————————————————————————————————
| setSize | elem1 | elem2 | ... | elemN |
—————————————————————————————————————————
/* 集合存储示例 */
————————————————————————————————————————————
| 3 | 3 | "zee" | 5 | "coder" | 5 | "cheng" |
————————————————————————————————————————————
|
集合中的每一个值都按照其值选取不同的编码存放,如字符串就按字符串,LZF压缩就压缩存储,小整数就按照小整数存储…..
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
ssize_t rdbSaveObject(rio *rdb, robj *o) {
ssize_t n =
0, nwritten =
0;
if (o->type == OBJ_SET) {
if (o->encoding == OBJ_ENCODING_HT) {
dict *
set = o->ptr;
dictIterator *di = dictGetIterator(
set);
dictEntry *de;
if ((n = rdbSaveLen(rdb,dictSize(
set))) ==
-1)
return
-1;
nwritten += n;
while((de = dictNext(di)) !=
NULL) {
robj *eleobj = dictGetKey(de);
if ((n = rdbSaveStringObject(rdb,eleobj)) ==
-1)
return
-1;
nwritten += n;
}
dictReleaseIterator(di);
}
else
if (o->encoding == OBJ_ENCODING_INTSET) {
size_t l = intsetBlobLen((intset*)o->ptr);
if ((n = rdbSaveRawString(rdb,o->ptr,l)) ==
-1)
return
-1;
nwritten += n;
}
else {
serverPanic(
"Unknown set encoding");
}
}
}
|
Zset对象编码
在Redis源码剖析–有序集合t_zset一文中提到,zset采用zskiplist或者ziplist编码,不过这两种编码不影响它在RDB文件中的存放格式。
———————————————————————————————————————————————————————————————————————
| zset_length | elem1 | score1 | elem2 | score2 | ... | elem3 | score3 |
———————————————————————————————————————————————————————————————————————
|
其源码实现如下,没什么特别的,不懂的看注释吧。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
ssize_t rdbSaveObject(rio *rdb, robj *o) {
ssize_t n =
0, nwritten =
0;
if (o->type == OBJ_ZSET) {
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
size_t l = ziplistBlobLen((
unsigned
char*)o->ptr);
if ((n = rdbSaveRawString(rdb,o->ptr,l)) ==
-1)
return
-1;
nwritten += n;
}
else
if (o->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = o->ptr;
dictIterator *di = dictGetIterator(zs->dict);
dictEntry *de;
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);
if ((n = rdbSaveStringObject(rdb,eleobj)) ==
-1)
return
-1;
nwritten += n;
if ((n = rdbSaveDoubleValue(rdb,*score)) ==
-1)
return
-1;
nwritten += n;
}
dictReleaseIterator(di);
}
else {
serverPanic(
"Unknown sorted set encoding");
}
}
}
|
Hash对象编码
在Redis源码剖析–哈希t_hash一文中,hash底层的数据结构有两种,ziplist和字典,同样在写入RDB文件的时候,需要判断编码类型,然后采用不同的形式存放。
—————————————————————————————————————————————————
| hashSize | key1 | value1| .... | key2 | value2 |
—————————————————————————————————————————————————
|
其源码实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
ssize_t rdbSaveObject(rio *rdb, robj *o) {
ssize_t n =
0, nwritten =
0;
if (o->type == OBJ_HASH) {
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
size_t l = ziplistBlobLen((
unsigned
char*)o->ptr);
if ((n = rdbSaveRawString(rdb,o->ptr,l)) ==
-1)
return
-1;
nwritten += n;
}
else
if (o->encoding == OBJ_ENCODING_HT) {
dictIterator *di = dictGetIterator(o->ptr);
dictEntry *de;
if ((n = rdbSaveLen(rdb,dictSize((dict*)o->ptr))) ==
-1)
return
-1;
nwritten += n;
while((de = dictNext(di)) !=
NULL) {
robj *key = dictGetKey(de);
robj *val = dictGetVal(de);
if ((n = rdbSaveStringObject(rdb,key)) ==
-1)
return
-1;
nwritten += n;
if ((n = rdbSaveStringObject(rdb,val)) ==
-1)
return
-1;
nwritten += n;
}
dictReleaseIterator(di);
}
else {
serverPanic(
"Unknown hash encoding");
}
}
}
|
RDB命令
RDB有两种命令,一种是SAVE,另一种是BGSAVE。我们一起来看看他们的实现源码。
SAVE命令
按照Redis的命令定义,可以知道SAVE命令的底层代码实现是由saveCommand实现,于是去源码中找到了它。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void saveCommand(client *c) {
if (server.rdb_child_pid !=
-1) {
addReplyError(c,
"Background save already in progress");
return;
}
if (rdbSave(server.rdb_filename) == C_OK) {
addReply(c,shared.ok);
}
else {
addReply(c,shared.err);
}
}
|
上述代码中,真正进行写rdb文件的函数是rdbSave函数,于是我们进一步跟踪到了它。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
int rdbSave(char *filename) {
char tmpfile[
256];
char cwd[MAXPATHLEN];
FILE *fp;
rio rdb;
int error =
0;
snprintf(tmpfile,
256,
"temp-%d.rdb", (
int) getpid());
fp = fopen(tmpfile,
"w");
if (!fp) {
char *cwdp = getcwd(cwd,MAXPATHLEN);
serverLog(LL_WARNING,
"Failed opening the RDB file %s (in server root dir %s) "
"for saving: %s",
filename,
cwdp ? cwdp :
"unknown",
strerror(errno));
return C_ERR;
}
rioInitWithFile(&rdb,fp);
if (rdbSaveRio(&rdb,&error) == C_ERR) {
errno = error;
goto werr;
}
if (fflush(fp) == EOF)
goto werr;
if (fsync(fileno(fp)) ==
-1)
goto werr;
if (fclose(fp) == EOF)
goto werr;
if (rename(tmpfile,filename) ==
-1) {
char *cwdp = getcwd(cwd,MAXPATHLEN);
serverLog(LL_WARNING,
"Error moving temp DB file %s on the final "
"destination %s (in server root dir %s): %s",
tmpfile,
filename,
cwdp ? cwdp :
"unknown",
strerror(errno));
unlink(tmpfile);
return C_ERR;
}
serverLog(LL_NOTICE,
"DB saved on disk");
server.dirty =
0;
server.lastsave = time(
NULL);
server.lastbgsave_status = C_OK;
return C_OK;
werr:
serverLog(LL_WARNING,
"Write error saving DB on disk: %s", strerror(errno));
fclose(fp);
unlink(tmpfile);
return C_ERR;
}
|
到这一步,还是没有看出来rdb的结构。不过可以知道在写RDB文件时,是先创建一个临时文件,向临时文件中写入数据,如果成功则改名,反之则删除。我们注意到调用了底层函数rdbSaveRio来执行的写操作。接着我们继续吧。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
int rdbSaveRio(rio *rdb, int *error) {
dictIterator *di =
NULL;
dictEntry *de;
char magic[
10];
int j;
long
long now = mstime();
uint64_t cksum;
if (server.rdb_checksum)
rdb->update_cksum = rioGenericUpdateChecksum;
snprintf(magic,
sizeof(magic),
"REDIS%04d",RDB_VERSION);
if (rdbWriteRaw(rdb,magic,
9) ==
-1)
goto werr;
if (rdbSaveInfoAuxFields(rdb) ==
-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)
return C_ERR;
if (rdbSaveType(rdb,RDB_OPCODE_SELECTDB) ==
-1)
goto werr;
if (rdbSaveLen(rdb,j) ==
-1)
goto werr;
uint32_t db_size, expires_size;
db_size = (dictSize(db->dict) <= UINT32_MAX) ?
dictSize(db->dict) :
UINT32_MAX;
expires_size = (dictSize(db->expires) <= UINT32_MAX) ?
dictSize(db->expires) :
UINT32_MAX;
if (rdbSaveType(rdb,RDB_OPCODE_RESIZEDB) ==
-1)
goto werr;
if (rdbSaveLen(rdb,db_size) ==
-1)
goto werr;
if (rdbSaveLen(rdb,expires_size) ==
-1)
goto werr;
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);
}
di =
NULL;
if (rdbSaveType(rdb,RDB_OPCODE_EOF) ==
-1)
goto werr;
cksum = rdb->cksum;
memrev64ifbe(&cksum);
if (rioWrite(rdb,&cksum,
8) ==
0)
goto werr;
return C_OK;
werr:
if (error) *error = errno;
if (di) dictReleaseIterator(di);
return C_ERR;
}
|
BGSAVE命令
BGSAVE命令是开一个进程,然后存储RDB文件在该进程中执行,属于后台存储。该存储方式需要注意一下几种情况。
- 如果后台正在进行RDB存储,则返回错误
- 如果后台正在进行AOF存储,则将rdb_bgsave_scheduled参数置1,等到系统函数
serverCron
定期执行的时候,检查参数,并执行BGSAVE命令
其源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
|
void bgsaveCommand(client *c) {
int schedule =
0;
if (c->argc >
1) {
if (c->argc ==
2 && !strcasecmp(c->argv[
1]->ptr,
"schedule")) {
schedule =
1;
}
else {
addReply(c,shared.syntaxerr);
return;
}
}
if (server.rdb_child_pid !=
-1) {
addReplyError(c,
"Background save already in progress");
}
else
if (server.aof_child_pid !=
-1) {
if (schedule) {
server.rdb_bgsave_scheduled =
1;
addReplyStatus(c,
"Background saving scheduled");
}
else {
addReplyError(c,
"An AOF log rewriting in progress: can't BGSAVE right now. "
"Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenver "
"possible.");
}
}
else
if (rdbSaveBackground(server.rdb_filename) == C_OK) {
addReplyStatus(c,
"Background saving started");
}
else {
addReply(c,shared.err);
}
}
int rdbSaveBackground(char *filename) {
pid_t childpid;
long
long start;
if (server.aof_child_pid !=
-1 || server.rdb_child_pid !=
-1)
return C_ERR;
server.dirty_before_bgsave = server.dirty;
server.lastbgsave_try = time(
NULL);
start = ustime();
if ((childpid = fork()) ==
0) {
int retval;
closeListeningSockets(
0);
redisSetProcTitle(
"redis-rdb-bgsave");
retval = rdbSave(filename);
if (retval == C_OK) {
size_t private_dirty = zmalloc_get_private_dirty();
if (private_dirty) {
serverLog(LL_NOTICE,
"RDB: %zu MB of memory used by copy-on-write",
private_dirty/(
1024*
1024));
}
}
exitFromChild((retval == C_OK) ?
0 :
1);
}
else {
server.stat_fork_time = ustime()-start;
server.stat_fork_rate = (
double) zmalloc_used_memory() *
1000000 / server.stat_fork_time / (
1024*
1024*
1024);
latencyAddSampleIfNeeded(
"fork",server.stat_fork_time/
1000);
if (childpid ==
-1) {
server.lastbgsave_status = C_ERR;
serverLog(LL_WARNING,
"Can't save in background: fork: %s",
strerror(errno));
return C_ERR;
}
serverLog(LL_NOTICE,
"Background saving started by pid %d",childpid);
server.rdb_save_time_start = time(
NULL);
server.rdb_child_pid = childpid;
server.rdb_child_type = RDB_CHILD_TYPE_DISK;
updateDictResizePolicy();
return C_OK;
}
return C_OK;
}
|
以上代码中需要注意的是,fork的特性,在子进程中childPid为0,在父进程中childPid为父进程的ID号,所以子进程在执行SAVE操作,父进程在检查操作是否执行成功并存储相关变量。
自动保存
在Redis.conf文件中,可以配置服务器定期执行SAVE命令,该参数如下:
1
2
3
|
save
900
1
save
300
10
save
60
10000
|
其含义依次如下:
- 服务器在900秒之内,对数据库至少进行了一次修改
- 服务器在300秒之内,对数据库至少进行了10次修改
- 服务器在60秒之内,对数据库至少进行了10000次修改
所以,服务器只要满足这三个条件之一,就会自动执行SAVE操作。那么这一功能是如何实现的呢?我们先来看一个数据结构,在server.h文件夹中。
1
2
3
4
|
struct saveparam {
time_t seconds;
int changes;
}
|
另外,在redisServer结构体中有如下参数,用来记录上述要求。
1
2
3
4
5
|
struct redisServer {
struct saveparam * saveparam ;
}
|
有了配置文件中的参数,那么服务器中对数据的修改次数和事件存放在哪呢?其实,之前的源码分析中都见到过,每次修改数据的时候,都需要将系统的脏数据个数加1,而且还要保存修改时间,没错就是它俩。
1
2
3
4
5
6
|
struct redisServer {
long
long dirty;
time_t lastsave;
}
|
了解了这些参数和结构体在哪之后,就可以通过判断自动保存的条件是否符合来执行BGSAVE命令了,其源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
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))
{
serverLog(LL_NOTICE,
"%d changes in %d seconds. Saving...",
sp->changes, (
int)sp->seconds);
rdbSaveBackground(server.rdb_filename);
break;
}
}
}
|
RDB小结
本文简要分析了RDB结构中数据的存放格式,而后分析了SAVE和BGSAVE命令的执行步骤和源码,最后分析了自动保存功能的实现原理,基本上整个RDB持久化操作的过程以及了然于心了。我们现在可以放心的保证,Redis的RDB全过程已GET!由于本人也是边学边写博客,其中难免有错误的地方,希望大家在阅读的时候能及时指出!期待和大家一起学习交流Redis!