聊聊Redis持久化策略RDB

写在文章开头

为避免服务器宕机着情况导致redis内存数据库数据丢失,redis默认出通过rdb保证可靠性,本文将从源码的角度带读者了解rdb读写时机和写入流程。

在这里插入图片描述

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

在这里插入图片描述

详解RDB持久化

save指令触发rdb

redis支持通过命令的方式持久化内存数据库数据,当我们键入save的时候,redis解析到这个指令之后,主线程直接调用saveCommand方法生成rdb文件落到磁盘中。

在这里插入图片描述

我们可以在rdb.c文件中看到该方法的实现,可以看到为了避免脏写等问题,saveCommand会检查当前是否有rdb子进程执行,如果没有在子进程执行rdb持久化则直接调用rdbSave方法生成dump.rdb文件落盘:

//调用save指令其内部调用rdbSave完成rdb文件生成
void saveCommand(redisClient *c) {//检查是否子进程执行rdb,若有则直接返回if (server.rdb_child_pid != -1) {addReplyError(c,"Background save already in progress");return;}//调用rdbSaveif (rdbSave(server.rdb_filename) == REDIS_OK) {addReply(c,shared.ok);} else {addReply(c,shared.err);}
}

步入rdbSave即可看到生成临时rdb写入数据,然后数据刷盘,最后完成文件名原子修改的操作:

int rdbSave(char *filename) {char tmpfile[256];FILE *fp;rio rdb;int error;//生成一个tmp文件snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());fp = fopen(tmpfile,"w");if (!fp) {redisLog(REDIS_WARNING, "Failed opening .rdb for saving: %s",strerror(errno));return REDIS_ERR;}//调用rdbSaveRio完成数据写入rioInitWithFile(&rdb,fp);if (rdbSaveRio(&rdb,&error) == REDIS_ERR) {errno = error;goto werr;}//直接刷盘到磁盘,避免留在系统输出缓冲区/* Make sure data will not remain on the OS's output buffers */if (fflush(fp) == EOF) goto werr;if (fsync(fileno(fp)) == -1) goto werr;if (fclose(fp) == EOF) goto werr;//完成写入后文件重命名为dump.rdbif (rename(tmpfile,filename) == -1) {redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno));unlink(tmpfile);return REDIS_ERR;}//......return REDIS_OK;//......
}

bgsave指令触发rdb

同时redis也支持后台持久化,如果用户需要考虑redis性能问题,可以直接通过bgsave指令创建rdb子进程完成数据库数据持久化。

在这里插入图片描述

我们同样可以在rdb.c文件中看到bgsave指令调用的方法bgsaveCommand,可以看到如果没有子进程进行rdb或者aof,该指令会调用rdbSaveBackground完成异步数据持久化:

//调用rdbSaveBackground创建一个子进程生成rdb文件,不影响主线程
void bgsaveCommand(redisClient *c) {//如果有子进程执行rdb或者aof,则直接返回错误提醒if (server.rdb_child_pid != -1) {addReplyError(c,"Background save already in progress");} else if (server.aof_child_pid != -1) {addReplyError(c,"Can't BGSAVE while AOF log rewriting is in progress");} else if (rdbSaveBackground(server.rdb_filename) == REDIS_OK) {//调用rdbSaveBackground进行数据持久化addReplyStatus(c,"Background saving started");} else {addReply(c,shared.err);}
}

步入rdbSaveBackground可以看到,其内部还会检查一次是否有文件进行rdb,如果明确没有之后直接fork一个子进程出来调用上文所说的rdbSave完成数据持久化到dump.rdb中:

int rdbSaveBackground(char *filename) {pid_t childpid;long long start;if (server.rdb_child_pid != -1) return REDIS_ERR;//......start = ustime();if ((childpid = fork()) == 0) {//创建子进程int retval;/* Child */closeListeningSockets(0);redisSetProcTitle("redis-rdb-bgsave");retval = rdbSave(filename);//生成rdb文件if (retval == REDIS_OK) {//......}exitFromChild((retval == REDIS_OK) ? 0 : 1);//退出子进程} else {//......}return REDIS_OK; /* unreached */
}

RDB被动触发

redis被动触发由时间事件轮询处理,我们可以在redis.conf配置rdb被动触发持久化的时机,默认配置如下当60s生成10000或者300 生成10次改变亦或者900s生成1s,我们就会执行一次被动rdb持久化:

save 900 1
save 300 10
save 60 10000

在这里插入图片描述

