Redis RDB持久化

news2025/7/8 20:43:24

前言

我们知道 Redis 之所以快,很大程度是因为它的数据直接放在内存里,而内存是易失性存储器,只有通电才存储数据,断电数据就会丢失。
这个时候就要看你的应用场景了,如果你只是拿 Redis 做关系型数据库的缓存,来加速数据的访问效率,那么 Redis 数据即使丢了也不影响,可以重新从关系型数据库中重新加载一遍。但如果你直接拿 Redis 当做数据库来用,在上面存储业务数据,那么你就要重点关注下 Redis 的持久化机制了。
RDB 是 Redis DataBase 的缩写,它是 Redis 提供的最简单的持久化机制,也叫内存快照。它会把某一时刻的数据全量写入磁盘,Redis 重启后会加载 RDB 文件来恢复数据。
image.png

触发

首先你要配置持久化的文件名,默认是dump.rdb

dbfilename dump.rdb

然后你可以通过命令save 和bgsave 来手动触发 RDB 持久化,它俩的区别是前者会阻塞主线程,后者会 fork 一个子进程异步处理

127.0.0.1:6379[1]> save
OK
127.0.0.1:6379[1]> bgsave
Background saving started

你还可以通过配置,在多长时间内有多少次写入变更就主动触发一次 RDB 持久化

save 3600 1
save 300 100
save 60 10000

除此之外,其它一些情况 Redis 也会触发 RDB 持久化,比如 Redis 正常关闭、副本节点全量数据同步等等。
需要注意的是,RDB 持久化是有代价的,不宜频繁触发。以save 命令为例,它会阻塞主线程,假设数据有 4G,磁盘的写入带宽是 100MB/s,RDB 持久化阻塞的时间最少是 40 秒,期间 Redis 不能处理任何其它命令,这显然是不可接受的。其次,就算是bgsave 异步持久化,主进程 fork 子进程也是要阻塞的,数据量越大阻塞的时间越长,子进程持久化期间,如果有大量写入就会导致大量的写时复制,也会严重影响 Redis 性能。
image.png

RDB文件格式

转储后的 RDB 文件由三部分组成:

  • 文件头:魔数、RDB 版本、Redis 版本,创建时间等信息
  • 数据库数据:各个数据库的所有键值对
  • 文件尾:结尾符、校验和

image.png
Redis 首先会写入文件头信息,主要包含 Redis 的版本号、Redis 运行的架构信息、创建时间、使用内存大小等等。

属性示例值说明
MagicREDIS0009魔数
redis-ver6.2.13Redis 版本号
redis-bits64架构信息 32or64
ctime1607211828创建时间
used-mem82718使用内存大小
aof-preamble1是否写入AOF前导

再接着开始写入各个数据库的所有键值对数据,由两部分组成:

  • RDB_OPCODE_SELECTDB:SELECT-DB 操作码,确定后续键值对属于哪个数据库
  • 若干个键值对

键值对本身还有一些额外信息,例如:过期时间,LRU/LFU 信息等等,Redis 定义了一批操作码来标识这些信息,键值对的信息如下:

属性操作码说明
RDB_OPCODE_EXPIRETIME_MS252过期时间(可选)
RDB_OPCODE_IDLE248LRU 闲置时间(可选)
RDB_OPCODE_FREQ249LFU 访问频率(可选)
ObjectType0~7对象类型
Key Length-Key 长度
Key-Key 值
Value-Value 值(不同类型存储方式不一样)

最后写入文件尾,包含两部分:

  • EOF:RDB 文件结尾符
  • checksum:校验和,防止文件篡改/损坏

眼见为实,下面来测试一下,看看 RDB 文件到底长啥样。首先清空数据库,然后写入一个字符串:

127.0.0.1:6379[1]> flushall
OK
127.0.0.1:6379[1]> set name jackson
OK
127.0.0.1:6379[1]> save
OK

然后查看 RDB 文件,因为是二进制的,无法直接查看,这里把对应的 ASCII 码打印出来。可以看到前面依次是:魔数、Redis 版本号、Redis 运行架构信息、创建时间、内存使用量、AOF 前导标志,然后再是键值对数据。

od -A x -t x1c -v dump.rdb
0000000    52  45  44  49  53  30  30  30  39  fa  09  72  65  64  69  73
           R   E   D   I   S   0   0   0   9 372  \t   r   e   d   i   s
0000010    2d  76  65  72  06  36  2e  32  2e  31  33  fa  0a  72  65  64
           -   v   e   r 006   6   .   2   .   1   3 372  \n   r   e   d
