redis的持久化RDB与AOF详解

Redis的持久化包括RDB快照和AOF日志两种方式,RDB在指定时间间隔做内存数据的全量快照,AOF记录所有写操作日志。AOF通过写后日志保证实时性,但可能丢失最近命令。AOF重写用于压缩文件大小,通过子进程完成,避免阻塞主进程。混合持久化结合RDB和AOF优点,先写RDB快照再追加AOF增量数据。
摘要由CSDN通过智能技术生成

什么是Redis持久化

Redis读写速度快、性能优越是因为它将所有数据存在了内存中,然而,当Redis进程退出或重启后,所有数据就会丢失。所以我们希望Redis能保存数据到硬盘中,在Redis服务重启之后,原来的数据能够恢复,这个过程就叫持久化。

Redis持久化的方式

Redis持久化的方式有两种:

RDB持久化:在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里

AOF持久化:以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。实时性更好,丢失的数据更少

AOF持久化

AOF配置控制

在redis中AOF 持久化功能默认是不开启的,需要我们修改 redis.conf 配置文件中的以下参数:

appendonly yes    #表示是否开启AOF持久化(默认是NO,关闭)

# The name of the append only file (default: "appendonly.aof")
appendfilename appendonly-63790.aof   #AOF持久化文件的名字

# If unsure, use "everysec".
appendfsync everysec    #AOF持久化写回策略

AOF日志

AOF持久化是通过AOF日志来实现的,而AOF日志是写后日志,即redis先执行命令,然后将数据写入内存中,最后才写入AOF日志中,如下图:

Redis AOF 操作过程

但是,要注意的是,AOF日志只记录写操作命令,不记录读操作命令,因为记录读操作没有任何意义。

AOF日志记录的是每一条写操作的命令,这些命令都是以文本的形式保存下来的,我们可以通过cat命令查看AOF日志里面的内容或者直接打开AOF日志文件也可以看到。那么它是与怎么的格式记录呢?

假如我们只redis客户端中执行「set name ian」命令,当redis 执行完命令之后,我们去查看AOF日志,可以看到以下内容:

Redis AOF 日志内容

 上图解析:

1. 「*3」表示当前命令有三个部分

2.  每部分都是以「$+数字」开头,其中数字表示该部分的命令、键、值一共有多少字节

3.  后面紧跟着具体的命令、键或值

从Redis AOF 操作过程,我们知道Redis 是先执行写操作命令后,才将该命令记录到 AOF 日志里的,那么它为什么这么设计呢?

1. 避免额外的检查开销:如果先记录日志再执行命令,那么记录日志的时候,就没有检查命令的正确性,一旦出现错误语法,在AOF日志也会记录下面。那么当需要通过AOF日志恢复数据时,也可能出错

2. 避免对当前命令的阻塞:而且先执行命令再记录日志,还可以避免操作日志对当前命令的阻塞

但是,这样设计也是处在风险的。因为执行写操作命令和记录日志是两个过程,当我们刚执行完一个写操作命令,但还没有来得及保存AOF文件时就宕机了,那么就会出现数据丢失的风险。另外我们前面也说了它不会阻塞当前命令,但如果下一个写入操作到来之前,该命令还没有写入完日志,就会阻塞,所以它会有阻塞下一次操作的风险。因为执行写操作命令和记录日志这两个过程都是由主进程完成的,所以它们是同步操作的。如下图:

 其实,我们认真分析一下就知道,这两个风险都有一个共性,就是什么时候写入磁盘(即AOF 日志写回硬盘的时机)

AOF 日志写回硬盘的三大策略

我们先了解一下Redis 写入 AOF 日志的过程,如下图:

Redis 写入 AOF 日志的过程

 上图解析:

  1. redis 执行完写操作命令后,会将命令追加到 server.aof_buf 缓冲区;

  2. 然后通过 write() 系统调用,将 aof_buf 缓冲区的数据写入到 AOF 文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区 page cache,等待内核将数据写入硬盘;

  3. 具体内核缓冲区的数据什么时候写入到硬盘,由内核决定。