对应的我们可以在redis.cserverCron函数在看到这段逻辑,它会遍历出我们配置的保存间隔配置saveparam,通过比对这3条配置的上次保存时间计算出时间间隔,以及当前redis变化书dirty看看是否符合要求,若如何要求则进行后台rdb持久化:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {//....../* Check if a background saving or AOF rewrite in progress terminated. */if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {//......}} else {//遍历3个配置的params,如果改变数和事件间隔配置要求则直接进行后台被动rdb持久化for (j = 0; j < server.saveparamslen; j++) {struct saveparam *sp = server.saveparams+j;if (server.dirty >= sp->changes && //查看变化数是否大于当前配置的changesserver.unixtime-server.lastsave > sp->seconds && //查看时间间隔是否大于配置(server.unixtime-server.lastbgsave_try >REDIS_BGSAVE_RETRY_DELAY ||server.lastbgsave_status == REDIS_OK)){//......//执行异步持久化rdbSaveBackground(server.rdb_filename);break;}}//......}}//......return 1000/server.hz;
}

其他被动落盘时机

其实有些时候我们执行的某些执行也会进行rdb持久化,例如flushall刷盘指令,其调用函数flushallCommand就会时间串行执行rdb持久化:

//调用flush指令时会调用rdbSave进行数据持久化
void flushallCommand(redisClient *c) {//......if (server.saveparamslen > 0) {//串行执行rdb持久化int saved_dirty = server.dirty;rdbSave(server.rdb_filename);//......}server.dirty++;
}

当我们关闭redis服务器的时候也会执行rdb串行持久化:

//服务器进程关闭时调用rdbSave生成rdb文件
int prepareForShutdown(int flags) {//......if (server.rdb_child_pid != -1) {//......}if (server.aof_state != REDIS_AOF_OFF) {//......}if ((server.saveparamslen > 0 && !nosave) || save) {if (rdbSave(server.rdb_filename) != REDIS_OK) {//......return REDIS_ERR;}}//......return REDIS_OK;
}

rdb写入文件数据详解

无论是rdbsave还是rdbbgsave对应的方法,其内部都会调用rdbSaveRio,它进行文件写入时对应写入数据大体顺序是:

  1. 写入REDIS大写。
  2. 补0填充长度。
  3. 写入当前redis版本号,以笔者源码为例则是6。
  4. 遍历数据库写入REDIS_RDB_OPCODE_SELECTDB表示开始存储数据库数据,这个值默认为254,redis会转为八进制376写入。
  5. 遍历当前数据库键值对key长度和keyvalue长度和value写入,后续数据库都是如此往复。
  6. 所有数据库写完后补REDIS_RDB_OPCODE_EOF和checksum用于后续rdb数据恢复的校验。

为保证读者更直观的了解redis持久化写入的内容,我们可以删除本地rdb文件,然后执行如下执行生成一个全新的rdb文件:

# 保存键值对
set key value
# 切换到1库
select 1
# 保存键值对到1库
set key-1 value
# 调用save进行数据持久化
save

正常情况下我们打开rdb文件会得到一堆类型乱码的内容,我们无法知晓写入的信息,我们可以直接键入od生成rdb文件16进制数据及其对应的ASCII字符:

od -A x -t x1c -v dump.rdb

最终我们就可以得到如下文件,可以看到数据格式和笔者上文所说基本一致:

#        大写REDIS          补0            254的8进制 当前数据库索引   键值对`key`长度和`key`,`value`长度和`value`      
#000000  52  45  44  49  53  30  30  30  36  fe  00  00  03  6b  65  79R   E   D   I   S   0   0   0   6 376  \0  \0 003   k   e   y
000010  05  76  61  6c  75  65  fe  01  00  05  6b  65  79  2d  31  05005   v   a   l   u   e 
#  254的8进制 当前数据库索引1  键值对key长度和key,value长度和value    
376 001  \0 005   k   e   y   -   1 005
000020  76  61  6c  75  65  ff  76  eb  e4  80  bd  df  66  11v   a   l   u   e 
# EOF 255八进制 剩下8位是对应的checksum
377   v 353 344 200 275 337   f 021
00002e

对应的我们给出这段源码,对应的写入流程如上文笔者所述:

int rdbSaveRio(rio *rdb, int *error) {dictIterator *di = NULL;dictEntry *de;char magic[10];int j;long long now = mstime();uint64_t cksum;if (server.rdb_checksum)rdb->update_cksum = rioGenericUpdateChecksum;snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION);//对应redis 3个0 然后版本号,当前版本为6if (rdbWriteRaw(rdb,magic,9) == -1) goto werr;//上述魔数写入rdb文件//遍历数据库for (j = 0; j < server.dbnum; j++) {redisDb *db = server.db+j;dict *d = db->dict;if (dictSize(d) == 0) continue;di = dictGetSafeIterator(d);if (!di) return REDIS_ERR;/* Write the SELECT DB opcode */if (rdbSaveType(rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr;//写入254,也就是内容中的376if (rdbSaveLen(rdb,j) == -1) goto werr;//写入当前库索引//遍历当前键值对写入while((de = dictNext(di)) != NULL) {sds keystr = dictGetKey(de);robj key, *o = dictGetVal(de);long long expire;initStaticStringObject(key,keystr);expire = getExpire(db,&key);if (rdbSaveKeyValuePair(rdb,&key,o,expire,now) == -1) goto werr;//写入键值对}dictReleaseIterator(di);}//....../* EOF opcode */if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;//写入结束符254 八进制为377cksum = rdb->cksum;memrev64ifbe(&cksum);if (rioWrite(rdb,&cksum,8) == 0) goto werr;//写入8位数校验和,其底层调用rioGenericUpdateChecksum,按照cksum到数组中获取就对应的值并return REDIS_OK;//......
}