0000020    69  73  2d  62  69  74  73  c0  40  fa  05  63  74  69  6d  65
           i   s   -   b   i   t   s 300   @ 372 005   c   t   i   m   e
0000030    c2  a3  05  22  65  fa  08  75  73  65  64  2d  6d  65  6d  c2
           £  ** 005   "   e 372  \b   u   s   e   d   -   m   e   m 302
0000040    30  00  11  00  fa  0c  61  6f  66  2d  70  72  65  61  6d  62
           0  \0 021  \0 372  \f   a   o   f   -   p   r   e   a   m   b
0000050    6c  65  c0  00  fe  01  fb  01  00  00  04  6e  61  6d  65  07
           l   e 300  \0 376 001 373 001  \0  \0 004   n   a   m   e  \a
0000060    6a  61  63  6b  73  6f  6e  ff  a1  ce  0b  3e  b5  94  1c  18
           j   a   c   k   s   o   n 377 241 316  \v   > 265 224 034 030
0000070

源码

Redis 在src/rdb.h文件下定义了一批操作码,用来区分写入 RDB 文件的命令和属性,比如:选择数据库、记录对象过期时间、写入结尾符等等。

#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. */

同时还定义了一批对象类型,用于将 Redis 对象类型映射到 RDB 对象类型:

#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

RDB 持久化的入口方法是rdbSave(),源码在src/rdb.c文件下。
Redis 首先会根据进程 ID 生成一个临时文件,然后开始执行 RDB 持久化写入临时文件,最后替换旧文件。

int rdbSave(char *filename, rdbSaveInfo *rsi) {
    char tmpfile[256];
    // 生成一个临时文件 通过进程ID命名
    snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
    fp = fopen(tmpfile,"w");
    // RDB持久化写入
    if (rdbSaveRio(&rdb,&error,RDBFLAGS_NONE,rsi) == C_ERR) {
        errno = error;
        goto werr;
    }
    // 刷新到磁盘,关闭文件
    if (fflush(fp)) goto werr;
    if (fsync(fileno(fp))) goto werr;
    if (fclose(fp)) { fp = NULL; goto werr; }
    // 替换旧的rdb文件
    if (rename(tmpfile,filename) == -1) {
        return C_ERR;
    }
    return C_OK;
}

持久化的核心方法是rdbSaveRio(),主要步骤:

  • 写入文件头信息
  • 遍历数据库
    • 写入 SELECTDB 操作码、数据库编号
    • 写入RESIZEDB操作码、设置全局哈希表和过期时间 Key 的哈希表大小
    • 遍历哈希表,写入每个键值对信息
  • 写入 EOF 操作码和 checksum
