redis 持久化

87 阅读7分钟

前言

复习redis持久化部分记录的文章。侵删

引子

reids因为其特点,被广泛应用在作为内存部分的存储响应的场景,但事情总有出错的时候,万一服务器宕机了,reids内存上的数据都没有了该怎么办?

reids的数据都是来自数据库中的,那么服务器宕机,redis重启恢复数据也要从数据库里读取才行。但这样就有了新的问题:

  1. 从数据库里读取数据要频繁访问数据库,造成数据库压力增大。
  2. 从数据库中读取数据所花费的时间对于内存来说实在是太慢了。这会使应用的反应时间增加。

从上面的问题看出来,靠数据库来恢复数据对redis和数据库双方都是一种折磨。那么,对 Redis 来说,实现数据的持久化,避免从后端数据库中进行恢复,是至关重要的。

redis的持久化主要有两大机制:AOF和RDB。

AOF

实现方式

试想一下,如果 Redis 每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里,然后重启 Redis 的时候,先去读取这个文件里的命令,并且执行它,这不就相当于恢复了缓存数据了吗?

这种保存写操作命令到日志的持久化方式,就是 Redis 里的 AOF(Append Only File)  持久化功能,注意只会记录写操作命令,读操作命令是不会被记录的,因为没意义。

所以AOF是执行一次写操作就写一条数据进AOF文件,那么执行操作和写记录哪个先开始呢?

AOF的执行顺序是先执行写操作,再执行写数据。也就是先写进内存再写进AOF文件。

这样做有两点好处:

  1. 避免检查开销,避免出现记录错误命令的情况:为了避免额外的检查开销,Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。而写后日志这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错。
  2. 不会阻塞当前写操作命令的执行:因为当写操作命令执行成功后,才会将命令记录到 AOF 日志。

但这种操作也有一些问题:

  1. 执行完了写操作后还没执行写入AOF文件服务器就宕机了,这样内存的数据没了,AOF文件也没有记录。
  2. AOF文件的写入数据可能会阻塞下一个执行命令。因为将命令写入到日志的这个操作也是在主进程完成的(执行命令也是在主进程),也就是说这两个操作是同步的。如果在将日志内容写入到硬盘时,服务器的硬盘的 I/O 压力太大,就会导致写硬盘的速度很慢,进而阻塞住了,也就会导致后续的命令无法执行。

上面两个问题仔细想就会发现都是和AOF文件写入磁盘的时机有关系。

写回策略

reids的写回策略有三种:

  • Always,这个单词的意思是「总是」,所以它的意思是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;
  • Everysec,这个单词的意思是「每秒」,所以它的意思是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;
  • No,意味着不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。

这三种方法各有优缺点,都不能完美解决「主进程阻塞」和「减少数据丢失」的问题。 大家根据自己的业务场景进行选择:

  • 如果要高性能,就选择 No 策略;
  • 如果要高可靠,就选择 Always 策略;
  • 如果允许数据丢失一点,但又想性能高,就选择 Everysec 策略。

选择好方案后AOF文件就会随着redis的运行越来越大,这样又引发了新的问题:一是,文件系统本身对文件大小有限制,无法保存过大的文件;二是,如果文件太大,之后再往里面追加命令记录的话,效率也会变低;三是,如果发生宕机,AOF 中记录的命令要一个个被重新执行,用于故障恢复,如果日志文件太大,整个恢复过程就会非常缓慢,这就会影响到 Redis 的正常使用。

AOF重写机制

Redis 为了避免 AOF 文件越写越大,提供了 AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。

AOF 重写机制是在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。

根据内存最新的数据来生成AOF中的数据记录,这样几条对同一数据的操作就会变成一条数据了。

为什么要新写一条AOF文件而不是直接在原来的修改呢?因为如果发生了意外的话这个AOF文件就没法用了,写新文件的话如果发生了意外还可以继续使用旧文件。

AOF的重写机制是在后台完成的,因为它写入的内容不多,所以一般不太影响命令的操作。

但是在触发 AOF 重写时,比如当 AOF 文件大于 64M 时,就会对 AOF 文件进行重写,这时是需要读取所有缓存的键值对数据,并为每个键值对生成一条命令,然后将其写入到新的 AOF 文件,重写完后,就把现在的 AOF 文件替换掉。

这个过程其实是很耗时的,所以重写的操作不能放在主进程里。

Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的。这样可以防止占用主线程。而且多进程工作可以使用写时复制技术不会像线程那样只能使用同一物理内存导致的问题也不会直接完全复制内存消耗资源。

在重写AOF文件的时候reids主进程也不是闲着的,它还会继续处理命令,这样数据就会不一致的。怎么办?

为了解决这种数据不一致问题,Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 重写AOF子自程之后开始使用。

在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」(AOF日志和AOF重写日志)。这样做是为了防止宕机后数据丢失(AOF缓冲区/AOF日志)也是为了重写的AOF日志/重写缓冲区能有最新的数据。

当子进程完成 AOF 重写工作(扫描数据库中所有数据,逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志)后,会向主进程发送一条信号,信号是进程间通讯的一种方式,且是异步的。

主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作:

  • 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致;
  • 新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。

信号函数执行完后,主进程就可以继续像往常一样处理命令了。