对应的我们步入rdbSaveKeyValuePair即可看到redis获取key长度和key,以及value长度和value并写入rdb文件的核心流程:

int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val,long long expiretime, long long now)
{//....../* Save type, key, value */if (rdbSaveObjectType(rdb,val) == -1) return -1;//写入类型以字符串形式就是0if (rdbSaveStringObject(rdb,key) == -1) return -1;//写入key长度和keyif (rdbSaveObject(rdb,val) == -1) return -1;//写入value长度和valuereturn 1;
}

小结

自此我们将redis持久化策略rdb都分析完成了,希望对你有帮助。

我是 sharkchiliCSDN Java 领域博客专家开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/39798.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

刷代码随想录有感(124):动态规划——最长公共子序列

题干&#xff1a; 代码&#xff1a; class Solution { public:int findLength(vector<int>& nums1, vector<int>& nums2) {vector<vector<int>>dp(nums1.size() 1, vector<int>(nums2.size() 1, 0));int res 0;for(int i 1; i <…

数据集采样策略对模型性能的影响问题

数据集采样策略对模型性能的影响问题&#xff0c;需要具体代码示例 随着机器学习和深度学习的快速发展&#xff0c;数据集的质量和规模对于模型性能的影响变得越来越重要。在实际应用中&#xff0c;我们往往面临着数据集规模过大、样本类别不平衡、样本噪声等问题。这时&#…

lnternet 发展史

一&#xff0c;lnternet 发展史 ARPA net &#xff08;上世纪50年代二战结束&#xff09; 无线 战场指挥通信协议落后 TCP/IP 包交换 WEB (70年代 ) 80年代 90年代 二&#xff0c;互联网的典型应用&#xff1a; 96年到2008年 第一代技术…

AJAX的概述 ,同步和异步的区别 ,AJAX 的交互模型和传统交互模型的区别

一. AJAX的概述 1.1 什么是ajax 同步&#xff1a; 异步&#xff1a; 1.AJAX Asynchronous JavaScript and XML&#xff08;异步的 JavaScript 和 XML&#xff09;。 ​ 说明&#xff1a;异步&#xff1a;就是不同步。例如我们向后台发送请求&#xff0c;同步的方式是后台必…

svn忽略上传文件node_modules文件

文章目录 1.点击svn项目右键-》选中svn的属性2. 点击 新建3. 点击其他4. 选择属性 svn:global-ignores5. 输入忽略文件 1.点击svn项目右键-》选中svn的属性 2. 点击 新建 3. 点击其他 4. 选择属性 svn:global-ignores 5. 输入忽略文件

四、【源码】Bean属性注入

源码地址&#xff1a;https://github.com/spring-projects/spring-framework 仓库地址&#xff1a;https://gitcode.net/qq_42665745/spring/-/tree/04-porperty-inject Bean属性注入 属性注入相关的类 1.PropertyValue&#xff1a;属性对象&#xff0c;name:value 2.Prope…

数据结构 —— 二叉树

1.树的概念及结构 1.1树的概念 树是一种非线性的数据结构&#xff0c;它有着多分支&#xff0c;层次性的特点。 由于其形态类似于自然界中倒过来的数&#xff0c;所以我们将这种数据结构称为“树形结构” 注意&#xff1a; 树形结构中&#xff0c;子树之间不能有交集&#x…

降重工具大揭秘:AI如何帮你轻松搞定论文重写?

已经天临五年了&#xff0c;大学生们还在为论文降重烦恼……手动降重确实是个难题&#xff0c;必须要先付点小经费去靠谱的网站查重&#xff0c;再对着红字标注去改&#xff0c;后面每一次的论文呢查重结果都像赌//博&#xff0c;谁也不知道明明是同一篇文章&#xff0c;第二次…

2024鲲鹏昇腾创新大赛集训营Ascend C算子学习笔记

异构计算架构&#xff08;CANN&#xff09; 对标英伟达的CUDA CuDNN的核心软件层&#xff0c;向上支持多种AI框架&#xff0c;向下服务AI处理器&#xff0c;发挥承上启下的关键作用&#xff0c;是提升昇腾AI处理器计算效率的关键平台。主要包括有各种引擎、编译器、执行器、算…