int rdbSaveRio(rio *rdb, int *error, int rdbflags, rdbSaveInfo *rsi) {
    // 哈希表迭代器,每个数据库有一个全局哈希表记录键值对
    dictIterator *di = NULL;
    // 哈希表节点指针,遍历键值对
    dictEntry *de;
    // 魔数 REDIS+4位RDB版本+结尾符
    char magic[10];
    // 文件校验和 防止被篡改
    uint64_t cksum;
    size_t processed = 0;
    int j;
    long key_count = 0;
    long long info_updated_time = 0;
    char *pname = (rdbflags & RDBFLAGS_AOF_PREAMBLE) ? "AOF rewrite" :  "RDB";
    if (server.rdb_checksum)
        rdb->update_cksum = rioGenericUpdateChecksum;
    // 生成魔数写入到缓冲区
    snprintf(magic,sizeof(magic),"REDIS%04d",RDB_VERSION);
    // 魔数写入RDB文件
    if (rdbWriteRaw(rdb,magic,9) == -1) goto werr;
    // 写入其它头信息 Redis版本、创建时间、内存大小等
    if (rdbSaveInfoAuxFields(rdb,rdbflags,rsi) == -1) goto werr;
    if (rdbSaveModulesAux(rdb, REDISMODULE_AUX_BEFORE_RDB) == -1) goto werr;
    // 遍历数据库
    for (j = 0; j < server.dbnum; j++) {
        redisDb *db = server.db+j;
        // 数据库的全局哈希表,即所有键值对
        dict *d = db->dict;
        if (dictSize(d) == 0) continue;// 没有数据,跳过
        // 哈希表迭代器
        di = dictGetSafeIterator(d);
        // 先写入SELECTDB操作码
        if (rdbSaveType(rdb,RDB_OPCODE_SELECTDB) == -1) goto werr;
        // 再写入数据库编号值
        if (rdbSaveLen(rdb,j) == -1) goto werr;
        // 写入RESIZEDB操作码,全局哈希表、过期Key哈希表的大小
        uint64_t db_size, expires_size;
        db_size = dictSize(db->dict);
        expires_size = dictSize(db->expires);
        if (rdbSaveType(rdb,RDB_OPCODE_RESIZEDB) == -1) goto werr;
        if (rdbSaveLen(rdb,db_size) == -1) goto werr;
        if (rdbSaveLen(rdb,expires_size) == -1) goto werr;
        // 遍历哈希表,写入每个键值对
        while((de = dictNext(di)) != NULL) {
            // 取出Key和Value
            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) == -1) goto werr;
            if (rdbflags & RDBFLAGS_AOF_PREAMBLE &&
                rdb->processed_bytes > processed+AOF_READ_DIFF_INTERVAL_BYTES)
            {
                processed = rdb->processed_bytes;
                aofReadDiffFromParent();
            }
            if ((key_count++ & 1023) == 0) {
                long long now = mstime();
                if (now - info_updated_time >= 1000) {
                    sendChildInfo(CHILD_INFO_TYPE_CURRENT_INFO, key_count, pname);
                    info_updated_time = now;
                }
            }
        }
        dictReleaseIterator(di);
        di = NULL; /* So that we don't release it again on error. */
    }
    if (rsi && dictSize(server.lua_scripts)) {
        di = dictGetIterator(server.lua_scripts);
        while((de = dictNext(di)) != NULL) {
            robj *body = dictGetVal(de);
            if (rdbSaveAuxField(rdb,"lua",3,body->ptr,sdslen(body->ptr)) == -1)
                goto werr;
        }
        dictReleaseIterator(di);
        di = NULL;
    }
    if (rdbSaveModulesAux(rdb, REDISMODULE_AUX_AFTER_RDB) == -1) goto werr;
    // 写入EOF操作码 0xff
    if (rdbSaveType(rdb,RDB_OPCODE_EOF) == -1) goto werr;
    // 写入校验和
    cksum = rdb->cksum;
    memrev64ifbe(&cksum);
    if (rioWrite(rdb,&cksum,8) == 0) goto werr;
    return C_OK;
werr:
    if (error) *error = errno;
    if (di) dictReleaseIterator(di);
    return C_ERR;
}

写入键值对的方法是rdbSaveKeyValuePair(),主要步骤:

  • 写入过期时间操作码和值(可选)
  • 写入 LRU 操作码和闲置时间(可选)
  • 写入 LFU 操作码和访问频率信息(可选)
  • 依次写入键值对的类型、Key值、Value值
int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val, long long expiretime) {
    int savelru = server.maxmemory_policy & MAXMEMORY_FLAG_LRU;
    int savelfu = server.maxmemory_policy & MAXMEMORY_FLAG_LFU;

    // 写入过期时间
    if (expiretime != -1) {
        if (rdbSaveType(rdb,RDB_OPCODE_EXPIRETIME_MS) == -1) return -1;
        if (rdbSaveMillisecondTime(rdb,expiretime) == -1) return -1;
    }

    // 写入LRU操作码和闲置时间
    if (savelru) {
        uint64_t idletime = estimateObjectIdleTime(val);
        idletime /= 1000; /* Using seconds is enough and requires less space.*/
        if (rdbSaveType(rdb,RDB_OPCODE_IDLE) == -1) return -1;
        if (rdbSaveLen(rdb,idletime) == -1) return -1;
    }

    // 写入LFU操作码和访问频率信息
    if (savelfu) {
        uint8_t buf[1];
        buf[0] = LFUDecrAndReturn(val);
        if (rdbSaveType(rdb,RDB_OPCODE_FREQ) == -1) return -1;
        if (rdbWriteRaw(rdb,buf,1) == -1) return -1;
    }
    // 依次写入 键值对类型、Key值、Value值
    if (rdbSaveObjectType(rdb,val) == -1) return -1;
    if (rdbSaveStringObject(rdb,key) == -1) return -1;
    if (rdbSaveObject(rdb,val,key) == -1) return -1;
    if (server.rdb_key_save_delay)
        debugDelay(server.rdb_key_save_delay);
    return 1;
}

rdbSaveObjectType()用来写入对象类型,Redis 会把 RedisObject 类型映射成 RDB 对象类型,对应的是一个数字。接着开始写入键值对,因为 Key 是字符串,所以会调用rdbSaveStringObject()方法写入,Redis 还会判断 Key 是否能用整型编码,如果可以会直接写入整形,否则写入字符串。

