热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:Redis中的数据持久化策略(RDB)

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Redis中的数据持久化策略(RDB)相关的知识,希望对你有一定的参考价值。Redis是一个内存数据库,所有的数据都

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Redis 中的数据持久化策略(RDB)相关的知识,希望对你有一定的参考价值。


Redis 是一个内存数据库,所有的数据都直接保存在内存中,那么,一旦 Redis 进程异常退出,或服务器本身异常宕机,我们存储在 Redis 中的数据就凭空消失,再也找不到了。

Redis 作为一个优秀的数据中间件,必定是拥有自己的持久化数据备份机制的,redis 中主要有两种持久化策略,用于将存储在内存中的数据备份到磁盘上,并且在服务器重启时进行备份文件重载。

RDB 和 AOF 是 Redis 内部的两种数据持久化策略,这是两种不同的持久化策略,一种是基于内存快照,一种是基于操作日志,那么本篇就先来讲讲 RDB 这种基于内存快照的持久化策略。


一、什么是 RDB 持久化策略

RDB(redis database),快照持久化策略。RDB 是 redis 默认的持久化策略,你可以打开 redis.conf,默认会看到这三条配置。

技术图片

save 900 1 900秒内执行一次set操作 则持久化1次
save 300 10 300秒内执行10次set操作,则持久化1次
save 60 10000 60秒内执行10000次set操作,则持久化1次

RDB 又分为两种,一种是同步的,调用 save 命令即可触发 redis 进行 RDB 文件生成备份,但是这是一个同步命令,在备份完成之前,redis 服务器不响应客户端任何请求。另一种是异步的,调用 bgsave 命令,redis 服务器 fork 一个子进程进行 RDB 文件备份生成,与此同时,主进程依然可以响应客户端请求。

显然,异步的 RDB 生成策略才是主流,除了某些特殊情况,相信不会有人会在生产环境中用 save 命令阻塞 redis 服务来生成 RDB 文件的。

以上我们介绍的两个命令,save 和 bgsave,这两个命令需要我们手动的在客户端发送请求才能触发,我们叫做主动触发。

而我们之前匆匆介绍过的配置触发,这种我们叫做被动触发,被动触发有一些配置,下面我们来看看。

1、save 配置

save 配置是一个非常重要的配置,它配置了 redis 服务器在什么情况下自动触发 bgsave 异步 RDB 备份文件生成。

基本语法格式:

save

当 redis 数据库在 秒内,数据库中的 keys 发生了 次变化,那么就会触发 bgsave 命令的调用。

2、dbfilename 配置

dbfilename 配置项决定了生成的 RDB 文件名称,默认配置为 dump.rdb。

dbfilename dump.rdb

3、rdbcompression 配置

rdbcompression 配置的是 rdb 文件中压缩启用配置,基本语法格式:

rdbcompression yes(|no)

如果 rdbcompression 配置为 yes,那么即代表 redis 进行 RDB 文件生成中,如果遇到字符串对象并且其中的字符串值占用超过 20 个字节,那么就会对字符串进行 LZF 算法进行压缩。

4、stop-writes-on-bgsave-error 配置

stop-writes-on-bgsave-error 配置了,如果进行 RDB 备份文件生成过程中,遭遇错误,是否停止 redis 提供写服务,以警示用户 RDB 备份异常,默认是开启状态。

stop-writes-on-bgsave-error yes(|no)

5、dir 配置

dir 配置的是 rdb 文件存放的目录,默认是当前目录。

dir ./

6、rdbchecksum 配置

rdbchecksum 配置 redis 是否使用 CRC64 校验算法校验 RDB 文件是否发生损坏,默认开启状态,如果你需要提升性能,可以选择性关闭。

rdbchecksum yes(|no)

二、saveparams 和 dirty 计数器

我们 redisServer 结构体中有这么两个字段:

技术图片

saveparams 结构定义如下:

struct saveparam {
time_t seconds; //秒数
int changes; //变更次数
};

相信你能够想到,上述配置文件中的 save 配置就对应了两个参数,多少秒内数据库发生了多少次的变更便触发 bgsave。

映射到代码就是我们 saveparam 结构,每一个 saveparam 结构都对应一行 save 配置,而最终会以 saveparam 数组的形式被读取到 redisServer 中。

