Redis(三):持久化一、RDB二、AOF三、混合持

Redis作为内存数据库高性能的同时也带来了内存存储数据在重启或异常宕机后会丢失数据的问题。因此redis提供了RDB、AOF和混合持久化三种方式来对内存中的数据进行持久化。

一、RDB

RDB是redis默认的持久化方式,符合一定条件时将目前服务中的所有数据全部写入到磁盘中。优势

  1. rdb是二进制压缩文件,本身占用空间很小,数据恢复速度快。 

  2. 对redis服务能力影响较小,只有在fork子进程的瞬间会阻塞,其他情况下都不影响主进程提供的服务能力。

劣势:

数据可靠性相对aof方式要低,使用 RDB 方式实现持久化,一旦 Redis 异常退出,就会丢失最后一次快照以后更改的所有数据。

1.1 RDB触发条件配置

在redis.conf中有一个save的配置项: 

格式:save <seconds> <changes>
save 900 1 :表示15分钟(900秒钟)内至少1个键被更改则进行快照。
save 300 10 :表示5分钟(300秒)内至少10个键被更改则进行快照。
save 60 10000 :表示1分钟内至少10000个键被更改则进行快照。
复制代码

可以配置多个条件(每行配置一个条件),每个条件之间是“或”的关系。

1.2 RDB相关其他配置说明:
  1. stop-writes-on-bgsave-error :默认值为yes。当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。这会让用户意识到数据没有正确持久化到磁盘上。如果Redis重启了,那么又可以重新开始接收数据了 

  2. rdbcompression :默认值是yes。启用LZF压缩算法,对于存储到磁盘中的快照,可以设置是否进行压缩存储。 

  3. rdbchecksum :默认值是yes。在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。 

  4. dbfilename :设置快照的文件名,默认是 dump.rdb

  5. dir:设置快照文件的存放路径,这个配置项一定是个目录,而不能是文件名。

1.3 手动触发RDB:

除了在redis配置文件中配置条件触发rdb操作以外,也可以通过执行save和bgsave命令来手动触发rdb。

save执行流程:

image.png

bgsave执行流程:

image.png

从上图执行流程可以看出save和bgsave最大的区别在于bgsave不阻塞主流程,在执行rdb生成的过程中仍然能够正常提供服务。bgsave能够实现不阻塞是通过fork一个子进程来进行rdb操作,那么fork到底是什么,redis又是如何通过fork实现不阻塞rdb生成的了?

FORK

fork()是unix和linux这种操作系统的一个api,而不是Redis的api,fork()用于创建一个子进程。fork()出来的进程共享其父类的内存数据。仅仅是共享fork()出子进程的那一刻的内存数据,后期主进程修改数据对子进程不可见,同理,子进程修改的数据对主进程也不可见。比如:A进程fork()了一个子进程B,那么A进程就称之为主进程,这时候主进程子进程所指向的内存空间是同一个,所以他们的数据一致。但是A修改了内存上的一条数据,这时候B是看不到的,A新增一条数据,删除一条数据,B都是看不到的。而且子进程B出问题了,对我主进程A完全没影响,我依然可以对外提供服务,但是主进程挂了,子进程也必须跟随一起挂。这一点有点像守护线程的概念。

Redis如何应用的fork

当bgsave执行时,Redis主进程会判断当前是否有fork()出来的子进程,若有则忽略,若没有则会fork()出一个子进程来执行rdb文件持久化的工作,子进程与Redis主进程共享同一份内存空间,所以子进程可以进行rdb文件持久化工作,主进程又能继续他的对外提供服务,二者互不影响。上面说了他们之后的修改内存数据对彼此不可见,但是明明指向的都是同一块内存空间,这又是如何实现的?肯定不可能是fork()出来子进程后顺带复制了一份数据出来,如果是这样的话比如我有4g内存,那么其实最大有限空间是2g,我要给rdb留出一半空间来,这个时候就copyonwrite技术出马了。