ssize_t rdbSaveStringObject(rio *rdb, robj *obj) {
    // 优先尝试整型编码写入
    if (obj->encoding == OBJ_ENCODING_INT) {
        return rdbSaveLongLongAsStringObject(rdb,(long)obj->ptr);
    } else { // 写入字符串
        serverAssertWithInfo(NULL,obj,sdsEncodedObject(obj));
        return rdbSaveRawString(rdb,obj->ptr,sdslen(obj->ptr));
    }
}

Value 则会根据对象类型有不同的序列化方式写入,方法是rdbSaveObject(),代码这里就不贴了。

尾巴

RDB 持久化可以把 Redis 数据库某一时刻的所有键值对写入磁盘文件,因为是二进制格式,所以恢复速度很快,非常适合数据备份、主从复制场景。转储后的 RDB 文件由文件头、数据库数据、文件尾组成,文件头主要记录 Redis 版本、运行架构信息等等;然后 Redis 会遍历数据库,先写入 SELECT-DB 操作码,再遍历哈希表写入所有键值对;最后写入结尾符和checksum,checksum 的主要作用是确保 RDB 文件没有被篡改或发生损坏。

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

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

相关文章

SpringBoot实现SSMP整合

一、整合JUnit 1、Spring 整合 JUnit 核心注解有两个&#xff1a; RunWith(SpringJUnit4ClassRunner.class) 是设置Spring专用于测试的类运行器&#xff08;Spring程序执行程序有自己的一套独立的运行程序的方式&#xff0c;不能使用JUnit提供的类运行方式&#xff09;Conte…

Deep Learning(0-14草履虫)

深度学习解决的问题 自动提取出最合适的特征 深度学习应用 神经网络基础 损失函数 前向传播 反向传播 绿色字体为正向传播i输入&#xff0c;红色字体为反向传播梯度 MAX门单元只把梯度传给最大的 神经网络整体架构 激活函数 隐藏层激活函数 一般选择Relu&#xff0c;sigmoid会…

STM32 IWDGWWDG

STM32 IWDG&WWDG 启动看门狗之后&#xff0c;看门狗是不能再被关闭的&#xff0c;除非发生复位。 IWDG独立看门狗 独立看门狗配置流程 开启LSI时钟&#xff0c;只有LSI时钟开启了&#xff0c;独立看门狗才能运行。 但是开启LSI的代码&#xff0c;并不需要我们来写&#xf…

【23种设计模式】装饰器模式

个人主页&#xff1a;金鳞踏雨 个人简介&#xff1a;大家好&#xff0c;我是金鳞&#xff0c;一个初出茅庐的Java小白 目前状况&#xff1a;22届普通本科毕业生&#xff0c;几经波折了&#xff0c;现在任职于一家国内大型知名日化公司&#xff0c;从事Java开发工作 我的博客&am…

WinSCP 集成 putty(也可以其他Terminal客户端)

putty 安装 官网安装地址 WinSCP集成putty&#xff08;也可以其他Terminal客户端&#xff09; 扩展 WinSCP是什么&#xff1f; WinSCP&#xff08;Windows Secure Copy Protocol&#xff09;是一个用于 Windows 操作系统的开源的 SFTP&#xff08;SSH File Transfer Protoc…

【Unity HDRP渲染管线下的WorleyUtilities文件,“Hash”函数】

Unity HDRP内置文件WorleyUtilities WorleyUtilities文件路径如下:文件代码如下然后转译到ShaderLab中:存档:WorleyUtilities文件路径如下: D:…\Library\PackageCache\com.unity.render-pipelines.high-definition@14.0.8\Runtime\Lighting\VolumetricClouds\WorleyUtili…

网络解析(二)

ICMP 报文有很多的类型,不同的类型有不同的代码。最常用的类型是主动请求为 8,主动请求的应答为 0。 ICMP 相当于网络世界的侦察兵。我讲了两种类型的 ICMP 报文,一种是主动探查的查询报文,一种异常报告的差错报文; ping 使用查询报文,Traceroute 使用差错报文。 IP和…

C++ 用户学习 Python 的最佳方法

对于很多是一名计算机科学专业的学生而言&#xff0c;很多入门是学习的C和 C&#xff0c;可能熟悉非常基本的 python 语法&#xff0c;以及 C 中相当高级的数据结构。现在想深入学习Python的话&#xff0c;光看很多在线教程可能没法有较大的提升&#xff0c;这里有一些针对C用户…

信息系统项目管理师第四版学习笔记——组织通用治理

组织战略 组织战略是组织高质量发展的总体谋略&#xff0c;是组织相关干系方就其发展达成一致认识的重要基础。组织战略是指组织针对其发展进行的全局性、长远性、纲领性目标的策划和选择。 战略目标是组织在一定的战略期内总体发展的总水平和总任务。它决定了组织在该战略期…

