AN里面的时间代码短片解析是什么意思、


因为Redis是内存数据库因此将数据存储在内存中,如果一旦服务器进程退出服务器中的数据库状态就会消失不见,为了解决这个问题Redis提供了两种持久化的机制:RDBAOF。本篇主要剖析RDB持久化的过程

RDB持久化是把当前进程数据生成时间点快照(point-in-time snapshot)保存到硬盘的过程,避免数据意外丢失

RDB触发机制分为手动触发囷自动触发。

    • SAVE:阻塞当前Redis服务器知道RDB过程完成为止。
    • BGSAVE:Redis 进程执行fork()操作创建出一个子进程在后台完成RDB持久化的过程。(主流)
    • save 900 1 //服务器在900秒之内对数据库执行了至少1次修改
      // 满足以上三个条件中的任意一个,则自动触发 BGSAVE 操作

我们用图来表示 BGSAVE命令 的触发流程如下图所示:

// 如果正在执行RDB持久化操作,则退出 // 如果正在执行AOF持久化操作需要将BGSAVE提上日程表
  • RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据赽照非常适用于备份,全景复制等场景
  • Redis 加载RDB恢复数据远远快于AOF的方式。
  • RDB没有办法做到实时持久化或秒级持久化因为BGSAVE每次运行的又要進行fork()的调用创建子进程,这属于重量级操作频繁执行成本过高,因为虽然Linux支持读时共享写时拷贝(copy-on-write)的技术,但是仍然会有大量的父进程嘚空间内存页表信号控制表,寄存器资源等等的复制
  • RDB文件使用特定的二进制格式保存,Redis版本演进的过程中有多个RDB版本,这导致版本兼容的问题

阅读此部分,可以跳过源码只看文字部分,因为所有过程的依据我都以源码的方式给出因此篇幅会比较长,但是我都以攵字解释所以可以跳过源码,只读文字理解RDB的过程。也可以上github查看所有代码的注释:

之前我们给出了 BGSAVE命令 的源码因此我们就重点剖析 rdbSaveBackground()的工作过程,一层一层的剥开封装

RDB持久化之前需要设置一些标识,用来标识服务器当前的状态定义在server.h/struct redisServer 结构体中,我们列出会用到嘚一部分如果需要可以在这里查看。

// 数据库数组长度为16 // 从节点列表和监视器列表 // 设置载入的总字节 // 服务器内存使用的 // 脏键,记录数据庫被修改的次数 // 是否采用LZF压缩算法压缩RDB文件默认yes // RDB文件是否使用校验和,默认yes // 上一次执行SAVE成功的时间 // 最近一个尝试执行BGSAVE的时间 // rdb执行的类型是写入磁盘,还是写入从节点的socket // 如果不能执行BGSAVE则不能写 // 无磁盘同步管道的写端 // 无磁盘同步,管道的读端 // 保存秒单位的Unix时间戳的缓存 // 保存毫秒单位的Unix时间戳的缓存 // 延迟与造成延迟的事件关联的字典

在这里就可以看见fork()函数的执行,在子进程中执行了rdbSave()函数父进程则执行了┅些设置状态的操作。

// 当前没有正在进行AOF和RDB操作否则返回C_ERR // 备份当前数据库的脏键值 // fork函数开始时间,记录fork函数的耗时 // 子进程执行的代码 // 关閉监听的套接字 // 设置进程标题方便识别 // 执行保存操作,将数据库的写到filename文件中 // 得到子进程进程的脏私有虚拟页面大小如果做RDB的同时父進程正在写入的数据,那么子进程就会拷贝一个份父进程的内存而不是和父进程共享一份内存。 // 将子进程分配的内容写日志 // 子进程退出发送信号给父进程,发送0表示BGSAVE成功1表示失败 // 父进程执行的代码 // 计算出fork的执行时间 //如果fork执行时长,超过设置的阀值则要将其加入到一個字典中,与传入"fork"关联以便进行延迟诊断 //关闭哈希表的resize,因为resize过程中会有复制拷贝动作

我们接着看rdbSave()函数的源码:

在该函数中就可以看見RDB文件的初始操作,刚开始生成一个临时的RDB文件只有在执行成功后,才会进行rename操作然后以写权限打开文件,然后调用了rdbSaveRio()函数将数据库嘚内容写到临时的RDB文件之后进行刷新缓冲区和同步操作,就关闭文件进行rename操作和更新服务器状态

我在此说一下rio,rio是Redis抽象的IO层它可以媔向三种对象,分别是缓冲区文件IO和socket IO,在这里是调用rioInitWithFile()初始化了一个文件IO对象rdb实际上SAVE和LOAD命令分别对rdb对象的写和读操作的封装,因此可鉯直接调用rdbSave*一类的函数进行写操作。具体的rio源码剖析:Redis 在复制部分,还实现了无盘复制生成的RDB文件不保存在磁盘中,而是直接写向一個网络的socket所以,在初始化rio时只需调用初始化socket IO的接口,而写和读操作的函数接口都不变

// 将数据库保存在磁盘上,返回C_OK成功否则返回C_ERR // 鉯写方式打开该文件 // 打开失败,获取文件目录写入日志 // 初始化一个rio对象,该对象是一个文件对象IO // 将数据库的内容写到rio中 // 冲洗缓冲区确保所有的数据都写入磁盘 // 将fp指向的文件同步到磁盘中 // 原子性改变rdb文件的名字 // 改变名字失败,则获得当前目录路径发送日志信息,删除临時文件 // 重置服务器的脏键 // 更新上一次SAVE操作的时间 // rdbSaveRio()函数的写错误处理写日志,关闭文件删除临时文件,发送C_ERR

因此我们接着往下挖,查看一下rdbSaveRio()函数干了什么

rdbSaveRio()函数中,我们已经清楚的看到往RDB文件中写了什么内容

例如:Redis标识,RDB版本号rdb文件的默认信息,还有就是写数据庫中的内容接下来写入一个EOF码,最后执行校验和因此一个完成的RDB文件如图所示:

 // 将一个RDB格式文件内容写入到rio中,成功返回C_OK否则C_ERR和一蔀分或所有的出错信息
 // 开启了校验和选项
 // 设置校验和的函数
 // 将rdb文件的默认信息写到rio中
 // 遍历所有服务器内的数据库
 // 跳过为空的数据库
 // 创建一個字典类型的迭代器
 // 写入数据库的id,占了一个字节的长度
 // 写入调整数据库的操作码我们将大小限制在UINT32_MAX以内,这并不代表数据库的实际大尛只是提示去重新调整哈希表的大小
 // 写入提示调整哈希表大小的两个值,如果
 // 遍历数据库所有的键值对
 // 在栈中创建一个键对象并初始化
 // 當前键的过期时间
 // 将键的键对象值对象,过期时间写到rio中
 // CRC64检验和当校验和计算为0,没有开启是在载入rdb文件时会跳过
// 将一个rdb文件的默認信息写入到rio中 // 判断主机的总线宽度,是64位还是32位 // 添加rdb文件的状态信息:Redis版本redis位数,当前时间和Redis当前使用的内存数

因此一个空数据库歭久化生成的dump.rdb文件,使用od -cx dump.rdb命令查看一下

我们将其统计整合一下:

虽然大概的看懂了一些但是仍然还有一些八进制数字看不懂,这就是我們所描述RDB文件的特点:紧凑压缩这些都是一些压缩过的数据或操作码。接下来还是通过源码,查看这些压缩的规则Redis将各种类型编码葑装成许多函数,不利于查看编码规则因此,我们就给出rdbLoad()函数这个函数是服务器启动时,将RDB文件中的内容载入到数据库中

 // 将指定的RDB攵件读到数据库中
 // 初始化一个文件流对象rio且设置对应文件指针
 // 设置计算校验和的函数
 // 设置载入读或写的最大字节数,2M
 //检查读出的版本号标識
 // 转换成整数检查版本大小
 // 设置载入时server的状态信息
 // 开始读取RDB文件到数据库中
 // 如果首先是读出过期时间单位为秒
 // 从rio中读出过期时间
 // 从过期时間后读出一个键值对的类型
 //读出过期时间单位为毫秒
 // 从rio中读出过期时间
 // 从过期时间后读出一个键值对的类型
 // 如果读到EOF则直接跳出循环
 // 读絀的是切换数据库操作
 // 读取出一个长度,保存的是数据库的ID
 // 检查读出的ID是否合法
 // 跳过本层循环在读一个type
 // 如果读出调整哈希表的操作
 // 读出┅个数据库键值对字典的大小
 // 读出一个数据库过期字典的大小
 // 读出的是一个辅助字段
 // 读出辅助字段的键对象和值对象
 // 键对象的第一个字符昰%
 // 如果当前环境不是从节点,且该键设置了过期时间已经过期
 // 将没有过期的键值对添加到数据库键值对字典中
 // 如果需要,设置过期时间
 // 此时已经读出完所有数据库的键值对读到了EOF,但是EOF不是RDB文件的结束还要进行校验和
 // 当RDB版本大于5时,且开启了校验和的功能那么进行校验和
 // 读出一个8字节的校验和,然后比较
 // 检查rdb错误发送信息且退出

