写点什么

Redis(三):持久化

作者:IT巅峰技术
  • 2021 年 12 月 14 日
  • 本文字数:4580 字

    阅读完需:约 15 分钟

Redis(三):持久化

 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 执行流程:



bgsave 执行流程:



从上图执行流程可以看出 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),陷入内核的一个中断例程。

中断例程中,内核就会把触发的异常的页复制一份(这里仅仅复制异常页,也就是所修改的那个数据页,而不是内存中的全部数据),于是主子进程各自持有独立的一份。

数据修改之前:



数据修改之后:



由上面两个图的变化可以看出就是更改数据之前进行 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 执行流程:



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 重写执行流程:



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 混合持久化方式流程:



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

3.2 如何选择持久化方式?

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

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

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

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

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

四、告一段落

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




用户头像

一线架构师、二线开发、三线管理 2021.12.07 加入

Redis6.X、ES7.X、Kafka3.X、RocketMQ5.0、Flink1.X、ClickHouse20.X、SpringCloud、Netty5等热门技术分享;架构设计方法论与实践;作者热销新书《RocketMQ技术内幕》;

评论

发布
暂无评论
Redis(三):持久化