【Redis源码】RDB持久化源码实现

RDB执行流程

RDB快照有两种触发方式,其一为通过配置参数,例如在配置文件中写入如下配置:

save 60 1000

则在60秒内如果有1000个key发生变化,就会触发一次RDB快照的执行。

其二是通过在客户端执行bgsave命令显式触发一次RDB快照的执行。bgsave执行流程如图20-1所示。

image.png

在客户端输入bgsave命令后,Redis调用bgsaveCommand函数,该函数fork一个子进程执行rdbSave函数进行实际的快照存储工作,而父进程可以继续处理客户端请求。当子进程退出后,父进程调用相关回调函数进行后续处理。

RDB文件结构

本节会先介绍整体文件结构,然后分别介绍RDB键的保存形式(即字符串的保存)和值的保存形式(根据数据类型有不同的保存方法)。

整体文件结构

RDB的整体文件结构如图所示。

image.png

各个部分按顺序详细介绍如下。

  • 头部5字节固定为“REDIS”字符串。
  • 4字节的RDB版本号(RDB_VERSION,注意不是Redis的版本号),当前RDB版本号为9,填充为4字节之后为0008。
  • 辅助字段(AUX_FIELD_KEY_VALUE_PAIRS,见表20-1)。

image.png

辅助字段可以标明以下信息。

  • 数据库序号:指明数据需要存放到哪个数据库。
  • 当前数据库键值对散列表的大小。Redis的每个数据库是一个散列表,这个字段指明当前数据库散列表的大小。这样在加载时可以直接将散列表扩展到指定大小,提升加载速度。
  • 当前数据库过期时间散列表的大小。Redis的过期时间也是保存为一个散列表,该字段指明当前数据库过期时间散列表的大小。
  • Redis中具体键值对的存储。
  • RDB文件结束标志。
  • 8字节的校验码。

通过上述结构的描述,请思考:加载RDB文件的时候怎么区分加载的是辅助字段还是数据库序号或者是其他类型呢?其实,在RDB每一部分之前都有一个类型字节,在Redis中称为opcodes。如下所示:

#define RDB_OPCODE_MODULE_AUX 247   /*  module相关辅助字段 */
#define RDB_OPCODE_IDLE       248   /* lru空闲时间 */
#define RDB_OPCODE_FREQ       249   /*  lfu频率*/
#define RDB_OPCODE_AUX        250   /*  辅助字段类型 */
#define RDB_OPCODE_RESIZEDB   251   /* ESIZEDB,即上文中介绍的5和6两项 */
#define RDB_OPCODE_EXPIRETIME_MS 252    /* 毫秒级别过期时间 */
#define RDB_OPCODE_EXPIRETIME 253       /*  秒级别过期时间*/
#define RDB_OPCODE_SELECTDB   254   /*  数据库序号,即第4项*/
#define RDB_OPCODE_EOF        255   /* 结束标志,即第8项*/

RDB带opcodes的整体文件结构如图20-3所示。

image.png

第2项辅助字段涉及字符串类型在RDB文件中的保存方式,可以参考本节后面将要讲解的内容。为了叙述方便,接下来对RDB结构的介绍中不会特意加上opcodes。

键值对结构

下面具体介绍一下第7项——键值对的结构,如图20-4所示。

image.png

  • EXPIRE_TIME:可选。根据具体的键是否有过期时间决定,该字段固定为8个字节。
  • LRU或者LFU:可选。根据配置的内存淘汰算法决定。LRU算法保存秒级别的时间戳,LFU算法只保存counter的计数(0~255,1字节)。
  • VALUE_TYPE:值类型。Redis数据类型和底层编码结构,值类型对应关系见表20-2。

image.png

表20-2中字符串在Redis中都是宏,括号中即为该宏对应的值。

  • KEY:键。键保存为字符串,下文会详细介绍字符串的保存形式。
  • ·VALUE:值。值根据数据类型和编码结构保存为不同的形式,下文详细介绍。

键的保存形式

Redis中键都是字符串,所以本节介绍的就是RDB中如何保存一个字符串,其实也是一个比较常见的方法,如图20-5所示。

image.png

前边LENGTH字段表示字符串长度,后边STRING即具体的字符串内容。LENGTH为了通用可以使用8个字节保存,但这样很明显会导致空间的浪费。Redis中的LENGTH是个变长字段,通过首字节能够知道LENGTH字段有多长,然后读取LENGTH字段可以知道具体的STRING长度。LENGTH字段类型如下:

00xxxxxx 字符串长度<64
01xxxxxx xxxxxxxx 字符串长度<16384
10000000 xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx 字符串长度<=UINT32_MAX
10000001 xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx 字符串长度>UINT32_MAX

首字节头两个比特是00时,表示LENGTH字段占用1个字节,STRING的长度保存在后6个比特中,最长为63。

同理当首字节头两个比特是01时,表示LENGTH字段占用2个字节,而STRING的长度保存在后14个字节中,最长为16383。

如果首字节为10000000,则表示LENGTH字段共占用5个字节,正好是一个无符号整型,STRING的长度最长为UINT32_MAX。

如果STRING长度大于UINT32_MAX,则首字节表示为10000001,LENGTH字段共占用9个字节。后8字节表示实际长度,为一个LONG类型。

RDB中对字符串的保存还有两种优化形式:一种是尝试将字符串按整型保存,一种是通过将字符串进行LZF压缩之后保存。下边分别讨论这两种情况。

image.png

TYPE字段其实类似图20-5中的LENGTH字段,LENGTH字段首字节头两个比特取值为00、01、10这种类型,TYPE字段首字节头两个比特取值为11,后6个比特表明存储的整型类型,如下:

11000000 xxxxxxxx INT8 取值范围[-128,127]
11000001 xxxxxxxx xxxxxxxx INT16 取值范围[-32768,32767]
11000010 xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx INT32 取值范围[-2147483648, 2147483647]

后6个比特是000000,表明存储的是一个有符号INT8类型,000001表明存储的是一个有符号INT16类型,000010表明存储的是一个有符号INT32类型。更长的长度直接按字符串类型保存。

image.png

TYPE首字节头两个比特仍然为11,后六个比特是000011。COMPRESS_LEN表明压缩之后的长度,该字段保存形式同图20-5中LENGTH字段的保存。LZF还保存了一个ORIGINAL_LEN字段,该字段记录压缩之前原始字符串的长度,保存形式也与图20-5中LENGTH字段的保存相同。最后一个DATA字段保存具体的LZF压缩之后的数据,数据长度从COMPRESS_LEN字段取得。

至此,RDB如何保存一个字符串已经讲解完毕。

转载自:Redis 5设计与源码实现



标 题:《【Redis源码】RDB持久化源码实现
作 者:zeekling
提 示:转载请注明文章转载自个人博客:小令童鞋

评论

  1. 我这篇也是抄的。

  2. 哦哦~谢谢大佬,那我抄走啦😄

  3. 在开源博客的皮肤上面做了增强:https://git.zeekling.cn/zeekling/bolo-fantastic

  4. 大佬,你的博客空间好好看,是用了什么特别的技巧吗?👀️

取消