谈谈Redis的持久化——AOF日志与RDB快照

一、前言

对于Mysql,数据是持久化在磁盘上的。如果误删数据,可以使用binlog进行恢复;突然宕机时,其本身可以借助redo log进行崩溃恢复。

更多关于Mysql日志的内容,可以参考我的另外一篇文章数据库日志——binlog、redo log、undo log扫盲

而对于Redis,一般是把数据直接存储在内存中。如果不做任何持久化工作,在出现宕机后,内存中的全部数据就会丢失。

显然,业务方是不能容忍这样的情况发生的。好在Redis提供了一系列的持久化机制,分别是AOF日志与RDB快照。


二、AOF

AOF全称是Append Only File,Redis每次执行完一个写类型的语句后,会将该语句以某种格式使用追加的方式顺序写入AOF日志中。

值得注意的是,AOF是默认不开启的。

谈谈Redis的持久化——AOF日志与RDB快照_第1张图片

AOF日志的格式

以winows为例,进入到redis安装目录中的redis.windows.conf中,将appendonly的值修改为yes,即可开启AOF

# 默认关闭
appendonly yes

# AOF的默认文件名称
appendfilename "appendonly.aof"

当执行以下命令后

set java helloworld

在appendonly.aof文件中,可以看到以下内容

*3            代表当前命令有3个部分
$3            第1部分命令的长度,3个字符
set           第1部分命令
$4            第2部分命令的长度,4个字符
java          第2部分命令
$10           第3部分命令的长度,10个字符
helloworld    第3部分命令

当我们首次使用某个客户端执行命令时,客户端会自动帮我们补充select 0(即选择编号0的数据库),这个命令也会被保存在AOF日志中。

写AOF日志的流程

大致的流程如图所示

谈谈Redis的持久化——AOF日志与RDB快照_第2张图片

 在server中,主线程执行完命令之后,会立即将命令写入AOF缓冲中。之后会调用系统函数write(),将命令写入内核缓冲区,并返回给客户端成功的响应。

内核会在合适的时机将内核缓冲区的中的数据写入到磁盘中。

我们设想其中某个阶段宕机时,会不会产生不一致的情况:

1、如果命令执行成功,但写入AOF缓存前崩溃重启,客户端会收到执行失败或超时的响应。重启之后AOF文件中没有该条数据,这个时候,数据是一致的。

2、如果命令执行成功,写入AOF缓存成功,但调用write时崩溃重启。其实这种情况和第一条一样,恢复后数据还是一致的。

3、如果命令执行、写入AOF缓存与内核缓存都成功,客户端会收到成功的响应。如果这个时候机器宕机,内核缓冲区中的数据将会丢失,也就是最后的AOF文件缺少该条命令,恢复后,就会产生数据不一致的情况。

第3种情况发生时,就会出现数据不一致的后果。怎么处理呢,很简单啊,变异步为同步不就行了吗。

调用write写入内核缓冲区后,再调用fsync强制让内核缓冲区中的数据刷到磁盘上,刷盘成功后,再返回给客户端响应。

这样的解决方式看似可以,但是刷盘的操作非常耗时。在Redis执行大量命令的时候,会一直进行不断的刷盘,当磁盘压力过大时,会阻塞下一个命令的执行,大大降低性能。

看来得把握刷盘的时机,刷得慢了,机器崩溃恢复后就会丢失大量数据。刷得快了,就会严重降低性能。

不过,Redis本身也提供了3种写回策略。

写回策略

  • always      同步写回。每执行一条命令,写完AOF日志后,再返回。
  • everysec   每秒写回。执行命令后,将数据写入到内核缓冲区就返回。只有会有一个线程,执行每秒刷盘的定时任务。
  • no              由内核自行控制的写回。每执行一条命令,将数据写入到内核缓冲区就返回。内核会在合适的时机刷盘。

这3种策略体现了不同的刷盘频率,因此拥有不同级别的一致性与性能。

always策略最大程度上保证数据不丢失,但性能最差。

no策略性能最好,但在机器崩溃重启后会丢失比较多的数据。

everysec是一种折中的策略,较always有不错的性能。在极端的情况下,只会丢失1秒内的数据,是比较推荐的方式。

redis.windows.conf中有appendfsync配置项,用来配置写回策略,默认的策略是everysec

随着Redis不断记录AOF日志,AOF日志文件将变得越来越大,用作恢复的时间也将越长。因此需要一种方式减少文件的大小,这时候AOF重写就派上用场了。

AOF重写

在出现触发重写的条件时(例如AOF文件达到某个阈值),Redis扫描整个库的所有数据,将数据以命令的方式记录在新的AOF日志中,待记录完成后,使用新的AOF日志替换旧的即可。

旧日志中,可能存有对同一个key的多次操作命令,重写的目的就是取最后一次有效的命令,删除那些历史命令,从而达到瘦身、压缩的效果。

刚才提到,AOF重写会扫描整个库的数据,因此注定就是一个非常耗时的操作,那么就不会在主线程中做,而是通过主线程fork出一个子进程进行重写的。

重写的流程图如下:

谈谈Redis的持久化——AOF日志与RDB快照_第3张图片

1、当AOF日志文件的大小超过执行的阈值后,就会触发AOF重写

2、主线程fork出一个子进程,fork的过程仍然是阻塞的。fork完之后,主线程依然可以接受命令并处理

3、子进程与主线程共享一个实例的所有数据,子进程会对整个实例进行扫描,将其中的数据以命令的格式写入到重写日志中。

4、在子进程重写的过程中,主线程可以接受命令,假设这个时候执行了一条写命令。

5、主线程会将数据存入到库中,利用写时复制技术,子进程不会感知到数据有任何变化。