ps:介绍这个的目前是为我们稍后分析 RDB 文件生成的源码实现做前置铺垫。

除此之外,redisServer 数据结构中还有这么两个字段:

技术图片

dirty 字段记录了自上次成功备份 RDB 文件之后,包括 save 和 bgsave 命令,整个 redis 数据库又发生了多少次修改。dirty_before_bgsave 字段可以理解为上一次 bgsave 命令备份时,数据库总的修改次数。

还有一些跟持久化相关时间字段,上一次成功 RDB 备份的时间点,上一次 bgsave 命令开始执行时间等等。

技术图片

下面我们也粘贴粘贴源码,分析分析看 redis 是如何进行 RDB 备份文件生成的。

int serverCron(....){
.....
//如果已经有子进程在执行 RDB 生成,或者 AOF 恢复,或者有子进程未返回
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
ldbPendingChildren())
{
int statloc;
pid_t pid;
//查看这个进程是否返回信号
if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
int exitcode = WEXITSTATUS(statloc);
int bysignal = 0;
if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);
//持久化异常,打印日志
if (pid == -1) {
serverLog(LL_WARNING,"wait3() returned an error: %s. "
"rdb_child_pid = %d, aof_child_pid = %d",
strerror(errno),
(int) server.rdb_child_pid,
(int) server.aof_child_pid);
} else if (pid == server.rdb_child_pid) {
//成功持久化 RDB 文件,调用方法用心的RDB文件覆盖旧的RDB文件
backgroundSaveDoneHandler(exitcode,bysignal);
if (!bysignal && exitcode == 0) receiveChildInfo();
} else if (pid == server.aof_child_pid) {
//成功执行 AOF,替换现有的 AOF文件
backgroundRewriteDoneHandler(exitcode,bysignal);
if (!bysignal && exitcode == 0) receiveChildInfo();
} else {
//子进程成功,但返回的 pid 类型异常,无法匹配
if (!ldbRemoveChild(pid)) {
serverLog(LL_WARNING,
"Warning, detected child with unmatched pid: %ld",
(long)pid);
}
}
//如果子进程未结束,不允许字典进行 rehash
updateDictResizePolicy();
closeChildInfoPipe();
}
} else{.......}
}

serverCron 每隔一百毫秒执行一次(可能后续的 redis 版本有所区别,本文基于 4.0),都会首先去判断 RDB 或 AOF 子进程是否成功完成,如果成功会进行旧文件替换覆盖操作等。我们继续看 else 部分。

int serverCron(....){
.....
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
ldbPendingChildren())
{
..........
}
else{
//如果未有子进程做 RDB 文件生成
//遍历 saveparams 数组,取出我们配置文件中的 save 配置项
for (j = 0; j struct saveparam *sp = server.saveparams+j;
//根据我们之前介绍的 dirty 计数器判断 save 配置条件是否满足
if (server.dirty >= sp->changes &&
server.unixtime-server.lastsave > sp->seconds &&
(server.unixtime-server.lastbgsave_try >
CONFIG_BGSAVE_RETRY_DELAY ||
server.lastbgsave_status == C_OK))
{
//记录日志
serverLog(LL_NOTICE,"%d changes in %d seconds. Saving...",
sp->changes, (int)sp->seconds);
rdbSaveInfo rsi, *rsiptr;
rsiptr = rdbPopulateSaveInfo(&rsi);
//核心方法,进行 RDB 文件生成
rdbSaveBackground(server.rdb_filename,rsiptr);
break;
}
}
//AOF 下篇我们在介绍,本篇看 RDB
if (server.aof_state == AOF_ON &&
server.rdb_child_pid == -1 &&
server.aof_child_pid == -1 &&
server.aof_rewrite_perc &&
server.aof_current_size > server.aof_rewrite_min_size)
{
long long base = server.aof_rewrite_base_size ?
server.aof_rewrite_base_size : 1;
long long growth = (server.aof_current_size*100/base) - 100;
if (growth >= server.aof_rewrite_perc) {
serverLog(LL_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth);
rewriteAppendOnlyFileBackground();
}
}
}
}

