Redis设计与实现 - RDB和AOF持久化

Redis 为什么需要持久化?
我们都知道 Redis 是一个内存数据库,它将自己的数据库状态存放在内存中,所以如果不想办法将存储在内存中的数据库状态保存到磁盘中,一旦服务器的进程退出,服务器中的数据库状态也就随之丢失,因此持久化数据是 Redis 必须要做的一件事。

RDB 持久化

RDB 持久化是通过将某一个时间节点的数据库状态保存到一个 RDB 文件(经过压缩的二进制文件)中,它可以手动执行也可以根据服务器的配置选项定期执行。

创建和载入RDB文件

在 Redis 命令中,通过 SAVE 和 BGSAVE 命令调用 rdbSave() 函数实现生成 RDB 文件。
SAVE 命令会阻塞 Redis 的服务器进程直到 RDB 文件创建完成,在服务器进程阻塞的时间内服务器是处理不了任何的命令请求。
BGSAVE 则是创建一个子进程,由子进程来创建 RDB 文件,服务器进程仍可以继续处理命令请求。但是不包括 SAVE、BGSAVE、BGREWRITEAOF 三个命令。原因如下:

  1. 禁止调用 SAVE 命令,是防止父进程和子进程同时执行两个 rdbSave() 函数产生竞争条件。
  2. 禁止调用 BGSAVE 命令,也是和上面一样,防止执行两个 BGSAVE 命令产生竞争。
  3. 禁止调用 BGREWRITEAOF 命令,是 BGREWRITEAOF 和 BGSAVE 命令不能同时执行,如果一个进程正在执行,另一个命令就会延迟
    到该进程命令执行完成再继续执行。这两个命令实质上都是由子进程调用,在操作方面没有什么冲突的地方,但是两个进程都是大量执行磁盘的写入操作,如果两个一起执行对性能会产生影响。
def SAVE():
    // 创建RDB文件
    rdbSave();
def BGSAVE():
    // 创建子进程
    pid = fork();
    // 子进程负责创建文件,并在完成后向父进程发送信号
    if pid == 0:
        rdbSave();
        signal_parent();
    // 父进程基础处理命令请求,通过轮询等待子进程的信号
    else if pid > 0:
        handle_request_and_wait_signal();

而对于载入 RDB 文件呢,它是在服务器启动的时候自动执行的,所以没有专用的载入命令。
并且这块有个需要注意的是因为 AOF 文件更新频率要比 RDB 高,所以如果服务器开启了 AOF 持久化功能它会优先通过使用 AOF 文件来还原数据库状态。

自动间隔时间保存

我们可以通过进行服务器的配置信息进行定期执行,用过通过 save 选项设置多个保存条件,只要其中一个条件被满足服务器就会执行 BGSAVE 命令,例如提供以下配置

save 900 1
save 300 10
save 60 100000

服务器如果满足以下三个条件之一就会执行 BGSAVE 命令:

  • 服务器在 900s 之内对数据库进行了至少 1 次修改
  • 服务器在 300s 之内对数据库进行了至少 10 次修改
  • 服务器在 60s 之内对数据库进行了至少 100000 次修改

实现原理

在 redisServer 结构中有一个 saveparams 属性,服务器会根据 save 选项设置的条件来设置该属性。saveparams 它是一个数组,每个数据都是 saveparam 结构,保存着 save 选项设置的保存条件。
并且 redisServer 结构中有 dirty、lastsave 属性,前一个属性记录着距离上一次成功执行 BGSAVE 命令后服务器进行了多少次修改;后一个属性记录着上一次成功执行 BGSAVE 命令或者 SAVE 命令的时间。

struct redisServer {
    ...
    struct saveparam * saveparams;
    long long dirty;
    time_t lastsave;
    ...
};
struct saveparam {
    // 时间,以秒为单位
    time_t seconds;
    // 修改数
    int changes;
}

