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个字节存储整形值。

整体形式如下:

[ bit integer]
[ bit integer]
[ 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持久化过程中出现的修改将会丢失。

redis源码分析(三)--rdb持久化的更多相关文章

  1. redis源码分析(四)--aof持久化

    Redis aof持久化 Redis支持两种持久化方式:rdb与aof,上一篇文章中已经大致介绍了rdb的持久化实现,这篇文章主要介绍aof实现. 与rdb方式相比,aof会使用更多的存储空间,因为它 ...

  2. Redis源码分析:serverCron - redis源码笔记

    [redis源码分析]http://blog.csdn.net/column/details/redis-source.html   Redis源代码重要目录 dict.c:也是很重要的两个文件,主要 ...

  3. redis源码分析之事务Transaction(下)

    接着上一篇,这篇文章分析一下redis事务操作中multi,exec,discard三个核心命令. 原文地址:http://www.jianshu.com/p/e22615586595 看本篇文章前需 ...

  4. tomcat源码分析(三)一次http请求的旅行-从Socket说起

    p { margin-bottom: 0.25cm; line-height: 120% } tomcat源码分析(三)一次http请求的旅行 在http请求旅行之前,我们先来准备下我们所需要的工具. ...

  5. 使用react全家桶制作博客后台管理系统 网站PWA升级 移动端常见问题处理 循序渐进学.Net Core Web Api开发系列【4】:前端访问WebApi [Abp 源码分析]四、模块配置 [Abp 源码分析]三、依赖注入

    使用react全家桶制作博客后台管理系统   前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基 ...

  6. redis源码分析之发布订阅(pub/sub)

    redis算是缓存界的老大哥了,最近做的事情对redis依赖较多,使用了里面的发布订阅功能,事务功能以及SortedSet等数据结构,后面准备好好学习总结一下redis的一些知识点. 原文地址:htt ...

  7. redis源码分析之事务Transaction(上)

    这周学习了一下redis事务功能的实现原理,本来是想用一篇文章进行总结的,写完以后发现这块内容比较多,而且多个命令之间又互相依赖,放在一篇文章里一方面篇幅会比较大,另一方面文章组织结构会比较乱,不容易 ...

  8. redis源码分析之有序集SortedSet

    有序集SortedSet算是redis中一个很有特色的数据结构,通过这篇文章来总结一下这块知识点. 原文地址:http://www.jianshu.com/p/75ca5a359f9f 一.有序集So ...

  9. Redis源码分析(intset)

    源码版本:4.0.1 源码位置: intset.h:数据结构的定义 intset.c:创建.增删等操作实现 1. 整数集合简介 intset是Redis内存数据结构之一,和之前的 sds. skipl ...

随机推荐

  1. podium micro-frontends 简单试用

    以下是一个简单的podium 试用,包含了layout 以及podlets,使用docker 运行 podium 主要包含了两大部分 podlets 片段服务 layouts 片段组合服务 环境准备 ...

  2. 原生js如何判断元素出现在可视区

    元素出现在可视区 scorll滑动的距离>=当前元素距离浏览器最顶端的高度+当前元素自身的高度-当前可视区的高度 触底 scorll滑动的距离>=当前scroll总高度-当前可视区的高度

  3. python实现余弦相似度文本比较

    向量空间模型VSM: VSM的介绍: 一个文档可以由文档中的一系列关键词组成,而VSM则是用这些关键词的向量组成一篇文档,其中的每个分量代表词项在文档中的相对重要性. VSM的例子: 比如说,一个文档 ...

  4. Android编程权威指南笔记

    学习android开发,你需要对java语言,包括类,对象,接口,监听器,包,内部类,匿名内部类有所了解. android studio 的下载与安装 可以从android开发者网站下载android ...

  5. GoCN每日新闻(2019-10-30)

    GoCN每日新闻(2019-10-30) GoCN每日新闻(2019-10-30) 1. Asta Xie: 玩转Go语言,从beego开始 https://mp.weixin.qq.com/s/Io ...

  6. Reconstructing Cloud-Contaminated Multispectral Images With Contextualized Autoencoder Neural Networks(自编码机重建云污染区)

    1.逐像元输入输出与邻域输入输出,邻域处理是先flatten,再unflatten 2.用MDL方法(最小描述长度)寻找自编码机最佳隐藏层数 3.多目标优化方法寻找MDL方法的超参数,平衡MDL方法两 ...

  7. kkk

    #!/usr/bin/env python3# coding=utf-8# Author: yannanxiu"""create_rsa_key() - 创建RSA密钥m ...

  8. MP实战系列(十九)之批量新增

    批量操作在实际开发中也应用非常多,例如批量下发优惠券.批量添加用户等. 以MyBatis为例,通常实现批量操作,有这么几种方式? 第一,单条插入sql语句,进行for循环遍历,基准条件是根据前端传过的 ...

  9. Octopus501工作站 安装记录

    cmake libreadline-dev 没有运行程序,nvidia-smi查看GPU-Util 达到100% 解决方案:需要把驱动模式设置为常驻内存才可以,设置命令:nvidia-smi -pm ...

  10. Python3中通过fake_useragent生成随机UserAgent

    安装和使用 fake_useragent第三方库,来实现随机请求头的设置: GitHub               ---> https://github.com/hellysmile/fak ...