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

接下来本文将结合具体的备份文件,通过分析其结构,从另一种角度来看两种持久化方式的差异。
首先是对 RDB 全量备份文件的解析,想要生成 RDB 文件,有两种方式,一种是手动方式:使用 save(阻塞)或者 bgsave(非阻塞)命令生成,一种是在配置文件中增加save m n(表示在 m 内,至少出现了 n 次变更就会执行 bgsave 命令)配置来实现。
下面就以一个具体的dump.rdb(在 0 号库中有一条键为 hello,值为 world 的记录)文件为例来解析其文件格式,由于 RDB 文件是二进制格式,这里使用了一个在线的十六进制编辑器进行查看:

下文均是结合 Redis 7.2.3 版本的源码的 rdb.c 文件进行解析,对应源码地址。
52 45 44 49 53 30 30 31 31,根据源码snprintf(magic,sizeof(magic),"REDIS%04d",RDB_VERSION);可以看到这里前五位是固定值REDIS,后四位用于标识RDB的版本对应11。
这部分涉及数据较多,先放出源码:
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;
结合编辑器右侧的信息,可以发现这部分数据下图中选中的数据:

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。
redis-bits(位架构)
这部分对应FA 0A 72 65 64 69 73 2D 62 69 74 73 C0 40。参考 1 可知开始的FA代表AUX,OA代表随后的 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 压缩算法处理后的内容。
ctime(文件创建时间)
这部分对应FA 05 63 74 69 6D 65 C2 44 11 57 65,参考 1 可知开始的FA代表AUX,05代表随后的 5 字节是属性名,即ctime。参考 2 中解析,可知随后的C2代表后续的 4 字节即44 11 57 65表示整数,由于需要按照小端序读取,因此对应的内容是 0x65571144,即秒级时间戳,如下图所示:

used-mem(内存使用大小)
这部分对应FA 08 75 73 65 64 2D 6D 65 6D C2 40 15 12 00,参考 1 可知开始的FA代表AUX,08代表随后的 8 字节是属性名,即used-mem。参考 3 ,可知随后的C2代表后续的 4 字节即40 15 12 00表示整数,对应的内容是 0x00121540,即 Redis 在 创建 rdb 文件前占用的内存是 1185088 字节(1.13 MB)。
aof-base (是否为 aof 基准文件)
这部分对应FA 08 61 6F 66 2D 62 61 73 65 C0 00,参考 1 可知开始的FA代表AUX,08代表随后的 8 字节是属性名,即aof-base。参考 2 中解析,可知随后的C0代表后续的 1 字节即00表示整数,即该 RDB 文件不是作为 AOF 的基准文件,后文中可以看到在 AOF 中生成的 RDB 文件中该值为 1。
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);
else
serverPanic("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);
else
serverPanic("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);
else
serverPanic("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);
else
serverPanic("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 6F即hello。最后的05 77 6F 72 6C 64代表值的长度也是 5,内容是77 6F 72 6C 64即world。
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 用于对数据库的增量备份,如果需要开启,需要将配置文件中的appendonly设置为 yes。同时,根据需要可以,设置appenddirname对应保存的文件夹,设置appendfilename用于配置文件名,设置appendfsync 用于配置频率。开启后,可以在指定的文件夹下看到类似以下的文件结构:

其中 rdb 结尾的代表是 AOF 备份的基准文件,aof 文件是增量备份的执行命令信息,manifest 文件是记录 aof 文件的元数据信息。
通过十六进制编辑器打开该文件,可以发现内容和 RDB 中的格式一致(创建数据前备份的,所以没有数据部分):

而由于是 AOF 的基准文件,这里aof-base的值是01即代表是基准文件。
文本文件,内容如下(*开头代表命令包含的参数个数,$开头代表命令的长度):
*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 的长度
文本文件,内容如下:
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)等未作介绍,将留到下次。