Redis持久化之RDB

时间:2023-03-09 03:08:21
Redis持久化之RDB

本文及后续文章,Redis版本均是v3.2.8

上篇文章介绍了RDB的优缺点,我们先来回顾下RDB的主要原理,在某个时间点把内存中所有数据保存到磁盘文件中,这个过程既可以通过人工输入命令执行,也可以让服务器周期性执行。

RDB持久化机制RDB的实现原理,涉及的文件为rdb.hrdb.c

一、初始RDB

先在Redis客户端中执行以下命令,存入一些数据:

127.0.0.1:6379> flushdb

OK

127.0.0.1:6379> set city "beijing"

OK

127.0.0.1:6379> save

OK

127.0.0.1:6379>

Redis提供了save和bgsave两个命令来生成RDB文件(即将内存数据写入RDB文件中),执行成功后我们在磁盘中找到该RDB文件(dump.rdb),该文件存放的内容如下:

Redis持久化之RDB

REDIS0007?redis-ver3.2.100?redis-bits繞?ctime聥阨Y?used-mem锣?

我们再来看下Redis Server的版本号

Redis持久化之RDB

RDB文件中存放的是二进制数据,从上面的文件非乱码的内容中我们大概可以看出里面存放的各个类型的数据信息。下面我们就来介绍一下RDB的文件格式。

二、RDB文件结构

我们先大致看下RDB文件结构

Redis持久化之RDB

1、RDB文件结构

我们看下图中的各部分含义:

名称 大小 说明
REDIS 5bytes 固定值,存放’R’,’E’,’D’,’I’,’S’
RDB_VERSION 4bytes

RDB版本号,在rdb.h头文件中定义

/* The current RDB version. When the format changes in a way that is no longer backward compatible this number gets incremented. */

#define RDB_VERSION 7

DB-DATA —— 存储真正的数据
RDB_OPCODE_EOF 1byte

255(0377),表述数据库结束,

在rdb.h头文件中定义

#define RDB_OPCODE_EOF

255

checksum —— 校验和

2、DB-DATA结构

名称 大小 说明
RDB_OPCODE_SELECTDB 1byte

以前我们介绍过,当redis 服务器初始化时,会预先分配 16 个数据库。这里我们需要将非空的数据库信息保存在RDB文件中。

在rdb.h头文件中定义

#define RDB_OPCODE_SELECTDB   254

db_number

1,2,5bytes

存储数据库的号码。

db编号即对应的数据库编号,每个db编号后边到下一个RDB_OPCODE_SELECTDB标识符出现之前的所有数据都是该db下的数据。在REDIS加载 RDB 文件时,会根据这个域的值切换到相应的数据库,以确保数据被还原到正确的数据库中去。

key_value_pairs —— 主要数据

3、key_value_pairs结构

  • 带过期时间

名称 大小 说明
RDB_OPCODE_EXPIRETIME_MS 1byte 252,说明是带过期时间的键值对
timestamp 8bytes 以毫秒为单位的时间戳
TYPE 8bytes 以毫秒为单位的时间戳
key ———
value ———
  • 不带过期时间

名称 大小 说明
TYPE 8bytes 以毫秒为单位的时间戳
key ———
value ———

TYPE的值,目前Redis主要有以下数据类型:

/* Dup object types to RDB object types. Only reason is readability (are we

* dealing with RDB types or with in-memory object types?). */

#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

/* NOTE: WHEN ADDING NEW RDB TYPE, UPDATE rdbIsObjectType() BELOW */

/* Object types for encoded objects. */

#define RDB_TYPE_HASH_ZIPMAP    9

#define RDB_TYPE_LIST_ZIPLIST  10

#define RDB_TYPE_SET_INTSET    11

#define RDB_TYPE_ZSET_ZIPLIST  12

#define RDB_TYPE_HASH_ZIPLIST  13

#define RDB_TYPE_LIST_QUICKLIST 14

/* NOTE: WHEN ADDING NEW RDB TYPE, UPDATE rdbIsObjectType() BELOW */

4、RDB_OPCODE_EOF

标识数据库部分的结束符,定义在rdb.h文件中:

#define RDB_OPCODE_EOF        255

5、rdb_checksum

RDB 文件所有内容的校验和, 一个 uint_64t 类型值。

Redis在写入RDB文件时将校验和保存在RDB文件的末尾, 当读取RDB时, 根据它的值对内容进行校验。

如果Redis未开启校验功能,则该域的值为0。

#define CONFIG_DEFAULT_RDB_CHECKSUM 1

