跳过正文
  1. 面试题库/

04|Redis 持久化

·6823 字·14 分钟
目录
Redis面试题库 - 这篇文章属于一个选集。
§ 4: 本文

1. RDB 和 AOF 本质区别是什么?
#

分析

如果是讲区别可以从 文件类型文件恢复速度安全性 来进行回答,但本质区别就是 RDB 是使用快照进行持久化,AOF 是日志。

其他可以列举的区别点,都是能从 快照 与 日志 中的对比找出的:

  • 文件类型:RDB 生成的是 二进制文件(快照),AOF 生成的是 文本文件(追加日志);
  • 安全性:缓存宕机时,RDB 容易丢失较多的数据,AOF 根据策略决定(默认的 everysec 可以保证最多有一秒的丢失);
  • 文件恢复速度:由于 RDB 是二进制文件,所以恢复速度也比 AOF 更快;
  • 操作的开销:每一次 RDB 保存都是一次全量保存,操作比较重,通常设置至少 5 分钟保存一次数据。而 AOF 的刷盘是一次追加操作,操作比较轻,通常设置策略为每一秒进行一次刷盘。

回答

本质区别就是 RDB 是保存快照进行持久化,而 AOF 则是追加日志文件进行持久化。因为这个本质区别,所以还会有文件恢复速度、安全性、操作开销等区别,需要展开讲吗?(这里其实摸不准面试官想法,所以可以询问一下)

2. 如果 RDB 和 AOF 只能选一种,你选哪个?
#

分析

两个方向分析:

  1. 性能和可靠之间做一个选择;
  2. 分析 Redis 为什么默认打开的是 RDB,以及在只开一个情况,实际是更推荐 RDB 的。

回答

如果从业务需要来看,如果我们能接受分钟级别的数据丢失,可以考虑选择 RDB,如果需要尽量保证数据安全,可以考虑混合持久化,如果只用 AOF,那么优先选择 everysec 策略进行刷盘(在可靠和性能之间有一个平衡)。

从持久化理念来看,始终开启快照是一个推荐的方式,这也是 Redis 官方为什么默认开启 RDB,而不开启 AOF,同时官网也明确不推荐只开 AOF。

3. 介绍一下 AOF 的三种写回策略?
#

分析

当我们设置的 appendfsync 策略不同,刷盘的情况也是不同的。

always

Redis 在每个事件循环都会将 aof_buf 缓冲区里的所有内容通过 write 写入 AOF 文件描述符,并且调用 fsync 同步它(是在主线程中进行同步刷盘的),所以 always 的效率也是最慢的(主线程直接跟磁盘 IO 打交道,单线程的 redis 会被阻塞,有个慢速的落盘操作),但是从安全性角度来讲它也是最安全的。因为即使出现故障停机,AOF 持久化也只会丢失一个事件循环中所产生的命令数据(可以很多也可以很少,always 将变更都写入文件和同步,才能回复客户端响应成功)。

everysec

服务器在每个事件循环都要将 aof_buf 缓冲区的所有内容通过 write 写入到 AOF 文件描述符,并且每隔超过一秒就要在子线程中对 AOF 文件进行一次 fsync 同步(注意是在子线程中异步刷盘的,如果主线程发现子线程还在 fsync,会推迟本秒的 fsync)。从效率上来讲,everysec 足够快(后台线程进行 fsync,主线程只进行 write),并且就算出现故障停机,也只会丢失一秒钟的数据(实际上看源码是最多会丢失两秒多的数据)。everysec 是一种折中的取舍,也是默认值,也是推荐值。

no

no 策略下 redis 不会进行任何主动的 fsync aof 刷盘操作,所以它的效率对于 redis 来说也是最高的,不过因为把同步权交给操作系统,所以 no 模式下的单次同步时长也是最长的,它会在系统缓冲区中积累一段时间的写入数据,然后才会真的被「落盘」。从平摊操作的角度来讲,no 跟 everysec 的效率应该都是差不多的,但是从安全性来讲,如果出现故障停机,使用 no 会丢失上次同步 AOF 文件之后的所有命令数据。通常情况下如果使用 no 策略 Linux 会每隔 30s 刷盘,不过这取决于内核的具体配置。