然后 Redis 服务器周期性操作函数 serverCron() 函数默认每 100 毫秒执行一次,这里面就包括减产 save 选项设置的保存条件有没有被满足,其实也就是遍历一遍 saveparams 数组,根据上面的属性值进行判断。
RDB文件结构
一个完整的 RDB 文件包含以下几个部分:
在这里插入图片描述

  • REDIS 常量,因为 RDB 文件是二进制数据,所以这个常量不是带 ‘\0’ 的字符串,而是五个字符。
  • db_version:长度为 4 个字节,是一个字符串表示的整数,记录着这个文件的版本号。
  • databases:包含着零个或者多个数据库以及各个数据库中的键值对数据。
  • EOF 常量:标识这 RDB 文件正文内容的结束。
  • check_num:8 字节的无符号整数,保存着检验和,保证 RDB 文件没有损坏。

databases部分

该部分可以保存任意多个非空的数据库。每个数据库都分为 SELECTDB、db_number、key_value_pairs 三个部分:

  • SELECTDB 常量:读入程序读到了这里就知道接下来要读入的是一个数据库号码。
  • db_number:保存着一个数据库号码,长度根据号码的大小不同可以是 1 字节、2 字节或者 5 字节,读入程序读到这就会调用 SELECT 命令进行数据库切换。
  • key_value_pairs:保存着数据库所有的键值对,如果有过期的时间过期时间也会一起保存下来。
    细致的看一个 key_value_pairs 部分:
    它分为不带过期时间和带过期时间两种表示。
    不带过期时间由 TYPE、key、value 三部分组成。带过期时间则是在这三部分前面加了 EXPIRETIME_MS、ms 两部分共五分部组成。
  • EXPIRETIME_MS 常量:1 字节,读入程序读到了这里就知道接下来要读入的是一个以毫秒为单位的过期时间。
  • ms:8 字节的带符号整数,记录着键值对的过期时间,是一个以毫秒为单位的 UNIX 时间戳。
  • TYPE 常量:也就是 value 值的类型长度为 1 字节。
  • key:总是一个字符串对象,键值对的键对象。
  • value:键值对的值对象。
    其中 value 对象根据类型的不同,value 部分的结构长度也会有所不同,底层不得编码方式啊,对象值会不会进行压缩都是在这里进行考虑的,细致的可以去看一下书就可以了解到了。

AOF持久化

RDB 持久化是通过保存数据库中的键值对来记录数据库状态的不同,而 AOF 则是通过保存 Redis 服务器执行过的写命令来记录数据库状态的。注意因为写入 AOF 的命令都是以 Redis 命令请求协议格式保存的所以都是纯文本格式。

AOF持久化实现

AOF 持久化实现分为命令追加、文件写入、文件同步三个步骤。

命令追加

当 AOF 持久化功能打开的时候,服务器执行完一个写命令的时候,就会以协议格式将这个命令追加到服务器状态的 aof_buf 缓冲区末尾:

struct redisServer{
    ...
    sds aof_buf;
    ...
};

文件写入和同步

Redis 的服务器进程就是一个事件循环,这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像 serverCron 函数这样需要定时运行的函数。
因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到 aof_buf 缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用 flushAppend0nlyFile 函数,考虑是否需要将 aof_ buf 缓冲区中的内容写入和保存到 AOF 文件里面。而 flushAppend0nlyFile 函数的行为是根据服务器配置的 appendfsync 的值来判断的。

appendfsync 的值flushAppend0nlyFile 函数行为
always将 aof_buf 中的内容写入并同步到 AOF 文件
everysec将 aof_buf 中的内容写入到 AOF 文件,但是同步操作是将上次同步 AOF 文件的时间距离现在超过了 1 秒钟才执行
no将 aof_buf 中的内容写入到 AOF 文件,但是不进行同步,何时同步由操作系统决定

这个值默认是 everysec。

AOF文件的载入

流程如下图:
在这里插入图片描述
因为 Redis 的命令只能在客户端上文中执行,载入 AOF 文件使用的命令是直接来源于文件而不是网络,所以要生成一个没有网络连接的伪客户端执行文件中的命令。

AOF文件的重写

