逐字节讲解 Redis 持久化(RDB 和 AOF)的文件格式(一)

2023-11-20 17:20

本文主要是介绍逐字节讲解 Redis 持久化(RDB 和 AOF)的文件格式(一),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

相信各位对 Redis 的这两种持久化机制都不陌生,简单来说,RDB 就是对数据的全量备份,AOF 则是增量备份,而从 4.0 版本开始引入了混合方式,以 7.2.3 版本为例,会生成三类文件:RDB、AOF 和记录 aof 文件的元数据信息文件,如下图所示,这时的 AOF 可以看作是一种差异备份。

image-20231117142130770

接下来本文将结合具体的备份文件,通过分析其结构,从另一种角度来看两种持久化方式的差异。

RDB

首先是对 RDB 全量备份文件的解析,想要生成 RDB 文件,有两种方式,一种是手动方式:使用 save(阻塞)或者 bgsave(非阻塞)命令生成,一种是在配置文件中增加save m n(表示在 m 内,至少出现了 n 次变更就会执行 bgsave 命令)配置来实现。

下面就以一个具体的dump.rdb(在 0 号库中有一条键为 hello,值为 world 的记录)文件为例来解析其文件格式,由于 RDB 文件是二进制格式,这里使用了一个在线的十六进制编辑器进行查看:

image-20231117151039644

下文均是结合 Redis 7.2.3 版本的源码的 rdb.c 文件进行解析,对应源码地址。

0x00 Redis 版本

52 45 44 49 53 30 30 31 31,根据源码snprintf(magic,sizeof(magic),"REDIS%04d",RDB_VERSION);可以看到这里前五位是固定值REDIS,后四位用于标识RDB的版本对应11。

0x01 辅助信息

这部分涉及数据较多,先放出源码:

if (rdbSaveAuxFieldStrStr(rdb,"redis-ver",REDIS_VERSION) == -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb,"redis-bits",redis_bits) == -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb,"ctime",time(NULL)) == -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb,"used-mem",zmalloc_used_memory()) == -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb, "aof-base", aof_base) == -1) return -1;

结合编辑器右侧的信息,可以发现这部分数据下图中选中的数据:

在这里插入图片描述

  1. redis-ver(Redis 版本)

    这部分对应FA 09 72 65 64 69 73 2D 76 65 72 05 37 2E 32 32 2E 33,其中开头的FA(250)代表这部分数据是 AUX 属性字段,根据源码#define RDB_OPCODE_AUX 250可以了解到。然后是09 72 65 64 69 73 2D 76 65 72,09 代表随后的 9个字节是属性名,即redis-ver,最后是05 37 2E 32 32 2E 33,其中 05 代表随后的 5 个字节是属性名对应的字段值,即 Redis 的版本号7.2.3

  2. redis-bits(位架构)

    这部分对应FA 0A 72 65 64 69 73 2D 62 69 74 73 C0 40。参考 1 可知开始的FA代表AUXOA代表随后的 10 字节是属性名,即redis-bits。但是随后的C0就不再是代表值的长度了,这里先说明C0代表后续的一个字节按照整数进行读取,对应0x40(64),即代表是 Redis 的 64位架构。下面我们再来说明为什么会有以上的区别:

    其实代表值长度的不一定只有一个字节,这里会根据前两位进行判断(C0 对应1100 0000):

    • 如果前两位是 00 ,那么后续的 6 位(可表示 0 ~ 63)就代表实际的字符串长度。

    • 如果前两位是 01,那么接下来的一个字节也会用于表示长度,加上第一个剩下的 6 位,总共 14 位(可表示0 ~ 16383)代表实际的字符串长度。

    • 如果前两位是 10,那么剩下 6 位的值如果是 0,就代表随后的 32 字节代表具体长度,如果剩下 6 位的值是 1,就代表随后的 64 字节代表具体长度。

    • 如果前两位是 11,则需要根据整个字节的值再进行判断,如果是C0就代表将随后的 1 字节表示整数,如果是 C1 就代表随后的 2 字节表示整数,如果是 C2 就代表随后的 4 字节表示整数,如果是C3就代表随后的内容是使用LZF 压缩算法处理后的内容。

  3. ctime(文件创建时间)

    这部分对应FA 05 63 74 69 6D 65 C2 44 11 57 65,参考 1 可知开始的FA代表AUX05代表随后的 5 字节是属性名,即ctime。参考 2 中解析,可知随后的C2代表后续的 4 字节即44 11 57 65表示整数,由于需要按照小端序读取,因此对应的内容是 0x65571144,即秒级时间戳,如下图所示:

    image-20231120085845280

  4. used-mem(内存使用大小)

    这部分对应FA 08 75 73 65 64 2D 6D 65 6D C2 40 15 12 00,参考 1 可知开始的FA代表AUX08代表随后的 8 字节是属性名,即used-mem。参考 3 ,可知随后的C2代表后续的 4 字节即40 15 12 00表示整数,对应的内容是 0x00121540,即 Redis 在 创建 rdb 文件前占用的内存是 1185088 字节(1.13 MB)。

  5. aof-base (是否为 aof 基准文件)

    这部分对应FA 08 61 6F 66 2D 62 61 73 65 C0 00,参考 1 可知开始的FA代表AUX08代表随后的 8 字节是属性名,即aof-base。参考 2 中解析,可知随后的C0代表后续的 1 字节即00表示整数,即该 RDB 文件不是作为 AOF 的基准文件,后文中可以看到在 AOF 中生成的 RDB 文件中该值为 1。