回答

Always、Everysec 和 No,这三种策略在可靠性上是从高到低,而在性能上从低到高。

Always 是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;Everysec 每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;No 就是不控制写回硬盘的时机。每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。

4. 为什么先执行 Redis 命令,再把数据写入 AOF 日志呢?
#

分析

好处:

  • 保证正确写入:如果当前的命令语法有问题,错误的命令记录到 AOF 日志里后可能还会进行语法检查。先执行 Redis 命令,再把数据写入 AOF 日志可以保证写入的都是正确可执行的命令;
  • 不阻塞当前写操作:因为当写操作命令执行成功后才会将命令记录到 AOF 日志,避免写入阻塞。

缺陷:

  • 数据可能会丢失:执行写操作命令和记录日志是两个过程,Redis 还没来得及将命令写入到硬盘时发生宕机,数据会有丢失的风险;
  • 阻塞其他操作:不会阻塞当前命令的执行,但因为 AOF 日志也是在主线程中执行,所以当 Redis 把日志文件写入磁盘的时候,还是会阻塞后续的操作无法执行。

针对这个问题,我们主要强调好处。

回答

有 2 点好处。

  1. 保证正确写入:如果当前的命令语法有问题,错误的命令记录到 AOF 日志里后可能还会进行语法检查。先执行 Redis 命令,再把数据写入 AOF 日志可以保证写入的都是正确可执行的命令;
  2. 不阻塞当前写操作:因为当写操作命令执行成功后才会将命令记录到 AOF 日志,避免写入阻塞。

5. AOF 子进程的内存数据跟主进程的内存数据不一致怎么办?
#

分析

Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的,这有两个好处:

  • 子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程;
  • 子进程带有主进程的数据副本,使用子进程而不是线程,因为如果是使用线程,多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能。创建子进程时,父子进程是共享内存数据的,不过这个共享的内存只能以只读的方式,而当父子进程任意一方修改了该共享内存,会发生写时复制,于是父子进程就有了独立的数据副本,不用加锁来保证数据安全。

AOF 子进程产生的时刻,数据和主进程是一致的,这里面试官是想问重写过程中主进程数据增加了,如何保持最后结果一致,回答的关键在 AOF 重写缓冲区。

回答

Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 AOF 缓冲区和 AOF 重写缓冲区。当子进程完成 AOF 重写工作后,会向主进程发送一条信号。主进程收到该信号后,会调用一个信号处理函数,将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致;新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。

6. RDB 在执行快照的时候,数据能修改吗?
#

分析

推荐学习官网对 RDB 行为的说明:

How it works
Whenever Redis needs to dump the dataset to disk, this is what happens:
1. Redis forks. We now have a child and a parent process.
2. The child starts to write the dataset to a temporary RDB file.
3. When the child is done writing the new RDB file, it replaces the old one.
This method allows Redis to benefit from copy-on-write semantics.

注意最后一句话:

This method allows Redis to benefit from copy-on-write semantics.

就是说这种方式让 Redis 从写时复制技术受益,Redis 官方文档基本没废话,这句话看似无关轻重,实际上说明了:执行 RDB 持久化过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的,这就是通过写时复制技术实现的。

回答

可以。执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,数据是能被修改的,采用的是写时复制技术(Copy-On-Write, COW)。执行 bgsave 命令的时候,会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个,由于共享父进程的所有数据,可以直接读取主线程里的内存数据,并将数据写入到 RDB 文件。此时如果主线程执行读操作,则主线程和 bgsave 子进程互相不影响。如果主线程要修改共享数据里的某一块数据,就会发生写时复制,数据的物理内存就会被复制一份,主线程在这个数据副本进行修改操作。与此同时,子进程可以继续把原来的数据写入到 RDB 文件。

7. Redis 用 RDB 持久化时对过期键会如何处理的?
#

分析