三、长度编码

在RDB文件中有很多地方需要存储长度信息,如字符串长度、list长度等等。如果使用固定的int或long类型来存储该信息,在长度值比较小的时候会造成较大的空间浪费。为了节省空间,Redis设计了一套特殊的方法对长度进行编码后再存储。我们先来看下定义的编码说明:

/* Defines related to the dump file format. To store 32 bits lengths for short

* keys requires a lot of space, so we check the most significant 2 bits of

* the first byte to interpreter the length:

*

* 00|000000 => if the two MSB are 00 the len is the 6 bits of this byte

* 01|000000 00000000 =>  01, the len is 14 byes, 6 bits + 8 bits of next byte

* 10|000000 [32 bit integer] => if it's 01, a full 32 bit len will follow

* 11|000000 this means: specially encoded object will follow. The six bits

*           number specify the kind of object that follows.

*           See the RDB_ENC_* defines.

*

* Lengths up to 63 are stored using a single byte, most DB keys, and may

* values, will fit inside. */

编码方式 占用字节数 说明
00|000000 1byte 这一字节的其余 6 位表示长度,可以保存的最大长度是 63 (包括在内)
01|000000 00000000 2byte 长度为 14 位,当前字节 6 位,加上下个字节 8 位
10|000000 [32 bit integer] 5byte 长度由随后的 32 位整数保存
11|000000   后跟一个特殊编码的对象。字节中的 6 位(实际上只用到两个bit)指定对象的类型,用来确定怎样读取和解析接下来的数据

rdb.h文件中具体定义的编码:

  • 普通编码方式

#define RDB_6BITLEN 0

#define RDB_14BITLEN 1

#define RDB_32BITLEN 2

#define RDB_ENCVAL 3

表格中前三种可以理解为普通编码方式。

  • 字符串编码方式

/* When a length of a string object stored on disk has the first two bits

* set, the remaining two bits specify a special encoding for the object

* accordingly to the following defines: */

#define RDB_ENC_INT8 0        /* 8 bit signed integer */

#define RDB_ENC_INT16 1       /* 16 bit signed integer */

#define RDB_ENC_INT32 2       /* 32 bit signed integer */

#define RDB_ENC_LZF 3         /* string compressed with FASTLZ */

表格中最后一种可以理解为字符串编码方式。

1、字符串转换为整数进行存储

/* String objects in the form "2391" "-100" without any space and with a

* range of values that can fit in an 8, 16 or 32 bit signed value can be

* encoded as integers to save space */

int rdbTryIntegerEncoding(char *s, size_t len, unsigned char *enc) {

long long value;

char *endptr, buf[32];

/* Check if it's possible to encode this value as a number */

value = strtoll(s, &endptr, 10);

if (endptr[0] != '\0') return 0;

ll2string(buf,32,value);

/* If the number converted back into a string is not identical

* then it's not possible to encode the string as integer */

if (strlen(buf) != len || memcmp(buf,s,len)) return 0;

return rdbEncodeInteger(value,enc);

}

该函数最后调用的rdbEncodeInteger函数是真正完成特殊编码的地方,具体定义如下:

/* Encodes the "value" argument as integer when it fits in the supported ranges

* for encoded types. If the function successfully encodes the integer, the

* representation is stored in the buffer pointer to by "enc" and the string

* length is returned. Otherwise 0 is returned. */

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;

}

}

2、使用lzf算法进行字符串压缩

当Redis开启了字符串压缩的功能后,如果一个字符串的长度超过20bytes,Redis会使用lzf算法对其进行压缩后再存储。

/* Save a string object as [len][data] on disk. If the object is a string

* representation of an integer value we try to save it in a special form */

ssize_t rdbSaveRawString(rio *rdb, unsigned char *s, size_t len) {

int enclen;

ssize_t n, nwritten = 0;

/* Try integer encoding */

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;

}

}

/* Try LZF compression - under 20 bytes it's unable to compress even

* aaaaaaaaaaaaaaaaaa so skip it */

if (server.rdb_compression && len > 20) {

n = rdbSaveLzfStringObject(rdb,s,len);

if (n == -1) return -1;

if (n > 0) return n;

/* Return value of 0 means data can't be compressed, save the old way */

}

/* Store verbatim */

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;

}

四、value存储

上面我们介绍了长度编码,接下来继续介绍不同数据类型的value是如何存储的?

我们在介绍redisobject《Redis数据结构之robj》时,介绍了对象的10种编码方式。