如果未有子进程进行 RDB 文件生成,那么遍历循环我们的 save 配置项是否满足,如果满足则调用 rdbSaveBackground 进行真正的 RDB 文件生成。我们继续看看这个核心方法:

int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
pid_t childpid;
long long start;
if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
server.dirty_before_bgsave = server.dirty;
server.lastbgsave_try = time(NULL);
openChildInfoPipe();
start = ustime();
if ((childpid = fork()) == 0) {
int retval;
closeListeningSockets(0);
redisSetProcTitle("redis-rdb-bgsave");
retval = rdbSave(filename,rsi);
if (retval == C_OK) {
size_t private_dirty = zmalloc_get_private_dirty(-1);
if (private_dirty) {
serverLog(LL_NOTICE,
"RDB: %zu MB of memory used by copy-on-write",
private_dirty/(1024*1024));
}
server.child_info_data.cow_size = private_dirty;
sendChildInfo(CHILD_INFO_TYPE_RDB);
}
exitFromChild((retval == C_OK) ? 0 : 1);
} else {
server.stat_fork_time = ustime()-start;
server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
if (childpid == -1) {
closeChildInfoPipe();
server.lastbgsave_status = C_ERR;
serverLog(LL_WARNING,"Can't save in background: fork: %s",
strerror(errno));
return C_ERR;
}
serverLog(LL_NOTICE,"Background saving started by pid %d",childpid);
server.rdb_save_time_start = time(NULL);
server.rdb_child_pid = childpid;
server.rdb_child_type = RDB_CHILD_TYPE_DISK;
updateDictResizePolicy();
return C_OK;
}
return C_OK;
}

rdbSaveBackground 核心的是 fork 函数和 rdbSave 函数的调用。fork 函数其实是一个系统调用,他会复制出一个子进程出来,子进程和父进程几乎一模一样的内存数据。

fork 函数是阻塞的,当子进程复制出来后,程序的后续代码段会由父子进程同时执行,也就是说,fork 之后,接下来的代码,父子进程会并发执行,但系统不保证执行顺序。

父进程中,fork 函数返回值等于子进程的进程 id,子进程中 fork 函数返回值等于零。

所以,rdbSaveBackground 函数的核心逻辑也就很清晰了,fork 成功之后,子进程调用 rdbSave 进行 RDB 文件写入,并产生一个“temp-%d.rdb”的临时文件,而父进程记录一些日志信息、子进程进程号,时间等信息。

至于 rdbSave 函数是怎么写入 RDB 文件的,这个也很简单,RDB 文件是有固定的协议规范的,程序只要按照协议写入数据即可,关于这个协议,我们等下详细说它。

总结一下,serverCron 这个定期执行的函数,会将配置文件中的 save 配置进行读取,并判断条件是否满足,如果条件满足则调用 rdbSaveBackground 函数 fork 出一个子进程完成 RDB 文件的写入,生成临时文件,并确保临时文件写入成功后,再替换旧 RDB 文件,最后退出子进程。

ps:fork 函数复制出来的子进程一定要记得退出,否则每一次主进程都会复制一个子进程,最终导致服务 OOM。


RDB 文件结构分析

任何格式的文件都会有自己的编码协议,Java 中的字节码也好、图片格式文件也好,我们这里的 RDB 文件也好,都是有自己的一套约定好的协议的,具体到每一个字节位置该放什么样的字段数据,这都是约定俗成的,编码的时候按协议写入二进制,读取的时候也按照协议读取字段字节。

RDB 协议规定整个文件包括如下几个字段:

技术图片

其中,第一部分是固定的五个字节,redis 把它称为 Magic Number,固定的五个字符 “R”,“E”,“D”,“I”,“S”。

技术图片

我们在 redis 的 0 号数据库中添加一个键值对,然后执行 save 命令生成 RDB 文件,接着打开这个二进制文件。

技术图片

我们用 od 命令,并以 ASCII 码选项输出二进制文件,你会发现前五个字节是我们固定的 redis 这五个字符。

下一个字段 REDIS_VERSION 占四个字节,描述当前 RDB 的版本,以上述为例,redis-4.0 版本对应的 RDB 文件版本就是 0008。