AOF 日志写回硬盘的三大策略(通过配置appendfsync来控制):Always、Everysec、No

 这三种写回策略都无法能完美解决「主进程阻塞」和「减少数据丢失」的问题,因这两个问题是一个对立的,所以我们选择的时候就应该根据情况进行取舍,如下选择:

  1. Always 可靠性较高,数据基本不丢失,但是对性能的影响较大;

  2. Everysec 性能适中,即使宕机也只会丢失 1 秒的数据;

  3. No 性能好,但是如果宕机丢失的数据较多。

我们总结起来,这三种策略就如下图:

三种策略总结对比

如果我们对这三个策略更深入的挖掘一下,就会发现它们的根本区别在于对fsync() 函数的调用时机,如下:

  • Always 策略就是每次写入 AOF 文件数据后,就执行 fsync() 函数;

  • Everysec 策略就会创建一个异步任务来执行 fsync() 函数;

  • No 策略就是永不执行 fsync() 函数,而是向普通文件写入一样,把数据拷贝内核缓冲区中,然后排入队列,最后有内核决定何时写入硬盘。

AOF 重写机制

为什么要有AOF 重写机制?

我们知道AOF 是通过文件的方式记录下所有写命令,但是随着写操作越来越多,文件也会越来越大。当AOF 日志文件过大就会带来性能问题,比如再次写入指令的话效率也会变低。另外如果发生宕机,需要读 AOF 文件的内容以恢复数据,如果文件过大,整个恢复的过程就会很慢。所以我们会给AOF 文件的大小设定的阈值,当超过该阈值时候,就会启用 AOF 重写机制,来压缩 AOF 文件,从而避免 AOF 文件越写越大。

AOF 重写是什么?

AOF 重写就是根据所有的键值对创建一个新的 AOF 文件,可以减少大量的文件空间,减少的原因是:AOF 对于命令的添加是追加的方式,逐一记录命令,但有可能存在某个键值被反复更改,产生了一些冗余数据,这样在重写的时候就可以过滤掉这些指令,从而更新当前的最新状态。

AOF 重写为什么要创建一个新的 AOF 文件?

因为如果 AOF 重写过程中失败了,现有的 AOF 文件就会造成污染,可能无法用于恢复使用。

AOF 后台重写

我们知道写入AOF日志的操作是有主进程来完成的,这是因为它写入的内容不多,所以不会太影响命令的执行。但是,重写AOF文件是一个内容比较多,而且很耗时的过程,所以重写操作不能放在主进程中执行。

所以,redis的重写AOF的过程有后台子进程bgrewriteaof来完成,这样可以避免堵塞主进程导致性能下降。这样做的好处有如下:

  • 子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程;

  • 因为主进程没有阻塞,仍然可以处理新来的操作,如果这时候存在写操作,会先把操作先放入缓冲区,对于正在使用的日志,如果宕机了这个日志也是齐全的,可以用于恢复;对于正在更新的日志,也不会丢失新的操作,等到数据拷贝完成,就可以将缓冲区的数据写入到新的文件中,保证数据库的最新状态

  • 子进程带有主进程的数据副本,这里使用子进程而不是线程,因为如果是使用线程,多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能。而使用子进程,创建子进程时,父子进程是共享内存数据的,不过这个共享的内存只能以只读的方式,而当父子进程任意一方修改了该共享内存,就会发生「写时复制」,于是父子进程就有了独立的数据副本,就不用加锁来保证数据安全

子进程是怎么拥有主进程一样的数据副本的呢?

主进程在通过 fork 系统调用生成 bgrewriteaof 子进程时,操作系统会把主进程的「页表」复制一份给子进程,这个页表记录着虚拟地址和物理地址映射关系,而不会复制物理内存,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。

这样一来,子进程就共享了父进程的物理内存数据了,这样能够节约物理内存资源,页表对应的页表项的属性会标记该物理内存的权限为只读

不过,当父进程或者子进程在向这个内存发起写操作时,CPU 就会触发缺页中断,这个缺页中断是由于违反权限导致的,然后操作系统会在「缺页异常处理函数」里进行物理内存的复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对内存进行写操作,这个过程被称为「写时复制(Copy On Write)」。

写时复制顾名思义,在发生写操作的时候,操作系统才会去复制物理内存,这样是为了防止 fork 创建子进程时,由于物理内存数据的复制时间过长而导致父进程长时间阻塞的问题。