/* Objects encoding. Some kind of objects like Strings and Hashes can be

* internally represented in multiple ways. The 'encoding' field of the object

* is set to one of this fields for this object. */

#define OBJ_ENCODING_RAW 0     /* Raw representation */

#define OBJ_ENCODING_INT 1     /* Encoded as integer */

#define OBJ_ENCODING_HT 2      /* Encoded as hash table */

#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */

#define OBJ_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */

#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */

#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */

#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */

#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */

#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */

1、string类型对象

我们知道,字符串类型对象的存储结构是RDB文件中最基础的存储结构,其它数据类型的存储大多建立在字符串对象存储的基础上。

  • OBJ_ENCODING_INT编码的字符串

对于REDIS_ENCODING_INT编码的字符串对象,有以下两种保存方式:

a、如果该字符串可以用 8 bit、 16 bit或 32 bit长的有符号整型数值表示,那么就直接以整型数保存;

b、如果32bit的整数无法表示该字符串,则该字符串是一个long long类型的数,这种情况下将其转化为字符串后存储。

对于第一种方式,value域就是一个整型数值;

对于第二种方式,value域的结构为:

Redis持久化之RDB

其中length域存放字符串的长度,content域存放字符序列。

/* Save a long long value as either an encoded string or a string. */

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 {

/* Encode as string */

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;

}

  • OBJ_ENCODING_RAW编码的字符串

对于REDIS_ENCODING_RAW编码的字符串对象,有以下三种保存方式:

a、如果该字符串可以用 8 bit、 16 bit或 32 bit长的有符号整型数值表示,那么就将字符串转换为整型数存储以节省空间;

b、如果服务器开启了字符串压缩功能,且该字符串的长度大于20bytes,则使用lzf算法对字符串压缩后进行存储;

c、如果不满足上面两个条件,Redis只能以普通字符序列的方式来保存该字符串字符串对象。

对于前面两种方式,详见小节【长度编码】中已经详细介绍过。

对于第三种方式,Redis以普通字符序列的方式来保存字符串对象,value域的存储结构为:

Redis持久化之RDB

其中length域存放字符串的长度,content域存放字符串本身。

/* Save a string object as [len][data] on disk. If the object is a string

* representation of an integer value we try to save it in a special form */

ssize_t rdbSaveRawString(rio *rdb, unsigned char *s, size_t len) {

int enclen;

ssize_t n, nwritten = 0;

/* Try integer encoding */

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;

}

}

/* Try LZF compression - under 20 bytes it's unable to compress even

* aaaaaaaaaaaaaaaaaa so skip it */

if (server.rdb_compression && len > 20) {

n = rdbSaveLzfStringObject(rdb,s,len);

if (n == -1) return -1;

if (n > 0) return n;

/* Return value of 0 means data can't be compressed, save the old way */

}

/* Store verbatim */

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;

}

2、list类型对象

  • OBJ_ENCODING_LINKEDLIST编码的list类型对象

每个节点以字符串对象的形式逐一存储。

在RDB文件中存储结构如下:

Redis持久化之RDB

  • OBJ_ENCODING_ZIPLIST编码的list类型对象

Redis将其当做一个字符串对象的形式进行保存。

3、hash类型对象

  • OBJ_ENCODING_ZIPLIST编码的hash类型对象

Redis将其当做一个字符串对象的形式进行保存。

  • OBJ_ENCODING_HT编码的hash类型对象

hash中的每个键值对的key值和value值都以字符串对象的形式相邻存储。

在RDB文件中存储结构如下:

Redis持久化之RDB

4、set类型对象

  • OBJ_ENCODING_HT编码的set类型对象

其底层使用字典dict结构进行存储,只是该字典的value值为NULL,所以只需要存储每个键值对的key值即可。每个元素以字符串对象的形式逐一存储。

在RDB文件中存储结构如下:

Redis持久化之RDB

  • OBJ_ENCODING_INTSET编码的set类型对象

Redis将其当做一个字符串对象的形式进行保存,

5、zset类型对象

  • OBJ_ENCODING_ZIPLIST编码的zset类型对象

Redis将其当做一个字符串对象的形式进行保存。

  • OBJ_ENCODING_QUICKLIST编码的zset类型对象

对于其中一个元素,先存储其元素值value再存储其分值score。zset的元素值是一个字符串对象,按字符串形式存储,分值是一个double类型的数值,Redis先将其转换为字符串对象再存储。

在RDB文件中存储结构如下:

Redis持久化之RDB

五、RDB如何完成存储

  • save命令