0x02 数据部分

FE 00 FB 01 00 00 05 68 65 6C 6C 6F 05 77 6F 72 6C 64,这部分开始对应具体的数据信息,先展示源码:

/* save all databases, skip this if we're in functions-only mode */
if (!(req & SLAVE_REQ_RDB_EXCLUDE_DATA)) {for (j = 0; j < server.dbnum; j++) {if (rdbSaveDb(rdb, j, rdbflags, &key_counter) == -1) goto werr;}
}// 以下内容是 rdbSaveDb 函数内的语句/* Write the SELECT DB opcode */
if ((res = rdbSaveType(rdb,RDB_OPCODE_SELECTDB)) < 0) goto werr;
written += res;
if ((res = rdbSaveLen(rdb, dbid)) < 0) goto werr;
written += res;
/* Write the RESIZE DB opcode. */
unsigned long long expires_size = dbSize(db, DB_EXPIRES);
if ((res = rdbSaveType(rdb,RDB_OPCODE_RESIZEDB)) < 0) goto werr;
written += res;
if ((res = rdbSaveLen(rdb,db_size)) < 0) goto werr;
written += res;
if ((res = rdbSaveLen(rdb,expires_size)) < 0) goto werr;
written += res;

可以看出这部分是遍历所有的数据库内容然后进行保存,下面再结合具体的内容进行介绍。

首先是FE 00,其中FE(254)对应RDB_OPCODE_SELECTDB常量是查询数据库的标志,00即代表 0 号数据库。

然后是FB 01 00,其中FB(251)对应RDB_OPCODE_RESIZEDB常量是查询该数据库大小的标志,根据if ((res = rdbSaveLen(rdb,db_size)) < 0) goto werr;知道01代表数据库的大小,即只有一条数据,根据if ((res = rdbSaveLen(rdb,expires_size)) < 0) goto werr;知道00代表没有包含过期标志的数据。

最后是00 05 68 65 6C 6C 6F 05 77 6F 72 6C 64,代表具体的数据内容。其中开始的00代表类型是字符串,参考源码可知(RDB_TYPE_STRING 的值是 0):