Copyonwrite

现在的问题是主进程和子进程共享了一块内存空间,怎么做到的彼此更改互不影响了? 主进程fork()子进程之后,内核把主进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向主进程。这也就是共享了主进程的内存,当其中某个进程写内存时(这里肯定是主进程写,因为子进程只负责rdb文件持久化工作,不参与客户端的请求),CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入内核的一个中断例程。中断例程中,内核就会把触发的异常的页复制一份(这里仅仅复制异常页,也就是所修改的那个数据页,而不是内存中的全部数据),于是主子进程各自持有独立的一份。

数据修改之前:

image.png

数据修改之后:

image.png

由上面两个图的变化可以看出就是更改数据之前进行copy一份更改数据的数据页出来,比如主进程收到了set k 2请求(之前k的值是1),然后这同时又有子进程在rdb持久化,那么主进程就会把k这个key的数据页拷贝一份,并且主进程中k这个指针指向新拷贝出来的数据页地址上,然后进行更改值为2的操作,这个主进程k元素地址引用的新拷贝出来的地址,而子进程引用的内存数据k还是修改之前的。
总的来说copyonwritefork()出来的子进程共享主进程的物理空间,当主子进程有内存写入操作时,read-only内存页发生中断,将触发的异常的内存页复制一份(其余的页还是共享主进程的)。在 Redis 服务中,子进程只会读取共享内存中的数据,它并不会执行任何写操作,只有主进程会在写入时才会触发这一机制,而对于大多数的 Redis 服务或者数据库,写请求往往都是远小于读请求的,所以使用fork()加上写时拷贝这一机制能够带来非常好的性能。

二、AOF

AOF(append only file)是redis提供的另外一种持久化方式,只追加文件,也就是每次处理完请求命令后都会将此命令追加到aof文件的末尾。默认情况下没有开启。可以通过修改redis.conf中的appendonly yes来开启。

优势:

  1. 数据更完整,秒级数据丢失(取决于设置fsync策略)。

  2. 兼容性较高,由于是基于redis通讯协议而形成的命令追加方式,无论何种版本的redis都兼容。 

  3. aof文件是明文的,可阅读性较好。

劣势:

  1. 数据文件体积较大,即使有重写机制,但是在相同的数据集情况下,AOF文件通常比RDB文件大。

  2. 相对RDB方式,AOF速度慢于RDB,并且在数据量大时候,恢复速度AOF速度也是慢于RDB。 

  3. 频繁地将命令同步到文件中,AOF持久化对性能的影响相对RDB较大。

2.1 AOF-FSYNC刷盘配置:

每当 Redis 执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到 AOF 文件的末尾,这个操作与主进程收到请求、处理请求是串行化的,而非异步并行的,所以aof的频率会对Redis带来很大性能影响,因为每次都是刷盘操作。跟mysql一样了。Redis每次都是先将命令放到缓冲区,然后根据具体策略(每秒/每条指令/缓冲区满)进行刷盘操作。刷盘策略有三个选项:

  1. appendfsync always 每次有新命令追加到 AOF 文件时就执行一次 fsync :非常慢,也非常安全。

  2. appendfsync everysec 每秒 fsync 一次:足够快(和使用 RDB 持久化差不多),并且在故障时只会丢失 1 秒钟的数据。

  3. appendfsync no 从不 fsync :将数据交给操作系统来处理。更快,也更不安全的选择。

推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。

2.1 AOF执行流程:

image.png

2.2 AOF重写:

aof 文件记录的是每一条redis命令,有时候我们会对某一个key进行多次set,中间会产生很多条命令,但是结果只有一个。 set key 1 ... set key 12 set key 123 ... set key 1234 set key 12345
我们执行了上述命令,aof 文件会记录着每一条命令。在redis重启时,会逐条执行上述的命令。但是其实可以精简为set name 12345,其余的几条命令没有意义。AOF重写就是实现了这个功能。

2.3 AOF重写触发条件:

1.自动触发: 

在redis.conf中有如下两个配置项来决定是否触发aof重写操作:

auto-aof-rewrite-percentage 100  指当前aof文件比上次重写的增长比例大小,达到这个大小就进行 aof 重写。 

auto-aof-rewrite-min-size 64mb   最开始aof文件必须要达到这个文件时才触发,后面的每次重写就不会根据这个变量了。 在 aof 文件小于64mb的时候不进行重写,当到达64mb的时候,就重写一次。重写后的 aof 文件可能是10mb。上面配置了auto-aof-rewrite-percentage 100,即 aof 文件到了20mb的时候,又可以开始重写。 

2.手动触发: 

也可以通过执行bgrewriteaof来手动触发。

2.4 AOF重写执行流程:

image.png

AOF文件重写过程与RDB快照bgsave工作过程类似似,都是通过fork子进程,由子进程完成相应的操作。 

  1. 开始bgrewriteaof,判断当前有没有bgsave命令(RDB持久化)/bgrewriteaof在执行,倘若有,则这些命令执行完成以后在执行。

  2. 主进程fork出子进程来进行aof重写操作。 

  3. 主进程fork完子进程后继续接受客户端请求。此时,客户端的写请求不仅仅写入aof_buf缓冲区,还写入aof_rewrite_buf重写缓冲区。一方面是写入aof_buf缓冲区并根据appendfsync策略同步到磁盘,保证原有AOF文件完整和正确。另一方面写入aof_rewrite_buf重写缓冲区,保存fork之后的客户端的写请求,防止新AOF文件生成期间丢失这部分数据。

  4. 子进程写完新的AOF文件后,向主进程发信号,主进程把aof_rewrite_buf中的数据写入到新的AOF文件。 

  5. 用新的AOF文件覆盖旧的AOF文件,标志AOF重写完成。

三、混合持久化

通过上面内容对rdb和aof持久化方式的了解,不难发现这两种方式各有利弊都不完美,那么有没有更好的选择来把这两种方式的优点结合起来了?Redis4.0之后推出了混合持久化的模式。
如果开启了混合持久化,AOF在重写时,不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将重写这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一起,都写入新的AOF文件,等到重写完新的AOF文件会进行改名,覆盖原有的AOF文件,完成新旧两个AOF文件的替换。Redis重启的时候,可以先加载RDB的内容,然后再重放增量AOF日志就可以完全替代之前的AOF全量文件重放,因此重启效率大幅得到提升。

版本开启方式: 

aof-use-rdb-preamble yes  # yes:开启,no:
复制代码
3.1 混合持久化方式流程:

image.png

混合持久化方式的执行流程和aof重写执行流程基本一致,只是在执行重写的那一步不是将aof文件中的命令进行压缩重写,而是将重写这一刻之前的内存做RDB快照,之后将aof_rewrite_buf中的命令追加存储起来,形成一个由rdb格式和aof格式混合组成的新文件。

3.2 如何选择持久化方式?

了解到了redis支持的各种持久化方式后,在实际应用中我们到底该选择那种持久化方式才是最好的了?很显然没有最好的方式,只有最合适的方式,不然redis也不用提供这么多种持久化方式来供选择了。

这里给出一个持久化方式选择的判断模式:

  1. 完全不在乎数据丢失:关闭持久化,将获得极致性能。

  2. 对数据丢失不敏感,能接受一段时间内(根据save策略配置)的数据丢失:选择rdb持久化方式。 

  3. 对数据丢失敏感,需要尽可能避免丢失数据:混合持久化。如果觉得以上的判断方式还是麻烦,还可以提供一个偷懒的思路:要么不开启持久化,要么就选择混合持久化。

四、告一段落

本篇我们详细了解了redis的各种持久化方式以及工作原理,比较了各种方 式的利弊以及根据利弊特性得出了持久化方式的选择逻辑。下篇我们将对redis的主从复制原理来做分析解读。