6、主线程将日志先写入AOF缓冲区,再写入重写缓冲区。

7、由特定写回策略,将缓冲区中的数据写入到旧的AOF日志中。

8、当子进程结束扫描,并且将所有命令写入重写日志后,再将重写缓冲区中的数据追加到重写日志中。

9、最后一步,主线程感知到子进程重写日志完成,于是使用新的日志文件替换旧的文件。

也许有人会发出以下的疑问

为什么是fork出子进程,直接使用子线程不是也可以吗?

如果是创建出来一个子线程,那么主线程在写入,子线程在读取,是需要通过加锁的方式来保证线程安全的,加锁就意味着降低性能。

而如果是fork出来子进程,主线程和子进程同样需要共享数据,当主线程写入数据的时候,会利用写时复制技术,避免加锁。

什么是写时复制?

大家应该都知道三角函数吧,嗯,这和写时复制没什么关系。

谈谈Redis的持久化——AOF日志与RDB快照_第4张图片

 CopyOnWriteArrayList就利用到了写时复制,读不加锁,写则是复制一份数组出来,在新的数组上进行修改,最后替换引用。非常适合应用于读多写少的场景,缺点是在替换引用前,线程读到的是旧数据。

主线程在fork出一个子进程的时候,会将自己的页表(虚拟地址与物理地址的映射表)复制一份出来给子进程,而不是直接复制内存。否则在重写的时候,Redis占用内存会立即翻倍。

这样的话,子进程就可以随意访问主线程中的数据。而当主线程修改一些实例数据时,就会复制一份物理内存出来,并变动主线程的页表,在新的内存地址上存储写之后的数据。因为没有变动子进程的页表,因此主线程写入的数据对子进程不可见。

重写AOF缓冲区的作用是什么?

CopyOnWriteArrayList的缺点在于读到的可能是旧数据,子进程在扫描的时候,其实扫描到的也是旧数据,因此需要在重写结束后做补偿。

子进程在重写的过程中,扫描的数据是fork动作结束的那一刻的快照。而在重写的过程中,主线程依然可以执行命令,那么这些多出来的写命令就可以放在一个独立的重写缓冲区中。在重写完成后,再将重写缓冲区中的内容追加到重写日志中,这就保证了数据的一致。

尽管存在AOF重写机制,但重写后的日志文件还是大,恢复速度较慢。

有没有一种直接存储数据,而不是存储命令(命令的大小显然大于数据本身)的方式呢?RDB就闪亮登场了!


三、RDB

RDB的全称是Redis Database Backup,即数据备份。

会将某一时刻内的所有数据生成一个快照文件。该文件是一种经过压缩的二进制文件,默认名称为dump.rdb,可通过修改dbfilename参数来改变RDB文件名。

快照文件仅保存数据,不保存额外的操作命令,且经过压缩,因此在恢复速度上快于AOF。但RDB没法做到实时的持久化,而AOF可以基本做到。

如何让Redis生成RDB文件

通过save命令手动触发

直接在主线程中执行,会阻塞其他命令

通过bgsave命令手动触发

主线程fork出来一个子进程,由子进程去执行备份。

整个fork的过程,是会阻塞主线程的。由于不会复制物理内存,因此fork是快速的。

fork结束后,主线程依然可以执行其他的命令。

通过配置自动触发

redis.windows.conf中有如下的几个配置可用于触发生成RDB文件

# 900秒内至少出现1条写命令就触发
save 900 1

# 300秒内至少出现10条写命令就触发
save 300 10

# 60秒内至少出现10000条写命令就触发
save 60 10000

这种方式,也是通过fork出一个子进程来做的。

三种方式的触发流程

谈谈Redis的持久化——AOF日志与RDB快照_第5张图片

客户端使用bgsave命令时,主线程fork出来子进程,由子进程完成备份。

在子进程备份期间,主线程依然可以执行命令。但该条数据并不会被子进程扫描到,和AOF重写一样,都利用到了写时复制。

既然RDB文件占用小,恢复速度快,那可以大幅增加RDB生成的频率吗?

那显然是不可以的,有可能上一轮RDB还未生成,下一轮又开始了。而且也存在性能问题,save全程都会阻塞主线程,bgsave的fork操作同样也会阻塞主线程。

当然,RDB这种方式,如果在持久化的过程中发生宕机,会丢失在上次备份之后产生的所有数据。


四、AOF与RDB的特点总结

下面使用一张表格来直观地展示两者之间的优缺点

AOF RDB
持久方式 实时追加写入 某一时刻的数据快照
文件大小 包含操作命令,文件较大 只包含数据,且经过压缩,文件较小
恢复速度
数据可靠性 由写回策略决定,最好情况下仅丢失1秒数据 由触发频率决定,会丢失上一次备份之后产生的所有数据,可靠性不如AOF

另外值得注意的是,当同时开启AOF与RDB时,Redis会优先使用AOF日志来恢复数据。

RDB相比而言,会丢失较多的数据。AOF只有在实例数据比较大的时候,恢复速度才慢。


五、Redis4.0混合持久化模式

既然AOF与RDB独有各自的优势,能否结合二者的特点呢?

在Redis4.0中,出现了一个新的模式——混合持久化。具体来讲,就是全量RDB+增量AOF,将两种类型的日志文件存放在一起。

RDB可以以较低的频率执行,两次RDB之间的产生的增量数据记录在AOF日志中,因此增量AOF日志的文件很小。

因此Redis在恢复时,先加载RDB数据,再重放增量的AOF日志。不需要像之前重放全量AOF日志,因此恢复效率大大提升。

你可能感兴趣的:(Redis,redis)