在 Redis 使用 RDB(Redis Database Backup)持久化时,过期键的处理方式取决于 RDB 生成和恢复的阶段,具体情况如下:

  • Redis 在生成 RDB 快照时,不会直接删除过期的键,而是检查每个 key 的 TTL,如果某个 key 已经过期,则不会写入 RDB 文件;如果 key 未过期,则会连同其 TTL 一起写入 RDB。这样,生成的 RDB 文件中不会包含已过期的键,避免存储无用数据;
  • 当 Redis 重启并加载 RDB 文件时:Redis 会正常加载所有 key 及其 TTL,但不会立即清理所有过期 key,而是由数据清理机制来保证(在客户端访问 key 时,Redis 发现 key 已过期,则立即删除。Redis 运行时的定期清理机制可能也会被触发,主动删除过期 key)。

回答

RDB 分为生成阶段和加载阶段,生成阶段会对 key 进行过期检查,过期的 key 不会保存到 RDB 文件中;加载阶段在载入 RDB 文件时,Redis 会正常加载所有 key 及其 TTL,而过期 key 的删除,是由专门的数据清理机制来保证,和 RDB 无关。

8. Redis 用 AOF 持久化时对过期键会如何处理的?
#

分析

总结为下表:

阶段AOF 处理方式
AOF 追加日志记录 EXPIRE 命令,过期后不自动删除,只有触发惰性删除或定期清理时才写入 DEL
AOF 重写过滤掉已过期的 key,生成更精简的 AOF 文件
AOF 恢复加载 AOF 时仍然恢复所有 key,过期 key 需要等待惰性删除或定期清理

回答

恢复时会恢复所有过期 key,等待惰性删除或定期清理,写入时会记录 EXPIRE 命令,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值。重写阶段会对 Redis 中的键值对进行检查,已过期的键不会被保存到重写后的 AOF 文件中。

9. AOF 模式下,Redis 主从模式中,对过期键会如何处理?
#

分析

主库会记录一条 del 指令到 AOF 文件,从库也会同步这条指令。

回答

从库不会进行过期扫描,从库的过期键处理依靠主服务器控制,主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。

如果主从同步发生意外,原本主库的 key 过期了,但是 del 指令没有同步给从库成功,导致从库内存中存在已经过期但没有删除的 key,这时候有客户端访问从库时,即使 key 还是内存的,但是从库发现 key 是过期的,就不会返回 key 的数据给客户端了。

10. RDB 持久化的触发时机?(简单了解)
#

分析

从源码来看,RDB 持久化函数整体上在这几个地方会被使用:

  • Redis Shutdown(Redis 关闭之前进行一次持久化);
  • 客户端发送 save 命令;
  • 客户端发送 bgsave 命令;
  • 每一次事件循环 ServerCron 检查是否需要 bgsave(也就是判断我们的 RDB 配置);
  • 主从全量复制发送 RDB 文件(也进行一次 RDB 持久化);
  • 客户端执行数据库清空命令 FLUSHALL。

虽然 RDB 的触发条件有很多,但实际执行过程很简单,而区别点在于子进程来执行还是主进程执行这个流程:

  • 打开一个临时的 RDB 文件(c 语言库函数 fopen);
  • 将执行命令这一时刻数据库数据写入到 IO 缓冲区(c 语言库函数 fwrite);
  • 会将这一时刻的数据按照 RDB 对应的版本格式进行写入;
  • 执行 fflush(将 IO 缓冲区里的数据刷新到内核缓冲区);
  • 执行 fsync(可以将内核缓冲区里的数据刷到磁盘);
  • 执行 fclose(关闭这个临时文件);
  • 修改临时文件名字,并让咱们的后台线程 BIO_LAZY_FREE 去删除旧的 RDB(到此,一次 RDB 过程结束)。

简单来说 RDB 的流程就是触发 RDB 持久化时,让主进程或子进程(区分条件)来将这一时刻的数据库数据写到一个新的 RDB 文件中。

回答

主要有这么几个地方,一个是调用 save 或者 bgsave 命令,一个是根据我们配置周期进行,一个是 Redis 关闭之前,这三个是比较常见的,其它边缘一点的还有主从全量复制发送 RDB 文件等。(如果追问可以说还有客户端执行数据库清空命令 FLUSHALL)