AOF 文件是通过保存执行的写命令记录数据库状态,所以随着服务器运行,AOF 文件就可能会变得很大,所以提供了重写的功能。
该功能不会分析现有的 AOF 文件,而是通过读取服务器当前的数据库状态来实现。也就是读取数据库中现在的值然后用一条命令(是不是一条也要看键值对的数量)来记录键值对,代替之前记录这写键值对的多条命令。
在 Redis 中是通过子进程来实现重写的,因为这个函数会进行大量的写入操作,如果主进程运行会被长时间阻塞,无法继续处理命令请求。
当然这样也就带来了一个问题在子进程进行 AOF 重写的过程中服务器继续处理命令请求,可能会对现在的数据库状态进行了修改,从而使得当前的数据库状态和重写后的 AOF 文件保存的数据库状态不一样。为了解决这个问题,Redis 设置了一个 AOF 重写缓存区,这个缓存区只有在服务器创建子进程之后才开始使用,当 Redis 执行完一个写命令之后就会同时将这个写命令发到 AOF 缓存区和 AOF 重写缓存区。
在这里插入图片描述
在子进程完成 AOF 重写工作之后,会发信号给父进程,父进程会调用一个信号处理器,执行以下两个事情:

  1. 将 AOF 重写缓存区内容写到新 AOF 文件中,保证保存的数据库状态和服务器当前状态一样。
  2. 对新的 AOF 文件进行改名,原子的覆盖现有的 AOF 文件,完成替换。

RDB持久化和AOF持久化的区别

RDB 的优点

  • RDB 是一个非常紧凑的文件,它保存了 Redis 在某个时间点上的数据集。 这种文件非常适合用于进行备份: 比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB 文件。 这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。
  • RDB 非常适用于灾难恢复:它只有一个文件,并且内容都非常紧凑。
  • RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。
  • RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

RDB 的缺点

  • 如果你需要尽量避免在服务器故障时丢失数据,那么 RDB 不适合你。 虽然 Redis 允许你设置不同的保存点来控制保存 RDB 文件的频率, 但是, 因为RDB 文件需要保存整个数据集的状态, 所以它并不是一个轻松的操作。 因此你可能会至少 5 分钟才保存一次 RDB 文件。 在这种情况下, 一旦发生故障停机, 你就可能会丢失好几分钟的数据。
  • 每次保存 RDB 的时候,Redis 都要 fork() 出一个子进程,并由子进程来进行实际的持久化工作。 在数据集比较庞大时, fork() 可能会非常耗时,造成服务器在某某毫秒内停止处理客户端; 如果数据集非常巨大,并且 CPU 时间非常紧张的话,那么这种停止时间甚至可能会长达整整一秒。 虽然 AOF 重写也需要进行 fork() ,但无论 AOF 重写的执行间隔有多长,数据的耐久性都不会有任何损失。

AOF 的优点

  • 使用 AOF 持久化会让 Redis 变得非常耐久:你可以设置不同的 fsync 策略,比如无 fsync ,每秒钟一次 fsync ,或者每次执行写入命令时 fsync 。 AOF 的默认策略为每秒钟 fsync 一次,在这种配置下,Redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据( fsync 会在后台线程执行,所以主线程可以继续努力地处理命令请求)。
  • AOF 文件是一个只进行追加操作的日志文件, 因此对 AOF 文件的写入不需要进行 seek , 即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机,等等), redis-check-aof 工具也可以轻易地修复这种问题。
  • Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
  • AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析也很轻松。 导出 AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。

AOF 的缺点

  • 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
  • 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间。
  • AOF 在过去曾经发生过这样的 bug : 因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集恢复成保存时的原样。 (举个例子,阻塞命令 BRPOPLPUSH 就曾经引起过这样的 bug 。) 测试套件里为这种情况添加了测试: 它们会自动生成随机的、复杂的数据集, 并通过重新载入这些数据来确保一切正常。 虽然这种 bug 在 AOF 文件中并不常见, 但是对比来说, RDB 几乎是不可能出现这种 bug 的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值