下一个字段是 Aux Fields,官方称辅助字段,是 RDB 7 以后加入的,主要包含以下这些字段信息:



  1. redis-ver:版本号

  2. redis-bits:OS Arch

  3. ctime:RDB文件创建时间

  4. used-mem:使用内存大小

  5. repl-stream-db:在server.master客户端中选择的数据库

  6. repl-id:当前实例 replication ID

  7. repl-offset:当前实例复制的偏移量

接着就是 DATABASE 部分,这部分会存储的我们字典中的真实数据,redis 中多个数据库,生成 RDB 文件的时候只会对有数据的数据库进行写入,而这部分的格式如下:

技术图片

对应到我们上述例子中,就是这一部分:

技术图片

我们的 rdb.h 文件头中有这么一些常量的定义:

#define RDB_OPCODE_AUX 250
#define RDB_OPCODE_RESIZEDB 251
#define RDB_OPCODE_EXPIRETIME_MS 252
#define RDB_OPCODE_EXPIRETIME 253
#define RDB_OPCODE_SELECTDB 254
#define RDB_OPCODE_EOF 255

十六进制 fe 转换成十进制就是 254,对应的就是 RDB_OPCODE_SELECTDB,标识即将打开某数据库,所以其后跟着的就是即将要打开的数据库编号,我们这里是零号数据库。

十六进制 fb 转换成十进制就是 251,对应的就是 RDB_OPCODE_RESIZEDB,标识当前数据库容量,即有多少个键,我们这里只有一个键。

紧接着就是存我们的键值对,这部分的格式如下:

技术图片

type 占一个字节标识当前键值对的类型,即对象类型,有如下可选类型:

#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
#define RDB_TYPE_MODULE 6
#define RDB_TYPE_MODULE_2 7
/* 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

key 始终是字符串,由字符串长度前缀加上自身内容构成,后跟 value 的内容。

EOF 字段标识 RDB 文件的结尾,占一个字节,并固定值等于 255 也就是十六进制 ff,这是能从 rdb.h 文件头中找到的。

CHECK_SUM 字段存储的是 RDB 文件的校验和,占八个字节,用于校验 RDB 文件是否损坏。

以上,我们就简单介绍了 RDB 文件的构成,其实也只是点到为止啊,每一种类型的对象进行编码的时候都是不一样的,还要一些压缩对象的手法等等等等,我们这里也不可能全部详尽。

总的来说,对 RDB 文件构成有个基本了解就行,实际上也很少有人没事去分析 RDB 文件里的数据的,即便是有也是通过工具进行分析的,比如 rdb-tools 等,人工分析也太炸裂了。

好了,关于 RDB 我们就简单介绍到这,下一篇我们研究研究 AOF 这种持久化策略,再见!



关注公众不迷路,一个爱分享的程序员


公众号回复「1024」加作者微信一起探讨学习!


每篇文章用到的所有案例代码素材都会上传我个人 github


https://github.com/SingleYam/overview_java


欢迎来踩!

技术图片


推荐阅读
  • 我正在使用 Ruby on Rails 构建个人网站。总体而言,RoR 是一个非常出色的工具,它提供了丰富的功能和灵活性,使得创建自定义页面变得既高效又便捷。通过利用其强大的框架和模块化设计,我可以轻松实现复杂的功能,同时保持代码的整洁和可维护性。此外,Rails 的社区支持也非常强大,为开发过程中遇到的问题提供了丰富的资源和解决方案。 ... [详细]
  • 如何在Oracle ASM_Diskgroup中重命名现有磁盘
    如何在Oracle ASM_Diskgroup中重命名现有磁盘 ... [详细]
  • 在进行网络编程时,准确获取本地主机的IP地址是一项基本但重要的任务。Winsock作为20世纪90年代初由Microsoft与多家公司共同制定的Windows平台网络编程接口,为开发者提供了一套高效且易用的工具。通过Winsock,开发者可以轻松实现网络通信功能,并准确获取本地主机的IP地址,从而确保应用程序在网络环境中的稳定运行。此外,了解Winsock的工作原理及其API函数的使用方法,有助于提高开发效率和代码质量。 ... [详细]
  • 本文深入解析了 Apache 配置文件 `httpd.conf` 和 `.htaccess` 的优化方法,探讨了如何通过合理配置提升服务器性能和安全性。文章详细介绍了这两个文件的关键参数及其作用,并提供了实际应用中的最佳实践,帮助读者更好地理解和运用 Apache 配置。 ... [详细]
  • 本文详细解析了如何使用 jQuery 实现一个在浏览器地址栏运行的射击游戏。通过源代码分析,展示了关键的 JavaScript 技术和实现方法,并提供了在线演示链接供读者参考。此外,还介绍了如何在 Visual Studio Code 中进行开发和调试,为开发者提供了实用的技巧和建议。 ... [详细]
  • 2019年后蚂蚁集团与拼多多面试经验详述与深度剖析
    2019年后蚂蚁集团与拼多多面试经验详述与深度剖析 ... [详细]
  • Java 8 引入了 Stream API,这一新特性极大地增强了集合数据的处理能力。通过 Stream API,开发者可以更加高效、简洁地进行集合数据的遍历、过滤和转换操作。本文将详细解析 Stream API 的核心概念和常见用法,帮助读者更好地理解和应用这一强大的工具。 ... [详细]
  • Python与R语言在功能和应用场景上各有优势。尽管R语言在统计分析和数据可视化方面具有更强的专业性,但Python作为一种通用编程语言,适用于更广泛的领域,包括Web开发、自动化脚本和机器学习等。对于初学者而言,Python的学习曲线更为平缓,上手更加容易。此外,Python拥有庞大的社区支持和丰富的第三方库,使其在实际应用中更具灵活性和扩展性。 ... [详细]
  • PHP中元素的计量单位是什么? ... [详细]
  • 本项目在Java Maven框架下,利用POI库实现了Excel数据的高效导入与导出功能。通过优化数据处理流程,提升了数据操作的性能和稳定性。项目已发布至GitHub,当前最新版本为0.0.5。该项目不仅适用于小型应用,也可扩展用于大型企业级系统,提供了灵活的数据管理解决方案。GitHub地址:https://github.com/83945105/holygrail,Maven坐标:`com.github.83945105:holygrail:0.0.5`。 ... [详细]
  • 本文深入探讨了 MXOTDLL.dll 在 C# 环境中的应用与优化策略。针对近期公司从某生物技术供应商采购的指纹识别设备,该设备提供的 DLL 文件是用 C 语言编写的。为了更好地集成到现有的 C# 系统中,我们对原生的 C 语言 DLL 进行了封装,并利用 C# 的互操作性功能实现了高效调用。此外,文章还详细分析了在实际应用中可能遇到的性能瓶颈,并提出了一系列优化措施,以确保系统的稳定性和高效运行。 ... [详细]
  • 在稀疏直接法视觉里程计中,通过优化特征点并采用基于光度误差最小化的灰度图像线性插值技术,提高了定位精度。该方法通过对空间点的非齐次和齐次表示进行处理,利用RGB-D传感器获取的3D坐标信息,在两帧图像之间实现精确匹配,有效减少了光度误差,提升了系统的鲁棒性和稳定性。 ... [详细]
  • 本文详细探讨了C语言中`extern`关键字的简易编译方法,并深入解析了预编译、`static`和`extern`的综合应用。通过具体的代码示例,介绍了如何在不同的文件之间共享变量和函数声明,以及这些关键字在编译过程中的作用和影响。文章还讨论了预编译过程中宏定义的使用,为开发者提供了实用的编程技巧和最佳实践。 ... [详细]
  • 深入解析Gradle中的Project核心组件
    在Gradle构建系统中,`Project` 是一个核心组件,扮演着至关重要的角色。通过使用 `./gradlew projects` 命令,可以清晰地列出当前项目结构中包含的所有子项目,这有助于开发者更好地理解和管理复杂的多模块项目。此外,`Project` 对象还提供了丰富的配置选项和生命周期管理功能,使得构建过程更加灵活高效。 ... [详细]
  • Go语言中的高效排序与搜索算法解析
    在探讨Go语言中高效的排序与搜索算法时,本文深入分析了Go语言提供的内置排序功能及其优化策略。通过实例代码,详细讲解了如何利用Go语言的标准库实现快速、高效的排序和搜索操作,为开发者提供了实用的编程指导。 ... [详细]
author-avatar
手机用户2502940275
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有