深入理解Redis持久化机制-RDB、AOF实现详解

1,343 阅读9分钟

Redis是一个基于内存中的数据结构存储系统,它所有的数据都存储在内存中。如果发生断电或者宕机,内存中的数据就会丢失。为了防止数据丢失,Redis提供了两种持久化的方案,一种是RDB(Redis DataBase),另一种是AOF(Append Only File)

本文主要内容参考自《Redis设计与实现》

RDB持久化

RDB持久化指的是将某个时间点上的数据库状态保存到一个RDB文件中,在启动的时候,可以通过RDB文件将数据库状态还原回来rdb

RDB文件的创建和载入

有2个Redis命令可以生成RDB文件,一个是SAVE,另一个是BGSAVESAVE命令会阻塞Redis服务进程,直到RDB文件创建完成为止,在此期间,客户端所有命令请求都会被阻塞。而BGSAVE命令会派生出一个子进程,由子进程负责创建RDB文件,服务器进程继续处理命令请求。当然,在BGSAVE期间,服务器处理SAVEBGSAVEBGREWRITEAOP三个命令的方式会有所不同。具体来说就是,在BGSAVE期间,服务器会拒绝执行SAVEBGSAVE命令(防止产出竞争);对于BGREWRITEAOP命令,服务器会延迟到BGSAVE执行完成才真正开始执行。

创建RDB文件实际上由rdb.c/rdbSave函数完成,SAVEBGSAVE命令会以不同的方式调用该函数,伪代码如下:

def SAVE():
    # 创建RDB文件
    rdbSave()

def BGSAVE():
    # 创建子进程
    pid = fork()
    if pid == 0:
        # 子进程负责创建RDB文件
        rdbSave()
        # 完成之后向父进程发送信号
        signal_parent()
    elif pid > 0:
        # 父进程继续处理命令请求,并通过轮询等待子进程信号
        handle_request_and_wait_sifnal()
    else:
        # 处理出错
        handle_fork_error()

fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新进程(子进程)的进程id。fork之后,操作系统会复制一个与父进程完全相同的子进程,虽说是父子关系,但是在操作系统看来,他们更像兄弟关系,这2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,子进程拥有父进程当前运行到的位置。具体可参考fork出的子进程和父进程

RDB文件载入是在Redis启动的时候自动执行的。在服务只开启RDB持久化时,只要在启动的时候检测到RDB文件,就会执行自动载入。在RDB文件载入期间,服务器会一直处理阻塞状态,直到载入完成为止。

自动间隔保存

除了手动执行SAVE或者BGSAVE命令来创建RDB文件,Redis还支持通过设置服务器配置的save选项,让服务器每隔一段时间自动执行BGSAVE命令。

用户可以设置多个保存条件,只要其中一个条件被满足,服务器就会执行BGSAVE命令。例如:

save 900 1
save 300 10
save 60 10000

那么满足以下任意一个条件,BGSAVE命令就会被执行:

  1. 服务器在900秒之内,对数据库进行了至少1次修改。
  2. 服务器在300秒之内,对数据库进行了至少10次修改。
  3. 服务器在60秒之内,对数据库进行了至少10000次修改。

RDB文件结构

一个完整RDB文件包含的各个部分如下图所示: rdbfile

为了方便区分变量、数据和常量,本文用全大写单词表示常量,用全小写单词表示变量和数据。

数据部分数据类型长度数据含义
REDIS常量5字节RDB文件标识,用来快速检查载入的文件是否是RDB文件
db_version变量4字节RDB文件版本号
database数据不定Redis各个非空数据库状态
EOF常量1字节标志着RDB文件正文内容结束
check_sum变量8字节校验和,根据前面4部分计算而来,用来检查RDB文件完整性

database部分

database部分保存了任意多个非空数据库状态,对于每一个非空数据库,database结构如下: rdb-database

数据部分数据类型长度数据含义
SELECT_DB常量1字节数据库开头标识
db_number变量1-5个字节数据库开头号码
key_value_pairs数据不定数据库所有键值对数据

key_value_pairs部分

key_value_pairs部分保存了一个数据所有的键值对数据,其中不带过期时间的键值对有TYPEkeyvalue三部分组成,带过期时间的话,会在前面多出EXPIRETIME_MSms两部分。 rdb-kv

数据部分数据类型长度数据含义
EXPIRETIME_MS常量1字节键值对过期时间标识
ms变量8字节键值对的过期时间(毫秒)
TYPE常量1字节数据库值的类型
key变量不定数据库键,永远是字符串对象
value变量不定数据库值,根据TYPE不用,value保存结构也不同

每个value都保存着一个值对象,每个值对象的类型由TYPE字段记录。根据TYPE类型不同,value部分的结构、长度也会有所不同。这块思想上跟内存中底层数据结构类似,这里就不展开细讲了。