/* Save the object type of object "o". */
int rdbSaveObjectType(rio *rdb, robj *o) {switch (o->type) {case OBJ_STRING:return rdbSaveType(rdb,RDB_TYPE_STRING);case OBJ_LIST:if (o->encoding == OBJ_ENCODING_QUICKLIST || o->encoding == OBJ_ENCODING_LISTPACK)return rdbSaveType(rdb, RDB_TYPE_LIST_QUICKLIST_2);elseserverPanic("Unknown list encoding");case OBJ_SET:if (o->encoding == OBJ_ENCODING_INTSET)return rdbSaveType(rdb,RDB_TYPE_SET_INTSET);else if (o->encoding == OBJ_ENCODING_HT)return rdbSaveType(rdb,RDB_TYPE_SET);else if (o->encoding == OBJ_ENCODING_LISTPACK)return rdbSaveType(rdb,RDB_TYPE_SET_LISTPACK);elseserverPanic("Unknown set encoding");case OBJ_ZSET:if (o->encoding == OBJ_ENCODING_LISTPACK)return rdbSaveType(rdb,RDB_TYPE_ZSET_LISTPACK);else if (o->encoding == OBJ_ENCODING_SKIPLIST)return rdbSaveType(rdb,RDB_TYPE_ZSET_2);elseserverPanic("Unknown sorted set encoding");case OBJ_HASH:if (o->encoding == OBJ_ENCODING_LISTPACK)return rdbSaveType(rdb,RDB_TYPE_HASH_LISTPACK);else if (o->encoding == OBJ_ENCODING_HT)return rdbSaveType(rdb,RDB_TYPE_HASH);elseserverPanic("Unknown hash encoding");case OBJ_STREAM:return rdbSaveType(rdb,RDB_TYPE_STREAM_LISTPACKS_3);case OBJ_MODULE:return rdbSaveType(rdb,RDB_TYPE_MODULE_2);default:serverPanic("Unknown object type");}return -1; /* avoid warning */
}

随后的05 68 65 6C 6C 6F中的 05表示键的长度是5,对应68 65 6C 6C 6Fhello。最后的05 77 6F 72 6C 64代表值的长度也是 5,内容是77 6F 72 6C 64world

0x03 尾部信息

FF 18 7F 33 2E 0F C6 20 19,根据源码#define RDB_OPCODE_EOF 255可知,FF(25)是文件的 EOF 即结束标志。随后的 8 位根据源码可知对应 CRC64 校验码:

/* EOF opcode */
if (rdbSaveType(rdb,RDB_OPCODE_EOF) == -1) goto werr;/* CRC64 checksum. It will be zero if checksum computation is disabled, the* loading code skips the check in this case. */
cksum = rdb->cksum;
memrev64ifbe(&cksum);
if (rioWrite(rdb,&cksum,8) == 0) goto werr;

AOF

AOF 用于对数据库的增量备份,如果需要开启,需要将配置文件中的appendonly设置为 yes。同时,根据需要可以,设置appenddirname对应保存的文件夹,设置appendfilename用于配置文件名,设置appendfsync 用于配置频率。开启后,可以在指定的文件夹下看到类似以下的文件结构:

image-20231117142130770

其中 rdb 结尾的代表是 AOF 备份的基准文件,aof 文件是增量备份的执行命令信息,manifest 文件是记录 aof 文件的元数据信息。

0x00 dump.aof.1.base.rdb

通过十六进制编辑器打开该文件,可以发现内容和 RDB 中的格式一致(创建数据前备份的,所以没有数据部分):

在这里插入图片描述

而由于是 AOF 的基准文件,这里aof-base的值是01即代表是基准文件。

0x01 dump.aof.1.incr.aof

文本文件,内容如下(*开头代表命令包含的参数个数,$开头代表命令的长度):

*2       // 两个参数
$6       // 第一个参数长度为 6, 对应 SELECT 的长度
SELECT   
$1       // 第二个参数长度为 1, 对应 0, 即 0 号数据库
0
*3       // 三个参数
$3       // 第一个参数长度为 3, 对应 set 的长度
set
$5       // 第二个参数长度为 5, 对应 hello 的长度
hello
$0       // 第三个参数长度为 0*3       // 三个参数
$3       // 第一个参数长度为 3, 对应 set 的长度
set
$5       // 第二个参数长度为 5, 对应 hello 的长度
hello
$5
world    // 第三个参数长度为 5, 对应 world 的长度

0x02 dump.aof.manifest

文本文件,内容如下:

file dump.aof.1.base.rdb seq 1 type b
file dump.aof.1.incr.aof seq 1 type i

其中seq 1 代表文件序号为 1,type b代表type base即基准文件,type i代表type increment即增量文件。

总结

本文根据一个简单的 RDB 文件讲解了 RDB 文件的存储格式,同时也简单介绍了 AOF 的文件格式。关于 RDB 中的 LZF 压缩算法和更复杂数据的存储方式(包含过期时间,数据类型为 Set,Map)等未作介绍,将留到下次。