CodePlan

CodePlan论文解读 最近在看老师给的LLM-Agent论文&#xff0c;在这记录一下 CodePlan: Repository-level Coding using LLMs and Planning【论文】 旨在解决储存库级别的coding task&#xff0c;提出一个框架called CodePlan综合多步骤的编辑链&#xff0c;其中每个步骤都导…

华为---PPP协议简介及示例配置

PPP协议简介 PPP是Point-to-Point Protocol的简称&#xff0c;中文翻译为点到点协议。与以太网协议一样,PPP也是一个数据链路层协议。以太网协议定义了以太帧的格式&#xff0c;PPP协议也定义了自己的帧格式&#xff0c;这种格式的帧称为PPP帧。 利用PPP协议建立的二层网络称为…

云耀服务器L实例部署Typecho开源博客系统|华为云云耀云服务器L实例评测使用体验

云耀服务器L实例部署Typecho开源博客系统 文章目录 云耀服务器L实例部署Typecho开源博客系统1. 华为云云耀服务器L实例介绍2. Typecho2.1 Typecho 3. 部署华为云云耀服务器L实例3.1 云耀服务器L实例购买3.1.1 云耀服务器L实例初始化配置3.1.2 远程登录云耀服务器L实例 4. Typec…

基于MATLAB的图像条形码识别系统(matlab毕毕业设计2)

摘要 &#xff1a; 本论文旨在介绍一种基于MATLAB的图像条形码识别系统。该系统利用计算机视觉技术和图像处理算法&#xff0c;实现对不同类型的条形码进行准确识别。本文将详细介绍系统学习的流程&#xff0c;并提供详细教案&#xff0c;以帮助读者理解和实施该系统。 引言…

Git构建分布式版本控制系统

一、版本控制 1、概念&#xff1a; 版本控制&#xff08;Version Control&#xff09;&#xff0c;也被称为版本管理、源代码管理或代码控制&#xff0c;是一种系统和工具&#xff0c;用于跟踪和管理文件、数据或源代码的不同版本和历史记录&#xff0c;在软件开发、文档管理…

深入理解Huffman编码:原理、代码示例与应用

目录 ​编辑 介绍 Huffman编码的原理 信息理论背景 频率统计 Huffman树 Huffman编码的代码示例 数据结构 权重选择 Huffman编码生成 完整示例 完整代码 测试截图 Huffman编码的应用 总结 介绍 在这个数字时代&#xff0c;数据的有效压缩和传输变得至关重要。Hu…

【Linux】Ubunt20.04在vscode中使用Fira Code字体【教程】

【Linux】Ubunt20.04在vscode中使用Fira Code字体【教程】 文章目录 【Linux】Ubunt20.04在vscode中使用Fira Code字体【教程】1. 什么是Fira Code字体2. 安装Fira Code字体3. 配置vscodeReference 1. 什么是Fira Code字体 Fira Code&#xff1a;是一种带有编程连字的等宽字体。…

多组试验时正态分布标准差估计公式

本文介绍如何通过多组试验数据来估计正态总体的标准差. 一,各组试验次数相等 设正态总体X&#xff5e;N(μ,σ),其中均值μ和标准差σ未知.今有m组样本,每组样本大小n相等,其试验数据如下:求标准差σ的估计σ. 多组试验时正态分布标准差估计公式 - 百度学术

机器人制作开源方案 | 行星探测车概述

1. 功能描述 行星探测车&#xff08;Planetary Rover&#xff09;是一种用于进行科学探索和勘测任务的无人车辆&#xff0c;它们被设计成能够适应各种复杂的地形条件和极端环境&#xff0c;以便收集数据、拍摄照片、采集样本等。行星探测车通常包含以下主要组件和功能&#xff…

Ubuntu - 查看 IP 地址

要查看 Ubuntu 操作系统中的 IP 地址&#xff0c;可以使用 ip 命令或者 ifconfig 命令。以下是使用这两个命令的示例&#xff1a; 使用 ip 命令&#xff1a; 打开终端。 输入以下命令&#xff1a; ip a 这将显示网络接口信息&#xff0c;包括 IP 地址。通常&#xff0c;IP…

彩虹工具网程序开源未加密版源码_支持插件扩展 支持暗黑模式

2023全新UI彩虹站长在线工具箱系统源码下载 全开源版本 支持暗黑模式 支持高达72种站长工具、开发工具、娱乐工具等功能。本地调用API、自带免费API接口&#xff0c; 是一个多功能性工具程序支持后台管理、上传插件、添加增减删功能。 源码下载&#xff1a;https://download…