至此,一个RDB文件完整的部分就出来了,如下所示: rdb-all

AOF持久化

除了RDB持久化之外,Redis还支持AOF持久化功能。AOF持久化是通过保存Redis服务器执行的写命令来记录数据库状态的aof

被写入AOF文件的所有命令都是以Redis的命令请求协议格式保存的,因为Redis的命令请求协议格式是纯文本格式。服务器在启动的时候,可以通过载入并重放AOF文件的命令来还原数据库状态。

AOF持久化的实现

AOF持久化实现可以分为以下三个步骤:

  1. 命令追加
  2. 文件写入
  3. 文件同步(刷盘)

命令追加

当启用AOF持久化的时候,服务器在执行完一个写命令之后,会将该命令追加到aof_buf缓存区的末尾。

struct redisServer {
    // ...

    // AOF缓冲区
    sds aof_buf;

    // ...
}

AOF文件的写入与同步

Redis服务器进程是一个事件循环,循环中的文件事件负责接收客户端的命令请求和发送命令回复,而时间事件则负责执行像serverCorn函数这样需要定时运行的函数。因为处理文件事件会包含写命令,使得一些内容追加到aof_buf缓冲区中,所以在事件循环结束前还需要调用flushAppendOnlyFile函数,决定是否需要将aof_buf缓冲区的内容写入并同步到AOF文件中。伪代码如下:

def eventLoop():
    while True:

        # 处理文件事件,接收命令请求和发送命令回复
        # 将写命令追加到aof_buf缓存区中
        processFileEvents()

        # 处理时间事件
        processTimeEvents()

        # 将aof_buf缓存区写入并同步在AOF文件中
        flushAppendOnlyFile()

flushAppendOnlyFile函数由配置项appendfsync决定:

appendfsync的值flushAppendOnlyFile函数行为
always每次都将aof_buf缓冲区数据写入并同步到AOF文件中
everysec(默认)每次都将aof_buf缓冲区数据写入AOF文件中,但是每隔1秒进行进行AOF文件同步
no每次都将aof_buf缓冲区数据写入AOF文件中,文件同步完全由操作系统控制

AOF文件载入与数据还原

因为AOF文件中包含了所有的写命令,所以在服务器启动的时候,只需要载入并重放AOF文件的命令就能够恢复到数据库原来的状态。具体步骤如下:

  1. 创建一个不带网络连接的伪客户端(fake client)。
  2. AOF文件中读出一条写命令。
  3. 使用伪客户端执行命令。
  4. 一直执行步骤二和三,直到AOF文件中的所有命令都执行完。

AOF重写

因为AOF持久化是通过追加命令的方式实现的,所以随着服务器运行,AOF文件体积也会越来越大。如果不加以控制,不仅会对整个Redis服务器造成不好的影响,而且还会导致AOF命令重放时间过长。为了解决AOF文件体积膨胀的问题,Redis支持了AOF文件重写功能。通过该功能,可以实现创建一个体积小的多的AOF文件来代替现有的AOF文件。

AOF重写的实现

虽然叫AOF重写,但实际上并不是对现有的AOF文件进行读取、分析和重新写入。实际上,AOF重写是通过读取服务器当前数据库状态来实现的首先从数据库中读取现有的键值,然后用一条命令去记录键值对,代替之前记录的针对该键的多条命令,这就是AOF重写的原理

AOF后台重写

AOF重写需要遍历整个数据库并把所有键值对都以命令的形式记录下来,很明显,这是一个非常耗费时间的事情。如果有服务器进程直接执行AOF重写,那么整个服务器将会被阻塞,这对于Redis来说显然是不能接受的。因此,Redis支持AOF后台重写功能,具体来讲就是由子进程处理AOF重写。

不过,AOF后台重写也有一个问题需要解决。因为在AOF后台重写期间,主进程仍然可以处理写命令,新的命令仍然会改变现有的数据库状态,最终就会导致AOF后台重写的文件保存的数据库状态和当前的数据库状态不一致。

为了解决上述的数据不一致的问题,Redis服务器设置了一个AOF重写缓存区。这个缓存区在创建子进程之后开始使用,此时Redis服务器执行完写命令之后,会同时将这个命令发送到AOF缓冲区和AOF重写缓存区。 aof-rewrite

这么做就能保证:

  1. AOF缓冲区的内容会正常写入和同步到现有AOF文件中,现有AOF文件依然可以正常处理。
  2. AOF后台重写期间,所有新加入的写命令都会保存在AOF重写缓存区中。再AOF后台重写完成之后,再将AOF重写缓存区的内容追加到新的AOF文件中即可,最后再用新的AOF文件替换之前的AOF文件。

原创不易,觉得文章写得不错的小伙伴,点个赞👍 鼓励一下吧~

欢迎关注我的开源项目:一款适用于SpringBoot的轻量级HTTP调用框架