gitbook/Redis源码剖析与实战/docs/423390.md
2022-09-03 22:05:03 +08:00

218 lines
15 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 加餐4 | RDB和AOF文件损坏了咋办?
你好,我是蒋德钧。今天的加餐课程,我来和你聊聊Redis对损坏的RDB和AOF文件的处理方法。
我们知道,Redis为了提升可靠性,可以使用AOF记录操作日志,或者使用RDB保存数据库镜像。AOF文件的记录和RDB文件的保存都涉及写盘操作,但是,如果在写盘过程中发生了错误,就会导致AOF或RDB文件不完整。而Redis使用不完整的AOF或RDB文件,是无法恢复数据库的。那么在这种情况下,我们该怎么处理呢?
实际上,Redis为了应对这个问题,就专门实现了针对AOF和RDB文件的完整性检测工具,也就是redis-check-aof和redis-check-rdb两个命令。今天这节课,我就来给你介绍下这两个命令的实现以及它们的作用。学完这节课后,如果你再遇到无法使用AOF或RDB文件恢复Redis数据库时,你就可以试试这两个命令。
接下来,我们先来看下AOF文件的检测和修复。
## AOF文件检测与修复
要想掌握AOF文件的检测和修复,我们首先需要了解下,AOF文件的内容格式是怎样的。
### AOF文件的内容格式
AOF文件记录的是Redis server运行时收到的操作命令。当Redis server往AOF文件中写入命令时,它会按照RESP 2协议的格式来记录每一条命令。当然,如果你使用了Redis 6.0版本,那么Redis会采用RESP 3协议。我在第一季的时候,曾经给你介绍过Redis客户端和server之间进行交互的[RESP 2协议](https://time.geekbang.org/column/article/298504),你可以再去回顾下。
这里我们就简单来说下,RESP 2协议会为每条命令或每个数据进行编码,在每个编码结果后面增加一个**换行符“\\r\\n”**,表示一次编码结束。一条命令通常会包含命令名称和命令参数,RESP 2协议会使用数组编码类型来对命令进行编码。比如,当我们在AOF文件中记录一条SET course redis-code命令时,Redis会分别为命令本身SET、key和value的内容course和redis-code进行编码,如下所示:
```plain
*3
$3
SET
$6
course
$10
redis-code
```
**注意**,编码结果中以“`*`”或是“`$`”开头的内容很重要,因为“`*`”开头的编码内容中的数值,表示了一条命令操作包含的参数个数。当然,命令本身也被算为是一个参数,记录在了这里的数值当中。而“`$`”开头的编码内容中的数字,表示紧接着的字符串的长度。
在刚才介绍的例子中,“`*3`”表示接下来的命令有三个参数,这其实就对应了SET命令本身,键值对的key“course”和value“redis-code”。而“`$3`”表示接下来的字符串长度是3,这就对应了SET命令这个字符串本身的长度是3。
好了,了解了AOF文件中命令的记录方式之后,我们再来学习AOF文件的检测就比较简单了。这是因为RESP 2协议会将命令参数个数、字符串长度这些信息,通过编码也记录到AOF文件中,redis-check-aof命令在检测AOF文件时,就可以利用这些信息来判断一个命令是否完整记录了。
### AOF文件的检测过程
AOF文件的检测是在redis-check-aof.c文件中实现的,这个文件的入口函数是**redis\_check\_aof\_main**。在这个函数中,我们可以看到AOF检测的主要逻辑,简单来说可以分成三步。
**首先**,redis\_check\_aof\_main会调用fopen函数打开AOF文件,并调用redis\_fstat函数获取AOF文件的大小size。
**其次**,redis\_check\_aof\_main会调用process函数,实际检测AOF文件。process函数会读取AOF文件中的每一行,并检测是否正确,我一会儿会给你具体介绍这个函数。这里你要知道,process函数在检测AOF文件时,如果发现有不正确或是不完整的命令操作,它就会停止执行,并且返回已经检测为正确的AOF文件位置。
**最后**,redis\_check\_aof\_main会根据process函数的返回值,来判断已经检测为正确的文件大小是否等于AOF文件本身大小。如果不等于的话,redis\_check\_aof\_main函数会根据redis-check-aof命令执行时的fix选项,来决定是否进行修复。
下面的代码就展示了刚才介绍的AOF文件检测的基本逻辑,你可以看下。
```plain
//调用fopen打开AOF文件
FILE *fp = fopen(filename,"r+");
 
//调用redis_fstat获取文件大小size
struct redis_stat sb;
if (redis_fstat(fileno(fp),&sb) == -1) { …}
off_t size = sb.st_size;
…
off_t pos = process(fp); //调用process检测AOF文件,返回AOF文件中已检测为正确的位置
off_t diff = size-pos;  //获取已检测正确的文件位置和文件大小的差距
printf("AOF analyzed: size=%lld, ok_up_to=%lld, diff=%lld\n", (long long) size, (long long) pos, (long long) diff);
if (diff > 0) { …} //如果检测正确的文件位置还小于文件大小,可以进行修复
else {
   printf("AOF is valid\n");
}
```
你也可以参考这里我给出的示意图:
![图片](https://static001.geekbang.org/resource/image/8d/c3/8d71de18442494a5301c44faf0de16c3.jpg?wh=1920x1080)
OK,了解了AOF文件检测的基本逻辑后,我们再来看下刚才介绍的process函数。
process函数的主体逻辑,其实就是**执行一个while(1)的循环流程**。在这个循环中,process函数首先会调用readArgc函数,来获取每条命令的参数个数,如下所示:
```plain
int readArgc(FILE *fp, long *target) {
    return readLong(fp,'*',target);
}
```
readArgc函数会进一步调用readLong函数,而readLong函数是用来读取“`*`”“`$`”开头的这类编码结果的。readLong函数的原型如下所示,它的参数fp是指向AOF文件的指针,参数prefix是编码结果的开头前缀,比如“`*`”或“`$`”,而参数target则用来保存编码结果中的数值。
```plain
int readLong(FILE *fp, char prefix, long *target)
```
这样一来,process函数通过调用readArgc就能获得命令参数个数了。不过,**如果readLong函数从文件中读到的编码结果前缀,和传入的参数prefix不一致,那么它就会报错**,从而让process函数检测到AOF文件中不正确的命令。
然后,process函数会根据命令参数的个数执行一个for循环,调用readString函数逐一读取命令的参数。而readString函数会先调用readLong函数,读取以“`$`”开头的编码结果,这也是让readString函数获得要读取的字符串的长度。
紧接着,readString函数就会调用readBytes函数,从AOF文件中读取相应长度的字节。不过,**如果此时readBytes函数读取的字节长度,和readLong获得的字符串长度不一致,那么就表明AOF文件不完整**,process函数也会停止读取命令参数,并进行报错。
下面的代码展示了readBytes函数的主要逻辑,你可以看下。
```plain
int readBytes(FILE *fp, char *target, long length) {
    …
    real = fread(target,1,length,fp);  //从AOF文件中读取length长度的字节
    if (real != length) {  //如果实际读取的字节不等于length,则报错
        ERROR("Expected to read %ld bytes, got %ld bytes",length,real);
        return 0;
}
…}
```
这里你需要注意的是,如果process函数实际读取的命令参数个数,小于readArgc函数获取的参数个数,那么process函数也会停止继续检测AOF文件。
最后,process函数会打印检测AOF文件过程中遇到的错误,并返回已经检测正确的文件位置。
现在,我们就了解了AOF文件检测过程中的主要函数process,你也可以看下这里我给出的示意图。
![图片](https://static001.geekbang.org/resource/image/03/c1/034349e8e77a5b7ae8355523ef2e4dc1.jpg?wh=1920x1080)
那么,就像我刚才给你介绍的,process函数返回已经检测的文件位置后,redis\_check\_aof\_main函数会判断是否已经完成整个AOF文件的检查。如果没有的话,那么就说明在检测AOF文件的过程中,发生了某些错误,此时redis\_check\_aof\_main函数会判断redis-check-aof命令执行时,**是否带了“–fix”选项**。
而如果有这一选项,redis\_check\_aof\_main函数就会开始执行修复操作。接下来,我们就来看下AOF文件的修复。
### AOF文件的修复
AOF文件的修复其实实现得很简单,它就是从AOF文件已经检测正确的位置开始,调用**ftruncate函数**执行截断操作。
这也就是说,redis-check-aof命令在对AOF文件进行修复时,一旦检测到有不完整或是不正确的操作命令时,它就只保留了从AOF文件开头到出现不完整,或是不正确的操作命令位置之间的内容,而不完整或是不正确的操作命令,以及其后续的内容就被直接删除了。
所以,我也给你一个**小建议**,当你使用redis-check-aof命令修复AOF文件时,最好是把原来的AOF文件备份一份,以免出现修复后原始AOF文件被截断,而带来的操作命令缺失问题。
下面的代码展示了redis\_check\_aof\_main函数中对AOF文件进行修复的基本逻辑,你可以看下。
```plain
if (fix) {
   …
   if (ftruncate(fileno(fp), pos) == -1) {
      printf("Failed to truncate AOF\n");
      exit(1);
    } else {
      printf("Successfully truncated AOF\n");
 }
```
好了,到这里,你就了解了AOF文件的检测和修复。接下来,我们再来看下RDB文件的检测。
## RDB文件检测
RDB文件检测是在redis-check-rdb.c文件中实现的,这个文件的入口函数是**redis\_check\_rdb\_main**。它会调用redis\_check\_rdb函数,来完成检测。
这里你要知道,和AOF文件用RESP 2协议格式记录操作命令不一样,RDB文件是Redis数据库内存中的内容在某一时刻的快照,它包括了文件头、键值对数据部分和文件尾三个部分。而且,**RDB文件是通过一定的编码来记录各种属性信息和键值对的**。我在[第18讲](https://time.geekbang.org/column/article/415563)中给你介绍过RDB文件的组成和编码方式,建议你可以再去回顾下,毕竟只有理解了RDB文件的组成和编码,你才能更好地理解redis\_check\_rdb函数是如何对RDB文件进行检测的。
其实,redis\_check\_rdb函数检测RDB文件的逻辑比较简单,就是根据RDB文件的组成,逐一检测文件头的魔数、文件头中的属性信息、文件中的键值对数据,以及最后的文件尾校验和。下面,我们就来具体看下。
首先,redis\_check\_rdb函数**先读取RDB文件头的9个字节**,这是对应RDB文件的魔数,其中包含了“REDIS”字符串和RDB的版本号。redis\_check\_rdb函数会检测“REDIS”字符串和版本号是否正确,如下所示:
```plain
int redis_check_rdb(char *rdbfilename, FILE *fp) {
…
if (rioRead(&rdb,buf,9) == 0) goto eoferr;  //读取文件的头9个字节
buf[9] = '\0';
if (memcmp(buf,"REDIS",5) != 0) { //将前5个字节和“REDIS”比较,如果有误则报错
   rdbCheckError("Wrong signature trying to load DB from file");
   goto err;
}
rdbver = atoi(buf+5); //读取版本号
if (rdbver < 1 || rdbver > RDB_VERSION) { //如果有误,则报错
   rdbCheckError("Wrong signature trying to load DB from file");
   goto err;
}
…}
```
然后,redis\_check\_rdb函数会**执行一个while(1)循环流程**,在这个循环中,它会按照RDB文件的格式依次读取其中的内容。我在第18讲中也给你介绍过,RDB文件在文件头魔数后记录的内容,包括了Redis的属性、数据库编号、全局哈希表的键值对数量等信息。而在实际记录每个键值对之前,RDB文件还要记录键值对的过期时间、LRU或LFU等信息,这些信息都是按照操作码和实际内容来组织的。
比如,RDB文件中要记录Redis的版本号,它就会用十六进制的FA表示操作码,表明接下来的内容就是Redis版本号;当它要记录使用的数据库时,它会使用十六进制的FE操作码来表示。
好了,了解了RDB文件的这种内容组织方式后,我们接着来看下**redis\_check\_rdb函数**,它在循环流程中,就是先调用rdbLoadType函数读取操作码,然后,使用多个条件分支来匹配读取的操作码,并根据操作码含义读取相应的操作码内容。
以下代码就展示了这部分的操作码读取和分支判断逻辑,而对于每个分支中读取操作码的具体实现,你可以去仔细阅读一下源码。
```plain
if ((type = rdbLoadType(&rdb)) == -1) goto eoferr;
if (type == RDB_OPCODE_EXPIRETIME) { …} //表示过期时间的操作码
else if (type == RDB_OPCODE_EXPIRETIME_MS) {…} //表示毫秒记录的过期时间操的作码
else if (type == RDB_OPCODE_FREQ) {…} //表示LFU访问频率的操作码
else if (type == RDB_OPCODE_IDLE) {…} //表示LRU空闲时间的操作码
else if (type == RDB_OPCODE_EOF) {…} //表示RDB文件结束的操作码
else if (type == RDB_OPCODE_SELECTDB) {…} //表示数据库选择的操作码
else if (type == RDB_OPCODE_RESIZEDB) {…} //表示全局哈希表键值对数量的操作码
else if (type == RDB_OPCODE_AUX) {…} //表示Redis属性的操作码
else {…} //无法识别的操作码
```
这样,在判断完操作码之后,redis\_check\_rdb函数就会调用rdbLoadStringObject函数,读取键值对的key,以及调用rdbLoadObject函数读取键值对的value。
最后,当读取完所有的键值对后,redis\_check\_rdb函数就会读取文件尾的校验和信息,然后验证校验和是否正确。
到这里,整个RDB文件的检测就完成了。在这个过程中,如果redis\_check\_rdb函数发现RDB文件有错误,就会将错误在文件中出现的位置、当前的检测操作、检测的键值对的key等信息打印出来,以便我们自行进一步检查。
## 小结
今天这节课,我带你了解了检测AOF文件和RDB文件正确性和完整性的两个命令,redis-check-aof和redis-check-rdb,以及它们各自的实现过程。
对于**redis-check-aof命令**来说,它是根据AOF文件中记录的操作命令格式,逐一读取命令,并根据命令参数个数、参数字符串长度等信息,进行命令正确性和完整性的判断。
对于**redis-check-rdb命令**来说,它的实现逻辑较为简单,也就是按照RDB文件的组织格式,依次读取RDB文件头、数据部分和文件尾,并在读取过程中判断内容是否正确,并进行报错。
事实上,Redis server在运行时,遇到故障而导致AOF文件或RDB文件没有记录完整,这种情况有时是不可避免的。当了解了redis-check-aof命令的实现后,我们就知道它可以提供出现错误或不完整命令的文件位置,并且,它本身提供了修复功能,可以从出现错误的文件位置处截断后续的文件内容。不过,如果我们不想通过截断来修复AOF文件的话,也可以尝试人工修补。
而在了解了redis-check-rdb命令的实现后,我们知道它可以发现RDB文件的问题所在。不过,redis-check-rdb命令目前并没有提供修复功能。所以如果我们需要修复的话,就只能通过人工自己来修复了。
## 每课一问
redis\_check\_aof\_main函数是检测AOF文件的入口函数,但是它还会调用检测RDB文件的入口函数redis\_check\_rdb\_main,那么你能找到这部分代码,并通过阅读说说它的作用吗?