(番外篇)指针的一些相关习题讲解(速进,干货满满)(2)

前言&#xff1a; 小编感觉最近有点太堕落&#xff0c;于是我开始从事这篇文章的撰写&#xff0c;现在也是进入七月份了&#xff0c;我现在文章开头定一个小目标&#xff0c;我决定在七月份发布至少十篇文章&#xff0c;希望我可以说到做到&#xff08;我前面就口头欠了不少文章…

OpenSSL的一些使用案例

目录 一、介绍 二、基本使用 1、Shell &#xff08;1&#xff09;文件加解密 &#xff08;2&#xff09;生成密钥文件 2、API &#xff08;1&#xff09;md5sum &#xff08;2&#xff09;AES256加解密 一、介绍 本篇博客重点不是详细描述 OpenSSL 的用法&#xff0c;只…

什么是校园气象站

在科技日新月异的今天&#xff0c;气象观测不仅局限于专业的气象机构&#xff0c;它已经走进了我们的校园&#xff0c;成为了学生们探索自然、学习科学知识的重要平台。 校园气象站是设置在学校内部&#xff0c;用于进行气象观测、数据记录和科学实验的设施。它通常由气象传感器…

常见锁策略之可重入锁VS不可重入锁

可重入锁VS不可重入锁 有一个线程,针对同一把锁,连续加锁两次,如果产生了死锁,那就是不可重入锁,如果没有产生死锁,那就是可重入锁. 死锁 我们之前引入多线程的时候不是讲了一个加数字的案例么,我们今天以它来举例 当我们这样写的时候会出现什么问题? 分析:第一个synchron…

前端基础--Vue3

Vue3基础 VUE3和VUE2的区别 2020年9月18日&#xff0c;Vue.js发布版3.0版本&#xff0c;代号&#xff1a;One Piece 于 2022 年 2 月 7 日星期一成为新的默认版本! Vue3性能更高,初次渲染快55%, 更新渲染快133% 。体积更小 Vue3.0 打包大小减少41%。 同时Vue3可以更好的支持T…

基于微服务智能推荐健康生活交流平台的设计与实现(SpringCloud SpringBoot)+文档

&#x1f497;博主介绍&#x1f497;&#xff1a;✌在职Java研发工程师、专注于程序设计、源码分享、技术交流、专注于Java技术领域和毕业设计✌ 温馨提示&#xff1a;文末有 CSDN 平台官方提供的老师 Wechat / QQ 名片 :) Java精品实战案例《700套》 2025最新毕业设计选题推荐…

解决使用monaco-editor编译器,编译器展示内容没有超过编译器高度,但是出现滚动条问题

前言&#xff1a; 最近在完成项目时&#xff0c;有使用编译器进行在线编辑的功能&#xff0c;就选用了monaco-editor编译器&#xff0c;但是实现功能之后&#xff0c;发现即使在编译器展示的内容没有超过编译器高度的情况下&#xff0c;编译器依旧存在滚动条&#xff0c;会展示…

计算机网络--网络层

一、网络层的服务和功能 网络层主要为应用层提供端对端的数据传输服务 网络层接受运输层的报文段&#xff0c;添加自己的首部&#xff0c;形成网络层分组。分组是网络层的传输单元。网络层分组在各个站点的网络层之间传输&#xff0c;最终到达接收方的网络层。接收方网络层将运…

如何在 Java 应用中使用 Jedis 客户端库来实现 Redis 缓存的基本操作

本人详解 作者:王文峰,参加过 CSDN 2020年度博客之星,《Java王大师王天师》 公众号:JAVA开发王大师,专注于天道酬勤的 Java 开发问题中国国学、传统文化和代码爱好者的程序人生,期待你的关注和支持!本人外号:神秘小峯 山峯 转载说明:务必注明来源(注明:作者:王文峰…

堆与栈的概念(RTOS)

目录 #堆在RTOS的概念 #相关代码表示 #堆相关特点 #栈在RTOS中的概念 #栈的代码表示 #栈的相关特点 #为什么每个RTOS任务都要有自己的栈 前言&#xff1a;本篇参考韦东山老师的RTOS&#xff0c;连接放在最后 #堆在RTOS的概念 本文所指的堆与栈并不是数据结构中&#xff…

【unity实战】在Unity中使用有限状态机制作一个敌人AI

最终效果 文章目录 最终效果前言有限状态机的主要作用和意义素材下载逻辑图敌人动画配置优雅的代码文件目录状态机代码定义敌人不同状态切换创建敌人效果更多的敌人参考源码完结 前言 有限状态机以前的我嗤之以鼻&#xff0c;现在的我逐帧分析。其实之前我就了解过有限状态机&…