11. AOF 刷盘的触发时机?(几乎不考)
#

分析

首先一定要弄清楚 AOF 的流程是怎样的:

  • 整个 AOF 的流程可以分为三个过程:命令追加(append 到 aof_buf)、文件写入(write 进内核缓冲区)、文件同步(fsync 让内核缓冲区数据进入磁盘文件)。

从源码来看,AOF 刷盘函数在三个地方会被触发:

  • Redis Shutdown 的时候;
  • 每一次事件循环钩子函数 beforeSleep()
  • 每一次事件循环的时间事件对应的 handler——servercron()
  • 通过配置指令关闭 AOF 功能时。

而当我们设置的 appendfsync 策略不同,刷盘的情况也是不同的(同第 3 题)。

回答

AOF 触发流程主要有 3 个,一个是 Redis 关闭的时候,另一个是每一次事件循环钩子函数 beforeSleep(),最后一个是每一次事件循环函数 servercron() 里面(这个问题比较复杂,如果追问需要根据分析理清楚 AOF 流程是怎样的,不同 appendfsync 策略是如何执行的,酌情回答)。

12. RDB 对主流程有什么影响?(几乎不考)
#

分析

主要从 RDB 的整个流程来寻找一些明显或潜在的风险。

回答

  • 当执行阻塞式持久化的时候,由主进程进行 RDB 快照保存,会阻塞主进程;
  • 当执行后台持久化时,由 fork 出的子进程来进行 RDB 快照保存:
    • 如果数据量比较大的时候,会导致 fork 子进程这个操作比较耗时,从而阻塞主进程;
    • 由于采用了写时复制技术,如果在进行 RDB 快照保存的时候,有大量的写入操作执行,会导致主进程多拷贝一份数据,消耗大量额外的内存。

13. AOF 对主流程有什么影响?(几乎不考)
#

分析

  • 明显的影响:使用 AOF 持久化,如果我们选择 always 的策略,每当 Redis 命令执行之后,需要由主进程进行 write + fsync 的操作,如果数据过大的话,主进程就会花费较大的时间用于写 AOF 日志,对后续请求影响较大(阻塞直到写 AOF 完成,才能响应客户端执行成功);
  • 潜在的影响:
    • 如果我们选择 everysecond 策略,虽然 fsync 是交给后台线程 BIO_AOF_FSYNC 来完成,但是主进程还需要进行 write 操作,如果后台线程上一轮 fsync 没有完成,那么主进程进 write 的时候仍然会阻塞(因为 write 作用在一个正在 fsync 的 fd 上,会阻塞);
    • AOF 重写是由 fork 出的子进程进行的,类似于上面提到的风险,fork 子进程这个操作有可能阻塞主进程。

回答

  • 当 appendfsync 使用 always,如果 AOF 写入日志压力过大会导致主进程处理其他请求很慢;
  • 当 appendfsync 使用 everysec,如果后台线程上一轮的 fsync 没有完成,会导致我们本轮主线程执行 write 被阻塞(直到 fsync 完成);
  • 当 AOF 重写发生时,如果数据量比较大,会导致 fork 子进程这个操作比较耗时,从而阻塞主进程。

14. AOF 混合持久化方案是什么?
#

分析

需要记住的是,AOF 混合持久化,就是在 AOF 重写的基础上做了一些改动。

回答

  • AOF 混合持久化会使用 RDB 持久化函数将内存数据写入到新的 AOF 文件中(数据格式也是 RDB 格式);
  • 而重写期间新的写入命令追加到新的 AOF 文件仍然是 AOF 格式;
  • 此时新的 AOF 文件就是由 RDB 格式和 AOF 格式组成的日志文件。

15. 简单描述 AOF 重写流程
#

分析

AOF 重写就三个关键点:

a. 子进程读取 Redis DB 中的数据以字符串命令的格式(也可以看作 AOF 文件格式)写入到新 AOF 文件中; b. 如果有新数据,由主进程将数据写入到 aof 重写缓冲区(aof_rewrite_buf); c. 当子进程完成重写操作后,主进程通过管道将 aof 重写缓冲区中的数据传输给子进程,然后子进程追加到新 aof 文件中。

