相信能看到这的各位大神 肯定都知道 RDB(Redis DataBase)/ AOF(Append Only File) 两种 Redis 持久化机制,大多数也熟知两种方式的 优劣之处;但是 它们的 原理、存储方式、协议 等等方面 就不是所有人都清楚的了,所以我们接下来就分作两期 分别对 RDB 和 AOF 机制进行详细说明…
RDB(Redis DataBase)顾名思义 Redis 数据存储,将 Redis 服务器里数据以 文件的形式 存入 硬盘 加以备份
- 优势:RDB 为一个紧致的二进制文件,保存了截止到某个点的所有数据集,极其适合备份,可以方便根据业务进行不同时机的备份,由于只有一个文件,无论是在灾备还是在恢复上都十分便利。备份的时候可以选择 fork 出子进程进行持久化操作,这样就可以极大的避免影响服务进程的 IO 操作
- 劣势:由于 RDB 每次备份可能由于 数据集 比较大而 在子进程中 运行一段时间,所以通常都是 定时执行,但是,如果业务对数据的高可用性要求很高,甚至不能容忍几分钟少量的数据丢失,那么 RDB 恐怕不是一个好的选择,毕竟服务器一旦在定时执行之前宕机,那么这段时间的数据将丢失掉
RDB文件创建和载入
手动创建
手动操作创建 RDB文件的 有 SAVE
和 BGSAVE
两个命令
SAVE
命令会阻塞 Redis 服务进程,直到 RDB 文件创建完毕,在服务器阻塞期间,服务器不能处理任何命令请求BGSAVE
命令不会阻塞 Redis 服务进程,不过会 fork 一个子进程,然后由子进程负责创建 RDB 文件,服务器进程继续处理命令请求;但是在执行BGSAVE
命令过程中:SAVE
命令会被服务器拒绝,服务器禁止SAVE
和BGSAVE
命令同时执行,是因为 服务进程 和 子进程 同时执行 rdbsave,产生竞争BGSAVE
命令也会被拒绝BGREWRITEAOF
命令会被延迟到BGSAVE
执行完以后才能执行
自动创建
由于 BGSAVE
命令是不阻塞服务进程的情况下执行的,所以 Redis 允许用户通过设置服务器配置 save 选项,让服务器每隔一段时间自动执行一次 BGSAVE
命令,假设我们在 conf 文件里面配置了如下一段策略(当然,如果没有在 conf 文件里面进行设置,服务器也会默认设置下方这三条策略):
以上三个条件只要有任意一个符合,则会被执行,分别代表如下含义:
- 服务器在 900秒 以内,对数据库进行了至少 1次 修改
- 服务器在 300秒 以内,对服务器进行了至少 10次 修改
- 服务器在 60秒 以内,对服务器进行了至少 10000次 修改
我们测试一下第三种情况,比如我们用 脚本在短时间内执行了 10000次 set 命令,那么 服务器会有如下日志显示:
那么自动执行是如何做到的呢,我们继续来看
redisServer 里面除了 saveparam 数组外,需要注意的是 还维护着 dirty 计数器 以及 lastsave 属性
dirty 计数器
记录距离上次成功执行 SAVE命令 或者 BGSAVE命令 之后,服务器对数据库状态(服务器中的所有数据库)进行了多少次修改(包括 写入、删除、更新 等操作)lastsave 属性
为一个 UNIX时间戳,记录了服务器上一次成功执行 SAVE命令 或者 BGSAVE命令 的时间
Redis 服务器周期操作函数 serverCron()
默认每隔 100毫秒
就会执行一次,该函数用于正在运行的服务器进行维护,它们的其中一项工作就是检查 save 选项所设置的条件是否有一项被满足,如果满足的话就行执行 BGSAVE 命令
载入
与 RDB 文件的创建不同,RDB 文件的载入工作是在服务器启动的时候自动执行的,所以 Redis 并没有专门用于载入 RDB 文件的命令,只要 Redis 服务器在启动的时候检测到 RDB 文件存在,就会自动载入 RDB 文件,启动的时候会有一段这样的日志显示
另外呢,因为 AOF 文件的更新频率通常比 RDB 文件要高,所以
- 如果服务器开启了 AOF 持久化功能,那么服务器会优先使用 AOF 文件来还原数据库状态
- 只有在 AOF 关闭的状态下,服务器才会使用 RDB 来还原数据库状态
RDB 文件结构
上面我们介绍了 Redis 服务器 RDB 文件的创建 以及 载入策略,下面我们对 RDB 文件的结构进行详细介绍
通过上面的示例图,我们可以看到 从整体上看 RDB 文件主要包含如下几个部分
REDIS 标识
REDIS 字符串用于标识是 Redis 的 RDB文件RDB 版本
RDB 文件的版本号,比如我们现在使用的是0008
版本辅助信息
REDIS_version
REDIS 版本号REDIS_bits
REDIS 服务器 操作系统位数ctime
系统当前时间used-mem
REDIS 已使用的内存数aof-preamble
是否为启用的 RDB+AOF 混合持久化模式,这个我们放在 AOF 结束的时候进行说明
databases 数据库主体
数据库主体部分由 每一个非空的数据库 相邻组成,比如 database 0 ~ database 15selectdb
标识当前进行切换数据库操作db_number
当前数据库 iddb_size
当前数据库键值对个数expire_size
当前数据库过期键个数键值对数据
这个比较复杂,我们下面进行讲解
EOF 结束符
1 字节,标志 RDB 文件正文内容结束,键值对加载完毕,默认RDB_OPCODE_EOF = 255
校验和
8字节的 CRC64 表示的文件校验和,用来验证 文件数据内容的有效性,防篡改以及数据缺失
长度编码
在讲解下面键值对数据之前,由于 C 中对指针指向的内存是无法计算长度的,于是 我们通常都是将该段内存的大小标识出来,在 Redis 中有很多长度信息需要保存,包括下面的键值对里面涉及的所有标识长度的部分,所以 我们先普及一个 长度编码(length encoding)
的概念
那么大家就会问了,直接使用 int 存储这个长度不就完了,怎么还特地整出来一个 长度编码呢?这是因为我们平时使用到的长度可能并不大,一个 int 4个字节是不是太浪费了。所以 Redis 就采用了一个 变长的编码方式,将不同大小的数字编码成不同的长度
长度编码 的使用中,我们一般首先读取 1个字节,其中 前两位 作为编码类型的判断
00
代表剩下的 6位 就直接表示具体长度01
那么就再读取后面的 1个字节 8位,加上紧挨着后续的 6位,一共 14位 来表示具体长度10
那么紧挨着后续的 6位 就废弃了,直接再读取后面的 4个直接,总共 32位 来表示具体长度11
代表一个特殊编码,紧挨着后续的 6位 用于标识 特殊编码 的种类,特殊编码主要用于 将数字存成字符串,或者编码后的字符串000000
代表 再往后 8位 表示该整数000001
代表 再往后 16位 表示该整数000010
代表 再往后 32位 表示该整数000011
代表 后续为 LZF 压缩字符串
那么这样
- 0 – 63 的数字 只需要 1个字节 存储
- 64 – 16383 的数字 只需要 2个字节 存储
- 16383 - 2^32 -1 的数字 只需要用 5个字节(1个字节 的标识加 4个字节 的值)存储
键值对数据
键值对数据(key_value_pair)涉及内容比较多,我们单独进行解析
其中 expirestime_ms
(过期信息标识) 和 ms
(过期时间) 根据 键值对的实际设置情况,可能有,也可能没有
type
部分存储 键值对类型,key
部分使用字符串的形式存储 键的内容,我们着重讲解一下每种数据类型的具体情况
字符串类型
字符串类型的存储分为两种形式 OBJ_ENCODING_INT
和 OBJ_ENCODING_RAW
OBJ_ENCODING_INT
保存对象是长度不超过 32 位的整数,超过以后转换为字符串保存OBJ_ENCODING_RAW
- 长度小于等于 20 字节 使用 普通字符串直接 存储
- 长度大于 20 字节 使用 LZF 压缩字符串 存储12345678910111213// OBJ_ENCODING_INT 编码结构// 8位数字11|000000 xxxxxxxx|// 16位数字11|000001 xxxxxxxx xxxxxxxx|// 32位数字11|000010 xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx|// OBJ_ENCODING_RAW// 普通字符串00|xxxxxx|字符串内容// LZF 压缩字符串,其中下面 长度也是符合 长度编码原理的11|000011|压缩后的长度|原长度|压缩后的数据
列表类型
列表类型的结构比较简单了,不过里面还是有可能有 元素比较长,类似 字符串类型一样 使用了 LZF 压缩字符串
集合类型
集合类型比较类似列表类型,跟之前结构图上一致
有序集合类型
有序集合类型类似列表类型,跟之前结构图上一致,不过区别的是 score 存储的时候还是按照 数字方式存储
哈希类型
哈希类型的结构也比较简单,跟之前结构图上一致
另外,这里还有一个技巧,就是使用 od 命令进行查看 rdb 文件,再结合上面的结构讲解,还是能看出来一些东西
源码解析
由于有些方法里的代码量比较大,我们这里按照 典型的代码片段进行解析,同志们可以根据文章提示的代码位置 和 代码里面的关键词 在源码中搜素,可能数据结构一些元素 看不太懂什么意思,没关系,先混个脸熟,后面看完回头再看过来就明白了
自动保存 RDB 文件是通过系统的 自动函数serverCron()
的逻辑中进行触发
手动 SAVE 命令,函数分别为 saveCommand()
和 bgsaveCommand()
,不过具体的执行逻辑为 rdbSave()
函数 和 rdbSaveBackground()
函数,而且 bgsave 命令在 fork 子进程以后,依然还是使用 rdbSave()
函数进行操作
根据上面的逻辑,rdb 保存的底层逻辑还需要继续看 rdbSaveRio()
函数
|
|
上面我们看到了一些地方调用 rdbSaveType()
和 rdbWriteRaw()
,不过具体逻辑还是 rioWrite()
函数,这个函数主要是 rdb 的 io 操作,用来写入 rdb 文件
写入辅助信息函数 rdbSaveInfoAuxFields()
rdbSaveRawString()
函数主要逻辑为 将 字符串对象以 len+data 形式写入 RDB 文件
压缩字符串保存函数 rdbSaveLzfStringObject()
,而在经过压缩过的字符串内容的保存逻辑 rdbSaveLzfBlob()
函数
长度编码 写入函数 rdbSaveLen()
,跟这个函数逻辑比较类似的为 整数长度编码函数rdbEncodeInteger()
,逻辑也比较简单,看这段的时候可以参考上面的 长度编码部分
保存逻辑继续走下去,直到键值对保存的入口函数 rdbSaveKeyValuePair()
保存键值对 类型标识函数 rdbSaveObjectType()
键的保存是以字符串的形式进行的,这个我们跟下面的 字符串类型值保存函数 一块讲解
我们先看一下 值的保存逻辑,函数为 rdbSaveObject()
|
|
字符串形式保存函数 rdbSaveStringObject()
RDB 文件数据加载逻辑为 上面写入逻辑 的逆操作,入口为 redis server 启动流程中的 loadDataFromDisk()
函数,主要的操作逻辑为 rdbLoadRio()
本文作者: wettper
本文链接: http://www.web-lovers.com/redis-source-rdb.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!