save是在Redis进程中执行的,由于Redis是单线程实现,所以当save命令在执行时会阻塞Redis服务器一直到该命令执行完成为止。

  • bgsave命令

与save命令不同的是,bgsave命令会先fork出一个子进程,然后在子进程中生成RDB文件。由于在子进程中执行IO操作,所以bgsave命令不会阻塞Redis服务器进程,Redis服务器进程在此期间可以继续对外提供服务。

bgsave命令由rdbSaveBackground函数实现,从该函数的实现中可以看出:为了提高性能,Redis服务器在bgsave命令执行期间会拒绝执行新到来的其它bgsave命令。

这里就不再列出rdbSave函数和rdbSaveBackground函数的具体实现,请移步到rdb.c文件中查看。

上篇文章《Redis持久化persistence》中,介绍了redis.conf中配置"触发执行"的配置:

<seconds> <changes>

表示如果在secons指定的时间(秒)内对Redis数据库DB至少进行了changes次修改,则执行一次bgsave命令

我们思考一个问题:Redis是如何判断save选项配置条件是否已经达到,可以触发执行的呢?

1、save选项配置条件如何存储?

在server.h头文件中,定义了saveparam结构体来保存save配置选项,该结构体的定义如下:

struct saveparam {

time_t seconds; // 秒数

int changes;      // 修改次数

};

Redis默认提供或用户输入的save选项则保存在 redisServer结构体中:

struct redisServer {

....

/* RDB persistence */

long long dirty;                /* Changes to DB from the last save */

long long dirty_before_bgsave;  /* Used to restore dirty on failed BGSAVE */

pid_t rdb_child_pid;            /* PID of RDB saving child */

struct saveparam *saveparams;   /* Save points array for RDB */

int saveparamslen;              /* Number of saving points */

char *rdb_filename;             /* Name of RDB file */

int rdb_compression;            /* Use compression in RDB? */

int rdb_checksum;               /* Use RDB checksum? */

time_t lastsave;                /* Unix time of last successful save */

time_t lastbgsave_try;          /* Unix time of last attempted bgsave */

time_t rdb_save_time_last;      /* Time used by last RDB save run. */

time_t rdb_save_time_start;     /* Current RDB save start time. */

int rdb_bgsave_scheduled;       /* BGSAVE when possible if true. */

int rdb_child_type;             /* Type of save by active child. */

int lastbgsave_status;          /* C_OK or C_ERR */

int stop_writes_on_bgsave_err;  /* Don't allow writes if can't BGSAVE */

int rdb_pipe_write_result_to_parent; /* RDB pipes used to return the state */

int rdb_pipe_read_result_from_child; /* of each slave in diskless SYNC. */

....

}

我们可以看到redisServer结构体中的saveparams字段是一个数组,里面一个元素就是一个save配置,而saveparamslen字段则指明了save配置的个数。

2、修改的次数和时间记录如何存储?

我们从redisServer结构体中,知道dirty和lastsave字段

  • dirty的值表示自最近一次执行save或bgsave以来对数据库DB的修改(即执行写入、更新、删除操作的)次数;

  • lastsave是最近一次成功执行save或bgsave命令的时间戳。

 

3、Redis如何判断是否满足save选项配置的条件?

到目前为止,我们已经有了记录save配置的redisServer.saveparams数组,告诉Redis如果满足save配置的条件则执行一次bgsave命令。此外我们也有了redisServer.dirty和redisServer.lastsave两个字段,分别记录了对数据库DB的修改(即执行写入、更新、删除操作的)次数和最近一次执行save或bgsave命令的时间戳。

接下来我们只要周期性地比较一下redisServer.saveparams和redisServer.dirty、redisServer.lastsave就可以判断出是否需要执行bgsave命令。

这个周期性执行检查功能的函数就是serverCron函数,定义在server.c文件中。

六、总结

  • rdbSave 会将数据库数据保存到 RDB 文件,并在保存完成之前阻塞调用者。

  • SAVE 命令直接调用 rdbSave ,阻塞 Redis 主进程; BGSAVE 用子进程调用 rdbSave ,主进程仍可继续处理命令请求。

  • SAVE 执行期间, AOF 写入可以在后台线程进行, BGREWRITEAOF 可以在子进程进行,所以这三种操作可以同时进行。

  • 为了避免产生竞争条件, BGSAVE 执行时, SAVE 命令不能执行。

  • 为了避免性能问题, BGSAVE 和 BGREWRITEAOF 不能同时执行。

  • RDB 文件使用不同的格式来保存不同类型的值。

Redis持久化之RDB

--EOF--