Redis的持久化
一、AOF(文件内容是操作命令)
1.概述
AOF 日志:每执行完一条写操作命令,就将该命令以追加的方式写入到 AOF 文件,然后在恢复时,以逐一执行命令的方式来进行数据恢复。
两个优点
- 不会阻塞当前写命令
- 避免多余的检查开销
写操作执行后再写入 AOF 日志,可以保证 AOF 中的命令都是可执行的正确的。若是先写入在执行,当有错误命令,Redis 在载入 AOF 日志的过程中会出错。
两个缺陷
- 可能会阻塞下一个写指令
写指令的执行和写入硬盘中 AOF 日志的过程都是在主进程中执行,当 I/O 压力过大时,AOF 日志的写入效率会降低,有可能会阻塞下一个写命令的执行。
- 可能有数据丢失风险
在 AOF 日志的写入还未成功之前,若服务器宕机,会导致数据的丢失。
2.AOF 日志的写入过程
- Redis 执行完写操作命令后,会将命令追加到 server.aof_buf 缓冲区;
- 然后通过 write() 系统调用,将 aof_buf 缓冲区的数据写入到 AOF 文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区 page cache,等待内核将数据写入硬盘;
- 具体内核缓冲区的数据什么时候写入到硬盘,由内核决定。
三种写回策略
Redis 提供了 3 种写回硬盘的策略,控制的就是上面说的第三步的过程。
三种写回策略:可靠性从高到低,性能上从低到高
- Always,每次执行完写操作命令就马上写回硬盘;
- Everysec,每次执行完写操作命令后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓存区里的内容写回到硬盘;
- No,每次执行完写操作命令后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。
3.AOF 的重写机制
随着写命令越来越多,AOF 日志文件也会越来越大,这样重启后载入 AOF 日志文件的时间也会变长,所以 Redis 提供了 AOF 重写机制。如果 AOF 日志文件的大小达到了设定的阈值,它会扫描数据所有的键值对,并为键值对的最新状态生成一条写操作命令,接着将命令写入到新的 AOF 文件中,重写完成后,替换当前现有的 AOF 文件。
为什么先写到新的 AOF 文件?
因为若重写过程中失败了会导致 AOF 文件被污染,当下次载入的时候会出错。所以 AOF 重写过程,先重写到新的 AOF 文件,重写失败的话,就直接删除这个文件就好,不会对现有的 AOF 文件造成影响。
AOF 后台重写
写入 AOF 日志的操作虽然是在主进程完成的,因为它写入的内容不多,所以一般不太影响命令的操作。
但是在触发 AOF 重写时,比如当 AOF 文件大于 64M 时,就会对 AOF 文件进行重写,这时是需要读取所有缓存的键值对数据,并为每个键值对生成一条命令,然后将其写入到新的 AOF 文件,重写完后,就把现在的 AOF 文件替换掉。 这个过程其实是很耗时的,所以重写的操作不能放在主进程里。
所以,Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的。
好处
- 子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程;
- 子进程带有主进程的数据副本(数据副本怎么产生的后面会说),这里使用子进程而不是线程,因为如果是使用线程,多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能。而使用子进程,创建子进程时,父子进程是共享内存数据的,不过这个共享的内存只能以只读的方式,而当父子进程任意一方修改了该共享内存,就会发生写时复制,于是父子进程就有了独立的数据副本,就不用加锁来保证数据安全。
4.AOF 存在问题(引出RDB)
用 AOF 日志的方式来恢复数据其实是很慢的,因为 Redis 执行命令由单线程负责的,而 AOF 日志恢复数据的方式是顺序执行日志里的每一条命令,如果 AOF 日志很大,这个「重放」的过程就会很慢了。
二、RDB(文件内容是二进制数据)
1.概述
RDB 文件的内容是二进制数据。RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。 因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。
2.如何使用
Redis提供了两个命令来生成 RDB 文件,区别在于是否在主进程中执行。
- save,由主线程来生成RDB文件,会阻塞主线程。
- bgsave,会创建一个子进程来生成RDB文件,这样可以避免主线程的阻塞。Redis 可以通过配置文件的选项来实现每隔一段时间自动执行一次 bgsave 命令。
3.RDB 存在问题
Redis 的快照是全量快照,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。所以可以认为,执行快照是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响。如果频率太低,服务器故障时,丢失的数据会更多。
通常可能设置至少 5 分钟才保存一次快照,这时如果 Redis 出现宕机等情况,则意味着最多可能丢失 5 分钟数据。 这就是 RDB 快照的缺点,在服务器发生故障时,丢失的数据会比 AOF 持久化的方式更多,因为 RDB 快照是全量快照的方式,因此执行的频率不能太频繁,否则会影响 Redis 性能,而 AOF 日志可以以秒级的方式记录操作命令,所以丢失的数据就相对更少。
三、AOF 进行重写的过程中和执行 RDB 快照时,主进程进行写操作怎么解决?(写时复制技术)
Redis 依然可以继续处理操作命令的,也就是数据是能被修改的。
那具体如何做到到呢?关键的技术就在于写时复制技术(Copy-On-Write, COW)。
1.写时复制技术
主进程在通过 fork 系统调用生成子进程时,操作系统会把主进程的「页表」复制一份给子进程,这个页表记录着虚拟地址和物理地址映射关系,而不会复制物理内存,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。
这样子进程就共享了父进程的物理内存数据了,这样能够节约物理内存资源,页表对应的页表项的属性会标记该物理内存的权限为只读。
当主线程对这些共享的内存数据都是只读操作时,主线程和子进程相互不影响。但是,如果主线程要修改共享数据里的某一块数据(比如键值对 A)时,就会发生写时复制,于是这块数据的物理内存就会被复制一份(键值对 A'),并重新设置其内存映射关系,然后主线程在这个数据副本(键值对 A')进行修改操作。与此同时,子进程可以继续把原来的数据(键值对 A)写入到 AOF/RDB 文件。注意这里只会复制主进程修改的物理内存数据,没修改物理内存还是与子进程共享的。
在发生更新/修改操作的时候,操作系统才会去复制物理内存,这样是为了防止 fork 创建子进程时的性能损耗,从而加快创建子进程的速度。因为物理内存数据的复制时间过长会导致主进程长时间阻塞的问题。毕竟创建子进程的过程中,是会阻塞主进程的。
当然,操作系统复制主进程页表的时候,主进程也是阻塞中的,不过页表的大小相比实际的物理内存小很多,所以通常复制页表的过程是比较快的。 不过,如果主进程的内存数据非常大,那自然页表也会很大,这时主进程在通过 fork 创建子进程的时候,阻塞的时间也越久。
有两个阶段会导致阻塞主进程:
- 创建子进程的途中,由于要复制主进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
- 创建完子进程后,如果子进程或者主进程修改了共享数据,就会发生写时复制,这期间会拷贝物理内存,如果内存越大,阻塞的时间也越长;
2.AOF 重写如何处理被修改的数据
为了解决数据不一致问题,Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。
在 bgrewriteaof 子进程执行 AOF 重写期间,主进程需要执行以下三个工作:
- 执行客户端发来的命令;
- 将执行后的写命令追加到 「AOF 缓冲区」;
- 将执行后的写命令追加到 「AOF 重写缓冲区」;
当子进程完成 AOF 重写工作(扫描数据库中所有数据,逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志)后,会向主进程发送一条信号,信号是进程间通讯的一种方式,且是异步的。
主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作:
- 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据状态一致;
- 新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。
信号函数执行完后,主进程就可以继续像往常一样处理命令了。
在整个 AOF 后台重写过程中,除了发生写时复制会对主进程造成阻塞,还有信号处理函数执行时也会对主进程造成阻塞,在其他时候,AOF 后台重写都不会阻塞主进程。
3.RDB 快照如何处理被修改的数据
bgsave 快照过程中,如果主线程修改了共享数据,发生了写时复制后,RDB 快照保存的是原本的内存数据,而主线程刚修改的数据,是没办法在这一时间写入 RDB 文件的,只能交由下一次的 bgsave 快照。因为此时主线程的内存数据和子进程的内存数据已经分离了,子进程写入到 RDB 文件的内存数据只能是原本的内存数据。
如果系统恰好在 RDB 快照文件创建完毕后崩溃了,那么 Redis 将会丢失主线程在快照期间修改的数据。
极端情况
在 Redis 执行 RDB 持久化期间,刚 fork 时,主进程和子进程共享同一物理内存,但是途中主进程处理了写操作,修改了共享内存,于是当前被修改的数据的物理内存就会被复制一份。
那么极端情况下,如果所有的共享内存都被修改,则此时的内存占用是原先的 2 倍。
所以,针对写操作多的场景,我们要留意下快照过程中内存的变化,防止内存被占满了。
AOF 和 RDB 合体
尽管 RDB 比 AOF 的数据恢复速度快,但是快照的频率不好把握:
- 如果频率太低,两次快照间一旦服务器发生宕机,就可能会比较多的数据丢失;
- 如果频率太高,频繁写入磁盘和创建子进程会带来额外的性能开销。
开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。
加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失。