当然,操作系统复制父进程页表的时候,父进程也是阻塞中的,不过页表的大小相比实际的物理内存小很多,所以通常复制页表的过程是比较快的。

不过,如果父进程的内存数据非常大,那自然页表也会很大,这时父进程在通过 fork 创建子进程的时候,阻塞的时间也越久。

所以,有两个阶段会导致阻塞父进程:

  • 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;

  • 创建完子进程后,如果子进程或者父进程修改了共享数据,就会发生写时复制,这期间会拷贝物理内存,如果内存越大,自然阻塞的时间也越长;

触发重写机制后,主进程就会创建重写 AOF 的子进程,此时父子进程共享物理内存,重写子进程只会对这个内存进行只读,重写 AOF 子进程会读取数据库里的所有数据,并逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志(新的 AOF 文件)。

但是子进程重写过程中,主进程依然可以正常处理命令。

如果此时主进程修改了已经存在 key-value,就会发生写时复制,注意这里只会复制主进程修改的物理内存数据,没修改物理内存还是与子进程共享的

所以如果这个阶段修改的是一个 bigkey,也就是数据量比较大的 key-value 的时候,这时复制的物理内存数据的过程就会比较耗时,有阻塞主进程的风险。

还有个问题,重写 AOF 日志过程中,如果主进程修改了已经存在 key-value,此时这个 key-value 数据在子进程的内存数据就跟主进程的内存数据不一致了,这时要怎么办呢?

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

在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」

也就是说,在 bgrewriteaof 子进程执行 AOF 重写期间,主进程需要执行以下三个工作:

  • 执行客户端发来的命令;

  • 将执行后的写命令追加到 「AOF 缓冲区」;

  • 将执行后的写命令追加到 「AOF 重写缓冲区」;

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

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

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

  • 新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。

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

在整个 AOF 后台重写过程中,除了发生写时复制会对主进程造成阻塞,还有信号处理函数执行时也会对主进程造成阻塞,在其他时候,AOF 后台重写都不会阻塞主进程

有人会问AOF重写缓冲区占满,怎么办?

其实AOF重写缓冲区不是一块很大的内存空间,而是一个内存链表,每块内存的大小为10MB。如果用完了,可以向系统申请。

AOF启动/修复/恢复

  • 正常恢复
    • 启动:设置Yes
      • 修改默认的appendonly no,改为yes
    • 将有数据的aof文件复制一份保存到对应目录(config get dir)
    • 恢复:重启redis然后重新加载
  • 异常恢复
    • 启动:设置Yes
      • 修改默认的appendonly no,改为yes
    • 备份被写坏的AOF文件
    • 修复:
      • Redis-check-aof --fix进行修复
    • 恢复:重启redis然后重新加载