回答

当 aof 重写触发那一刻,主进程就会 fork 出一个子进程,然后这个子进程读取 Redis DB 中的数据,以字符串命令的格式写入到新 AOF 文件中。

如果这个时候 Redis 接收到了新的写入命令,那么主进程会将这些“增量数据”写入到 AOF 重写缓冲区中。

在子进程将数据都写入到新 AOF 文件后,主进程会通过管道将 AOF 重写缓冲区里面的数据发送给子进程,子进程再将这一份数据追加到新 AOF 文件中,保证新 AOF 文件的完整性。

16. AOF 重写你觉得有什么不足之处么?
#

分析

  • 重写期间新的写命令,会将数据写入到两处地方(AOF 缓冲和 AOF 重写缓冲)中,这是额外的 CPU 和内存开销;
  • 重写时会 AOF 缓冲和 AOF 重写缓冲分别写入到旧日志和新日志中,这是额外的磁盘开销;
  • 除了清楚 AOF 的不足之处,如果还知道改进方案,那么会令面试官刮目相看,Redis 7.0 就对此做了新的改进。

回答

我认为主要有 3 点不足之处:

  1. 额外的 CPU 开销: a. 在重写时,主进程需要将新的写入数据写入到 AOF 重写缓冲(aof_rewrite_buf); b. 主进程需要通过管道向子进程发送 AOF 重写缓冲的数据; c. 子进程还需要将这些数据写入到新的 AOF 日志中;
  2. 额外的内存开销:在重写时,AOF 缓冲和 AOF 重写缓冲中的数据都是一样的(浪费了一份);
  3. 额外的磁盘开销:在重写时,AOF 缓冲需要刷入旧的 AOF 日志,AOF 重写缓冲也需要刷入到新的 AOF 日志,导致在重写时磁盘多占一份数据。

Redis 在 7.0 版本也做了对应的优化,我可以讲一下吗?(如果可以跳到下道题)

17. 针对 AOF 重写的不足,你有什么优化思路呢?
#

分析

改进之处:

  • 在 Redis 7.0 版本,对 AOF 重写作出了优化,提出了 MP-AOF 方案,原来的 AOF 重写缓冲被移除,AOF 日志也分成了 Base AOF 日志、Incr AOF 日志:

    MP-AOF: Multi Part AOF = one BASE AOF + many INCR AOFs
    
  • Base AOF 日志记录重写之前的命令;Incr AOF 日志记录重写时新的写入命令(正常 AOF 刷盘的时候写的是 Incr AOF);

  • 当重写发生时,主进程 fork 出一个子进程,对 Base AOF 日志进行重写(将当前内存数据写入到新的 Base AOF 日志);如果此时有新的写入命令,会由主进程写入到 aof_buf,再将缓冲数据刷入新的 Incr AOF 日志。这样新的 Incr AOF 日志 + 新的 Base AOF 日志就构成了完整的新的 AOF 日志;

  • 子进程重写结束时,主进程会负责更新 manifest 文件,将新生成的 BASE AOF 和 INCR AOF 信息加进清单,并将之前的 BASE AOF 和 INCR AOF 标记为 HISTORY(manifest 用于追踪管理 AOF 文件);

  • 这些 HISTORY 文件默认会被 Redis 异步删除(unlink),一旦 manifest 文件更新完成,就代表着整个 AOFRW 流程结束。

回答

其实在 Redis 7.0 版本,就使用 MP-AOF 方案对 AOF 重写做了优化,核心其实就是去掉原来的重写缓冲,同时将 AOF 日志拆分为 Base AOF 日志、Incr AOF 日志,由 manifest 来管理。重写时,还是开一个子进程,对 Base AOF 日志进行重写,但是新命令会往新的 Incr AOF 日志写,Incr AOF 日志 + 新的 Base AOF 日志就构成了完整的新的 AOF 日志。

Redis面试题库 - 这篇文章属于一个选集。
§ 4: 本文