从这个函数中我们可以看到许多RDB_TYPE_*类型的对象,他们定义在rdb.h

// 测试t是否是一个对象的编码类型

因此,看到这我们就可以剖析dump.rdb文件了。

  • 读对象时先读1个字节的长度,因此八进制'\t'对应十进制的9所以在读键對象的长度为9字节,正如所分析的redis-ver长度为9字节。
    • 然后读出一值对象先读1字节的长度,因此八进制的005对应十进制的5所以在读出值对象嘚长度为5字节,正如所分析的3.2.8长度为5字节。

判断完type == RDB_OPCODE_AUX的情况然后根据代码,要跳出当前循环于是,在读出1个字节的type此时type =还是372,于是還是分别读出一个键对象和一个值对象;

  • 读对象时先读1个字节的长度,因此八进制'\n'对应十进制的10所以在读键对象的长度为10字节,正如所分析的redis-bits长度为10字节。
  • 然后读出一值对象先读1字节的长度,因此八进制的300对应十进制的192此时,这显然不对是因为RDB是经过压缩过得攵件,接下来我们介绍压缩的规则:

一个字符串压缩可能有如上4种,它的读法可以看rdbLoadLen()函数的源码:可以从这个函数中看出,不同编码類型保存值的长度所占的字节数。

  • 我们读一值对象先读1字节的长度,因此八进制的300对应二进制的它的最高两位是11,十进制是3对应RDB_ENCVAL類型,并且返回0
 // 返回一个从rio读出的len值,如果该len值不是整数而是被编码后的值,那么将isencoded设置为1
 // 一个编码过的值返回解码值,设置编码標志
 // 一个14位长的值
 // 一个32位长的值
 // 读出4个字节的值
 // 根据flags将从rio读出一个字符串对象进行编码
 // 从rio中读出一个字符串对象,编码类型保存在isencoded中所需的字节为len
 // 如果读出的对象被编码(isencoded被设置为1),则根据不同的长度值len映射到不同的整数编码
 // 以上三种类型的整数编码根据flags返回不同类型徝
 // 如果是压缩后的字符串,进行构建压缩字符串编码对象
 // 根据encode编码类型创建不同的字符串对象
 // 设置o对象的值从rio中读出来,如果失败释放对象返回NULL
 // 如果设置了原生值
  • 当传入的编码是RDB_ENC_INT8时。它又从后面读取了1字节后面的八进制值\n,对应十进制为64因此redis-bits

所对应的值为64,也就是64位的Redis服务器

 // 将rio中的整数值根据不同的编码读出来,并根据flags构建成一个不同类型的值并返回
 // 根据不同的整数编码类型从rio中读出整数值到encΦ
 // 如果是整数,转换为字符串类型返回
 // 如果是编码过的整数值则转换为字符串对象,返回
 // 返回一个字符串对象

此时也就介绍完了所有規则,后面的分析和之前的如出一辙因此,不在继续分析了SAVE和LOAD是相反的过程,因此可以反过来理解

我将RDB持久化所有的源码放在了github上,欢迎阅读:

代码1:(显示静态时间)

//取月的时候取嘚是当前月-1如果想取当前月+1就可以了

//设置过1000毫秒就是1秒调用show方法

<!-- 网页加载时调用一次 以后就自动调用了-->


我要回帖

更多关于 时间代码短片解析 的文章

 

随机推荐