这篇关于逐字节讲解 Redis 持久化(RDB 和 AOF)的文件格式(一)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/396216

相关文章

详谈redis跟数据库的数据同步问题

《详谈redis跟数据库的数据同步问题》文章讨论了在Redis和数据库数据一致性问题上的解决方案,主要比较了先更新Redis缓存再更新数据库和先更新数据库再更新Redis缓存两种方案,文章指出,删除R... 目录一、Redis 数据库数据一致性的解决方案1.1、更新Redis缓存、删除Redis缓存的区别二

Redis与缓存解读

《Redis与缓存解读》文章介绍了Redis作为缓存层的优势和缺点,并分析了六种缓存更新策略,包括超时剔除、先删缓存再更新数据库、旁路缓存、先更新数据库再删缓存、先更新数据库再更新缓存、读写穿透和异步... 目录缓存缓存优缺点缓存更新策略超时剔除先删缓存再更新数据库旁路缓存(先更新数据库,再删缓存)先更新数

Redis事务与数据持久化方式

《Redis事务与数据持久化方式》该文档主要介绍了Redis事务和持久化机制,事务通过将多个命令打包执行,而持久化则通过快照(RDB)和追加式文件(AOF)两种方式将内存数据保存到磁盘,以防止数据丢失... 目录一、Redis 事务1.1 事务本质1.2 数据库事务与redis事务1.2.1 数据库事务1.

mac安装redis全过程

《mac安装redis全过程》文章内容主要介绍了如何从官网下载指定版本的Redis,以及如何在自定义目录下安装和启动Redis,还提到了如何修改Redis的密码和配置文件,以及使用RedisInsig... 目录MAC安装Redis安装启动redis 配置redis 常用命令总结mac安装redis官网下

Redis主从复制实现原理分析

《Redis主从复制实现原理分析》Redis主从复制通过Sync和CommandPropagate阶段实现数据同步,2.8版本后引入Psync指令,根据复制偏移量进行全量或部分同步,优化了数据传输效率... 目录Redis主DodMIK从复制实现原理实现原理Psync: 2.8版本后总结Redis主从复制实

SpringBoot使用注解集成Redis缓存的示例代码

《SpringBoot使用注解集成Redis缓存的示例代码》:本文主要介绍在SpringBoot中使用注解集成Redis缓存的步骤,包括添加依赖、创建相关配置类、需要缓存数据的类(Tes... 目录一、创建 Caching 配置类二、创建需要缓存数据的类三、测试方法Spring Boot 熟悉后,集成一个外

Redis分布式锁使用及说明

《Redis分布式锁使用及说明》本文总结了Redis和Zookeeper在高可用性和高一致性场景下的应用,并详细介绍了Redis的分布式锁实现方式,包括使用Lua脚本和续期机制,最后,提到了RedLo... 目录Redis分布式锁加锁方式怎么会解错锁?举个小案例吧解锁方式续期总结Redis分布式锁如果追求

Redis的Hash类型及相关命令小结

《Redis的Hash类型及相关命令小结》edisHash是一种数据结构,用于存储字段和值的映射关系,本文就来介绍一下Redis的Hash类型及相关命令小结,具有一定的参考价值,感兴趣的可以了解一下... 目录HSETHGETHEXISTSHDELHKEYSHVALSHGETALLHMGETHLENHSET

如何提高Redis服务器的最大打开文件数限制

《如何提高Redis服务器的最大打开文件数限制》文章讨论了如何提高Redis服务器的最大打开文件数限制,以支持高并发服务,本文给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧... 目录如何提高Redis服务器的最大打开文件数限制问题诊断解决步骤1. 修改系统级别的限制2. 为Redis进程特别设置限制

Redis KEYS查询大批量数据替代方案

《RedisKEYS查询大批量数据替代方案》在使用Redis时,KEYS命令虽然简单直接,但其全表扫描的特性在处理大规模数据时会导致性能问题,甚至可能阻塞Redis服务,本文将介绍SCAN命令、有序... 目录前言KEYS命令问题背景替代方案1.使用 SCAN 命令2. 使用有序集合(Sorted Set)