redis源码分析(三)--rdb持久化

Redis rdb持久化

  Redis支持两种持久化方式:rdb与aof。rdb将一个节点上的内存数据序列化后存储到磁盘中,序列化的数据以尽可能节约空间的方式存储,并非完全的ascii表示。它的优点在于节约空间,恢复速度快,缺点在于每一次操作都需要对整个内存数据进行序列化,并且持久化过程中的修改被丢失。而aof将数据以操作命令的方式进行存储,从aof恢复数据即从aof文件读入命令再执行命令。它的优点是可以记录持久化过程中的产生的命令,而缺点在于完全以ascii编码,使用空间更多,且恢复速度更慢。这篇文件章主要对rdb的持久化方式进行大致介绍。

  Redis中有多种数据类型,如string、set、zset、hash、list等,并且具有多个db(一个db即是一个字典,存储了string、list、set等类型的数据),为了完成不同类型数据的序列化,Redis设计了相应的持久化格式。

1. 序列化string

  序列化string时,首先会存储string的长度,然后存储string的真实值。为了节约空间,对于不同长度的string,redis使用尽可能少的空间来存储它的长度,因此设计了如下的字符串长度存储格式:     

  1. len < 1<<6,使用1个字节编码长度,该字节的高2bits为00,低6bits代表长度
  2. 1<<6 <= len < 1<<14,使用2个字节编码长度,第1个字节的高两bits为01,后续14bits代表长度
  3. 1<<14 <= len <= UINT32_MAX,使用5个字节编码长度,第1个字节为0x80,后续4字节代表长度
  4. UINT32_MAX < len,使用9个字节编码长度,第1个字节为0x81,后续8字节代表长度

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|XXXXXX => if the two MSB are 00 the len is the 6 bits of this byte
 * 01|XXXXXX XXXXXXXX =>  01, the len is 14 byes, 6 bits + 8 bits of next byte
 * 10|000000 [32 bit integer] => A full 32 bit len in net byte order will follow
 * 10|000001 [64 bit integer] => A full 64 bit len in net byte order will follow
 * 11|OBKIND 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. */

举例,对string“hello world”编码结果如下:

0xC “hello, world”

0xC以1个字节表示字符串长度为12字节,后续12字节即为字符串的真实值。由于是非前缀码,因此根据第1个字节的值完全可以进行区分是哪种格式,反序列化时首先读1个字节的内容,然后根据该字节的值即可选择正确的操作。

  redis中rdb.c源文件中的rdbSaveLen函数完成长度的编码,而rdbLoadLen完成长度值的解码。

2. 序列化int

  当需要序列化的值为整形值时,整形值会以二进制的形式直接进行序列化,而不是转换成ascii码的字符串形式进行序列化,从而进一步节约空间。此外,根据整形值的大小,redis同样以尽可能少的空间来存储该值。与字符串类似,首先有一个字节的高2bits设置为11代表后续的值是一个整形值,同时以低6bits的值区分使用的字节数。

  1.   (value >= -(1<<7) && value <= (1<<7)-1),低6bit值为0,后续1个字节存储整形值。
  2.   (value >= -(1<<15) && value <= (1<<15)-1),低6btis值为1,后续2个字节存储整形值。
  3.   (value >= -((long long)1<<31) && value <= ((long long)1<<31)-1),低6bits值为2,后续4个字节存储整形值。

整体形式如下:  

11000000[8 bit integer]
11000001[16 bit integer]
11000002[32 bit integer]

举例,序列化整形值0x300的结果如下:

0xC1 0x0300

0xC1表示后续是一个以int16形式存储的整形值,0x0300即为该整形值。由于第1个字节的高2bits为11,与string类型表示长度的格式不相同,所此可以区分是string还是int型。

  此外,若第1个字节的高2bits为11,低6bits为3(RDB_ENC_LZF),那么后续的数据为压缩数据,该字节后跟着两个长度值,分别表示压缩后的数据长度与压缩前的数据长度,再后面才是真正的数据。

3. RDB_TYPE_*与RDB_OPCODE_*

  Redis是一个key-value缓存系统,所有的值都以key-value的形式存储在db字典中。但是redis中value的类型并不仅限于string,它还可以是结构体类型,如set、list、hash等。为了序列化这些类型,redis中首先会以一个字节存储数据类型,然后如果是复合类型,会存储该类型的成员数目,然后遍历成员值并以基本的int或者string类型进行存储。

  Redis中定义了如下的类型值:

/* Map object types to RDB object types. Macros starting with OBJ_ are for
 * memory storage and may change. Instead RDB types must be fixed because
 * we store them on disk. */
#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
#define RDB_TYPE_ZSET_2 5 /* ZSET version 2 with doubles stored in binary. */
#define RDB_TYPE_MODULE 6
#define RDB_TYPE_MODULE_2 7 /* Module value with annotations for parsing without
                               the generating module being loaded. */
/* 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
#define RDB_TYPE_STREAM_LISTPACKS 15
/* NOTE: WHEN ADDING NEW RDB TYPE, UPDATE rdbIsObjectType() BELOW */

这些类型值代表了被序列化的value的类型,如list、set等,此外还代表了value的具体实现方式,如set可以使用hash表实现,相应类型值为RDB_TYPE_SET,也可以使用一个有序的整形数组实现,对应类型值为RDB_TYPE_SET_INTSET。反序列化时,首先读取1个字节的值判断后续数据的类型,然后进行相应的类型重建。

举例,一个string类型的key-value对 “key1” “hello, world”的序列化结果为:

RDB_TYPE_STRING 0x4 “key1” 0xC “hello, world”

首先1个字节的类型值RDB_TYPE_STRING,然后一个字节的长度值0x4,即后面有4个字节的关键字字符串,然后一个字节的长度值0xC,即后面有12个字节的字符串值。

反序列化时:

  1. 首先读取1个字节得RDB_TYPE_STRING,得到后续为一个string对象
  2. 然后读取长度值0x4,并读取相应的4个字节得到关键字,
  3. 最后再读取长度0xC,并读取相应的12字节值得到value值。

举例,一个list类型的key-value对:”key2” 2 3 4 0x300的序列化结果为:

RDB_TYPE_LIST 0x4 “key2” 0x4 0xc0 0x2 0xc0 0x3 0xc0 0x4 0xC1 0x0300

首先是1个字节的类型值RDB_TYPE_LIST,然后长度值为0x4,表示有4个字节的关键字字符串,然后是0x4表示该list有4个成员,然后4个成员依次以整形的格式序列化。

反序列化时:

  1. 首先读取1个字节得到RDB_TYPE_LIST,得到后续为一个list对象,
  2. 读取关键字的长度为0x4,读取4字节得到”key2”
  3. 读取长度0x4,得到list成员数目为4
  4. 读取长度,然后读取整形值,循环4次,完成list对象的重建。

  除了RDB_TYPE*与redis中存储的数据类型对应,还有一类RDB_OPCODE*表示一些其它的数据,如:RDB_OPCODE_EXPIRETIME表示后面的数据是该对象的超时时间;RDB_OPCODE_SELECTDB表示接下来的数据是一个db索引,直到遇到下一个RDB_OPCODE_SELECTDB之前,所有反序列化的数据都应该存储在该索引的db字典中。Redis中定义了如下类型的RDB_OPCODE*值:

/* Special RDB opcodes (saved/loaded with rdbSaveType/rdbLoadType). */
#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. */

这些特殊类型后续值的长度通常是固定的,如RDB_OPCODE_EXPIRETIME以32bit表示超时时间,单位为s;RDB_OPCODE_EXPIRETIME_MS后面是64bit表示的超时时间,单位为ms;RDB_OPCODE_SELECTDB后续是使用的db索引,以len的编码方式进行存储,而RDB_OPCODE_AUX表示一些key-value对,而这些key, value都是string与int等基本类型。

  除了rdb文件开头的固定字节的magic码,所有rdb序列化的数据都有一个前置的RDB_TYPE_*值或者RDB_OPCODE_*值,它表示了后续数据的存储方式,从而反序列化时采取正确的操作。

4. RDB序列化流程

1. 序列化前置信息,如magic识别码,版本号,时间戳

2.遍历db,序列化每一个db

   2.1 序列化RDB_OPCODE_SELETCTDB

   2.2 序列化RDB_OPCODE_RESIZEDB,存储该db的大小

   2.3 序列化db中的每一个key-value对

       2.3.1 序列化超时时间RDB_OPCODE_EXPIRETIME

       2.3.2 序列化lru值,RDB_OPCODE_IDLE

       2.3.3 序列化LFU值,RDB_OPCODE_FREQ

       2.3.4 序列化值类型

       2.3.5 序列化key(string)

       2.3.6 序列化value

在前一篇文件中介绍过redis的rio抽象层,而这些序列化操作正是以rio作为接口,以rio为目的地,既可以将序列化内容输出到文件,也可以将序列化内容输出到多个sockets中。普通的持久化操作使用文件作为输出对象,而在master-slave中的数据同步可能会使用到sockets作为输出对象,通过rio的抽象,将序列化与底层io进行解藕。

  redis中的序列化函数调用栈如下:

          

 

左图是输出对象为文件的序列化调用关系,右图是输出对象为sockets的序列化调用关系。

5. RDB反序列化流程

 1.读取9字节magic标志,并验证

循环进行2-3步

2. 读取1字节RDB_TYPE_*或者RDB_OPCODE_*标志

3. 根据RDB_OPCODE_*或者RDB_TYPE_*的值做相应的处理

上述第3步中,如果需要读取string或者int这种基本类型,处理过程为:

  1. 调用rdbLoadLen读取长度
  2. 根据rdbLoadLen返回值读取string或者整形值

如果对应的类型为复合类型,如list、set等,处理过程为:

  1. 调用rdbLoadLen读取复合类型成员数目
  2. 循环读取成员值,直到指定数目的值被读出。读取成员的操作即读取string或者int基本类型。

反序列化的输入为文件,即使序列化的输出目的地为sockets,接收端也会先将数据存储到一个文件中,然后再从文件反序列化。反序列化的调用栈为

  1. rdbLoad,初始化rio为文件流
  2. rdbLoadRio以rio为输入,从文件中读取数据并完成反序列化。

6. 后台进程

 由于redis是单线程模式,因此它选择将持久化操作放在子进程中进行,否则持久化过程中将停止响应请求。

  根据fork函数的特性,子进程创建后与父进程拥有相同的内存内容,因此fork函数调用后子进程即得到了此时db中的完整内容。并且由于copy-on-write特性,并不会发生大量的内存copy,仅在write操作发生时,相应的内存页才进行一个copy生成副本,即该操作也不会特别耗时。

  但相应的,父进程继续接受客户端的命令,修改的内容并不会反应到子进程的内存中,因此rdb持久化过程中出现的修改将会丢失。

posted @ 2019-10-06 17:14  yangzd~  阅读(511)  评论(0编辑  收藏  举报