RDB持久化(内存快照

从上面的简单介绍,我们知道这两种持久化都是使用一个日志记录信息的,只是它们记录的内容不同:

  • AOF 文件的内容是操作命令

  • RDB 文件的内容是二进制数据,即实际的某一个瞬间的内存数据。

RDB配置控制

● save m n : 时间m 和被修改的key个数n ,只有同时满足这两个条件才会触发 bgsava 命令

●  dbfilename dump.rdb : rdb日志文件名

●  dir./ : rdb日志的存放路径

● stop-writes-on-bgsave-error yes:当执行 BGSAVE 命令出现错误时,Redis 是否终止执行写命令。参数的值默认被设置为 yes,表示当硬盘出现问题时,服务器可以及时发现,及时避免大量数据丢失;当设置为 no 时,就算执行 BGSAVE 命令发生错误,服务器也会继续执行写命令;当对 Redis 服务器的系统设置了监控时,建议将该参数值设置为 no。

● rdbcompression yes:是否开启 RDB 压缩文件,默认为 yes 表示开启,不开启则设置为 no。

● rdbchecksum yes:是否开启 RDB 文件的校验,在服务器进行 RDB 文件的写入与读取时会用到它。默认设置为 yes。如果将它设置为 no,则在服务器对 RDB 文件进行写入与读取时,可以提升性能,但是无法确定 RDB 文件是否已经被损坏

RDB日志

Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave,他们的区别就在于是否在「主线程」里执行:

  • save:在主线程中执行,会导致主线程阻塞;

  • bgsave:会创建一个子进程,该进程专门用于写入 RDB 文件,可以避免主线程的阻塞,也是默认的方式。

save和bgsave命令触发条件:

  • 手动通过save和bgsave命令

  • 通过flushall命令,也会产生dump.rdb文件,但是里面是空的,无意义

  • 通过shutdown命令,安全退出,也会生成快照文件(和异常退出形成对比,比如:kill杀死进程的方式)

我们可以采用 bgsave 的命令来执行全量快照,提供了数据的可靠性保证,也避免了对 Redis 的性能影响。执行快照期间数据能不能修改呢?如果不能修改,快照过程中如果有新的写操作,数据就会不一致,这肯定是不符合预期的。Redis 借用了操作系统的写时复制(可以参考AOF后台重写),在执行快照的期间,正常处理写操作。

主要流程为:

  • bgsave 子进程是由主线程 fork 出来的,可以共享主线程的所有内存数据;

  • bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件中;

  • 如果主线程对这些数据都是读操作,那么主线程和 bgsave 子进程互不影响;

  • 如果主线程需要修改一块数据,那么这块数据会被复制一份,生成数据的副本,然主线程在这个副本上进行修改;bgsave 子进程可以把原来的数据写入 RDB 文件

在bgsave 快照过程中我们发现,如果该过程中,主线程修改了共享数据,发生了写时复制后,RDB 快照保存的是原本的内存数据,而主线程刚修改的数据,是被办法在这一时间写入 RDB 文件的,只能交由下一次的 bgsave 快照。

所以 Redis 在使用 bgsave 快照过程中,如果主线程修改了内存数据,不管是否是共享的内存数据,RDB 快照都无法写入主线程刚修改的数据,因为此时主线程的内存数据和子线程的内存数据已经分离了,子线程写入到 RDB 文件的内存数据只能是原本的内存数据。

如果系统恰好在 RDB 快照文件创建完毕后崩溃了,那么 Redis 将会丢失主线程在快照期间修改的数据。

另外,写时复制的时候会出现这么个极端的情况。

在 Redis 执行 RDB 持久化期间,刚 fork 时,主进程和子进程共享同一物理内存,但是途中主进程处理了写操作,修改了共享内存,于是当前被修改的数据的物理内存就会被复制一份。

那么极端情况下,如果所有的共享内存都被修改,则此时的内存占用是原先的 2 倍。

所以,针对写操作多的场景,我们要留意下快照过程中内存的变化,防止内存被占满了。

 混合使用 AOF 日志和 RDB 快照

尽管 RDB 比 AOF 的数据恢复速度快,但是快照的频率不好把握:

  • 如果频率太低,两次快照间一旦服务器发生宕机,就可能会比较多的数据丢失;

  • 如果频率太高,频繁写入磁盘和创建子进程会带来额外的性能开销。

那有没有什么方法不仅有 RDB 恢复速度快的优点和,又有 AOF 丢失数据少的优点呢?

当然有,那就是将 RDB 和 AOF 合体使用,这个方法是在 Redis 4.0 提出的,该方法叫混合使用 AOF 日志和内存快照,也叫混合持久化。

如果想要开启混合持久化功能,可以在 Redis 配置文件将下面这个配置项设置成 yes:

aof-use-rdb-preamble yes

混合持久化工作在 AOF 日志重写过程

当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。

也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据

这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快

加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失。

混合持久化的AOF文件怎么区分前半部分和后半部分呢?

主要是通过 RDB的结束符RDB_OPCODE_EOF结束标记。

如何恢复

appendonly no   #appendonly 设置成no,redis启动时会把/data 目录下dump.rdb 中的数据恢复。dir 和dbfilename 都可以设置。我测试时appendonly 设置成yes 时候不会将dump.rdb文件中的数据恢复

dbfilename dump.rdb
dir /data  #可以自行指定

AOF 和 RDB 的选择问题 

  • 据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择;

  • 如果允许分钟级别的数据丢失,可以只使用 RDB;

  • 如果只用 AOF,优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yi Ian

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值