图说Redis持久化 RDB和AOF,我终于全明白了!

前言

哈罗,大家好,距离上一次更新文章已经半个多月了,原本是每周一更的,但是因为别的事耽搁了,具体什么事情,咱们后续会说,今天的内容主要是围绕redis的持久化去展开。

大家准备好,发车!一起学习,一起成长!

全文字数 : 8k+ ⏳

阅读时长 : 12min

关键词 : RDB、AOF、AOF重写、写时复制、混合持久化

我们先来看看为什么需要进行持久化!

Redis 是内存数据库,这个大家都知道,它将自己的数据库状态储存在内存里,但是如果不想办法将储存在内存中的数据库状态保存到磁盘里,那么一旦服务器进程退出(比如宕机,断电啥的),服务器中的数据库状态也会消失不见。然而在很多使用场景中我们希望数据不丢失,服务重启之后数据还能恢复到停机前的状态,这就需要进行持久化到磁盘上了!

Redis 提供了两种持久化方式------ RDB(Redis DataBase) 和 AOF(Append Only File) ,这可以将 Redis 在内存中的数据库状态保存到磁盘里。

RDB 是保存和还原Redis服务器所有数据库中的键值对数据(二进制数据) ,快照并不是很可靠。如果你的电脑突然宕机了,或者电源断了,又或者不小心杀掉了进程,那么最新的数据就会丢失。

AOF (Append Only File) 持久化是通过保存Redis服务器执行的写命令来记录数据库状态的。默认是关闭的,通过将 redis.conf 中将 appendonly no,修改为 appendonly yes 来开启AOF 持久化功能。

RDB是Redis的默认持久化方式,如果服务器开始了 AOF 持久化功能,服务器会优先使用 AOF 文件来还原数据库状态。只有在 AOF 持久化功能处于关闭状态时,服务器才会使用 RDB 文件来还原数据库状态,加载持久化文件的先后顺序如下。

我们来看看优缺点,对于Redis持久化优缺点对比如下图

RDB持久化

先来看看官方Redis官网链接怎么描述RDB(Redis Database)的:

RDB (Redis Database): RDB persistence performs point-in-time snapshots of your dataset at specified intervals 翻译过来就是:RDB持久性以指定的间隔执行数据集的时间点快照

所谓的快照,就是记录某一个瞬间东西,比如当我们给风景拍照时,那一个瞬间的画面和信息就记录到了一张照片,这么比喻是不是更好理解一点。

RDB生成方式

rdb文件生成的方式有多种

自动触发

在redis.conf配置文件中有对应的配置来自动触发进行快照:

Redis 还可以通过配置文件的选项来实现每隔一段时间自动执行一次 bgsave 命令,虽然选项名称时save,实际上执行的是bgsave命令。

bash 复制代码
save 900 1     #900 秒之内,对数据库进行了至少 1 次修改;
save 300 10    #300 秒之内,对数据库进行了至少 10 次修改
save 60 10000  #60 秒之内,对数据库进行了至少 10000 次修改

# The filename where to dump the DB
dbfilename dump.rdb #默认的rdb文件名
dir ./    #指定rdb目录,这里代表的是当前目录

这几个配置分别代表的意思是,只要满足上面任意一个条件,就会执行bgsave,而 dump.rdb 是默认的快照文件名。

save和bgsave命令

Redis提供了两个命令来⽣成RDB⽂件(手动触发),分别是save和bgsave。

执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程,导致

Redis不能处理其他命令,因此线上禁止使用。

举个栗子,示例中设置两个键值对,然后之心save命令:

命令bgsave执行后,会立刻返回OK,Redis 会fork一个子进程,原来的redis主进程继续执行后续操作,新fork的子进程负责将数据保存到磁盘,然后退出,如下图:

flushall命令

Redis Flushall 命令用于清空整个 Redis 服务器的数据(删除所有数据库的所有 key ),执行flushall/flushdb命令也会产生dump.rdb文件,但里面是空的,无意义,但是我们要知道也会产生这个rdb文件。

第一次主从复制时

了解过redis主从复制的朋友可能更清楚一点,不过没关系,这里把主要的流程画一下,你就知道为啥这里也会进行rdb快照了。

主从复制分为全量和增量复制:

  • 全量复制:比如第一次同步时
  • 增量复制:只会把主从库网络断连期间主库收到的命令,同步给从库

而在全量复制的时候会涉及到rdb生成,一起来看图,瞅瞅什么时候会发生rdb的生成:

  1. slave给master发送 psync 命令,表示要进行数据同步,主库根据这个命令的参数来启动复制
  2. master会执行BGSAVE指令,生成RDB文件,并发给从库
  3. 从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件
  4. master发送rdb到slave的过程中,收到的指令存储到replication buffer中,并发送给从库

RDB加载

RDB文件是保存在硬盘里面的,所以即使Redis服务器进程退出,甚至运行Redis服务器的计算机停机,但只要RDB文件仍然存在,Redis服务器就可以用它来还原数据库状态。

默认情况下rdb文件是在/usr/local/bin目录下,在实际开发中,一般会选择网络存储设备,比如对象存储,NAS或者云盘。

rdb存储目录可修改吗?

是的,可以。通过执行config set dir {newDir}和config set dbfilename {newFileName}运行期动态执行,当下次运行时RDB文件会保存到新目录。

写时复制 Copy-On-Write

这里我们可能有会产生个问题,就是执行 bgsave 过程中,由于是交给子进程来构建 RDB 文件,主线程还是可以继续工作的,那么此时主进程还能继续接收用户的指令吗?

答案是可以的,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的,这里就是用到了写时复制技术(Copy-On-Write)

这里其实分为两部分吧,一部分是bgsave进行子进程创建,另一部分是在执行bgsave是同时接收用户执行指令,我们分开来说。

一:在执行 bgsave 命令的时候,会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个。

二:而在只有在发生修改内存数据的情况时,物理内存才会被复制一份,都是读操作的话互不影响

为什么在这个时候才会发生呢?

主要是为了减少创建子进程时的性能损耗,从而加快创建子进程的速度,毕竟创建子进程的过程中,是会阻塞主线程的

从图中可以总结出:

当主进程开始写操作的话,首先把数据变更成 【read-only】只读模式

然后复制一份【物理内存】中的数据生成一个【副本】数据

此时主进程的写操作就会基于【副本】数据进行操作,而子线程是继续对原数据写到rdb文件中

同时主进程的读操作也会变更成从【副本】的读数据,也就是【主进程】中指向物理内存的地址将要变更【副本】地址

如果再发生【bgsave】的时候就会把新的数据持久化到rdb文件中

极端情况

在 Redis 执行 RDB 持久化期间,刚 fork 时,主进程和子进程共享同一物理内存,但是途中主进程处理了写操作,修改了共享内存,于是当前被修改的数据的物理内存就会被复制一份。 那么极端情况下,**假如所有的内存都被修改,那么此时的内存占用是原先的 2 倍,**比如4G的内存数据需要同时进行写操作的话,就会全部copy一份,这样一下子就会占用8G的内存。

AOF持久化

AOF (Append Only File): AOF persistence logs every write operation received by the server. These operations can then be replayed again at server startup, reconstructing the original dataset. Commands are logged using the same format as the Redis protocol itself.

AOF持久性记录服务器接收到的每个写操作,然后,可以在服务器启动时再次重播这些操作,重建原始数据集,使用与Redis协议本身相同的格式记录命令。

AOF【默认情况下是不开启的】,但是是一种更为可靠的持久化方式,每当 Redis 接受到会修改数据集的命令时,就会把命令追加到 AOF 文件里,当你重启 Redis 时,AOF 文件里的命令会被重新执行一次,从而达到数据重建的目的。

注意哦,这里是会记录写操作命令,读操作是不会被记录的,因为记录读操作没有意义!

而且是在命令执行写操作命令成功后,才将命令记录到AOF日志里。

配置内容

我们来看看redis关于aof持久化的配置,如下图,侧面也看出来默认是no,不开启

这里我们将appendonly改成yes ,从截图开头的部分可以看到AOF和RDB是同时存在的,不过同时开启的情况下Redis是先加载AOF

AOF文件内容

存在AOF文件的内容都是以 Redis 的命令请求协议(简称:RESP协议)格式保存的,不清楚的这个协议的同学可以去看我之前的分享【Redis是如何执行用户命令的?过程居然是这样的!】,里面有对Redis命令协议的介绍。

这里也简单举个栗子:

vbnet 复制代码
//执行set命令, key是xiaoxu, value是code
SET xiaoxu "code"

那么在AOF文件中将存下面内容

bash 复制代码
*3\r\n$3\r\nSET\r\n$3\r\nxiaoxu\r\n$5\r\ncode\r\n

三种写磁盘策略

AOF 持久化功能的实现可以分为 命令追加(append)、文件写入、文件同步(sync)三个步骤。

在服务器在执行完一个写命令之后,会以Redis协议格式将被执行的写命令追加到服务器状态的 aofbuf 缓冲区的末尾,要是再执行一个写命令,那么会继续追加到aof_buf 缓冲区末尾,这就是追加方式。

然后通过 write() 函数,将 aof_buf 缓冲区的数据写入到 AOF 文件,在AOF功能开启的情况下,文件事件会将成功执行之后的写命令追加到aof_buf缓冲区,在主服务进程死循环的最后,会调用flushAppendOnlyFile函数,该函数会将aof_buf中的数据写入到内核缓冲区,然后判断是否应该进行同步。

为什么会需要write()?

为了提高文件的写入效率,在现代的操作系统中,当用户调用 write 函数,将一些数据写入到文件的时候,操作系统通常会将写入数据暂时保存在一个内存缓冲区里面,等到缓冲区的空间被填满、或者超过了指定的时限之后,才真正的将缓冲区中的数据写入到磁盘里。

对Redis而言,服务进程就是一个事件循环(loop),每次结束一个事件循环之前,它都会调用 flushAppendOnlyFile 函数,考虑是否需要将 aof_buf 缓冲区的内容写入和同步到 AOF文件里,也就是上图中的写入磁盘的过程。

实际上写入时是执行函数 【flushAppendOnlyFile】 ,具体由服务器配置的 appendfsync 选项的值来决定,而appendfsync的选项值是有三种,分别是 always、everysec、no

always:每次执行写操作命令后,同步将AOF日志记录到磁盘文件中,这种方案的完整性好但是IO开销很大,性能较差

everysec:每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘。但是如果在一秒内宕机的话可能失去这一秒内的数据

no:不由 Redis 控制写回硬盘的时机,至于何时对 AOF 文件进行同步,则由操作系统控制

我们用图看看在不同模式下区别,分别用图来理一理!

always模式下每个写命令执行完,立刻同步地将日志写回磁盘。此模式下同步操作是由 Redis 主进程执行的,所以在同步执行期间,主进程会被阻塞,不能接受命令请求,流程图如下:

everysec模式下每秒同步,每个写命令执行完,只是先把日志写到 AOF文件的内核缓冲区,理论上每隔1秒把缓冲区中的内容同步到磁盘,且同步操作有单独的子线程进行,因此不会阻塞主进程。

no模式下由操作系统内核决定同步时机,每个写命令执行完,只是先把日志写入AOF文件的内核缓冲区,不立即进行同步。

但是你会发现好像这三种模式的选择好像都是会选择牺牲另一个点,我们总结下三种模式的优缺点:

借用下网图,从不同角度比较了三种模式。

场景模式选择建议:

  • 想要性能:就选择 no 策略
  • 想要可靠性:就选择 always 策略;
  • 允许数据丢失一点,但又想性能高:就选择 everysec 策略

aof文件载入和还原

因为AOF 文件里面已经是包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍 AOF 文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态,是的执行一遍写命令就可以了。

1:创建一个不带网络连接的伪客户端(fake client)

2:从 AOF 文件中分析并读取出一条写命令。

3:使用伪客户端执行被读出的写命令。

4:直到 AOF 文件中的所有写命令都被处理完毕为止。

为什么是创建不带网络连接的客户端: 因为 Redis 的命令只能在客户端上下文中执行,而载入 AOF 文件时所使用的命令直接来源于 AOF 文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行 AOF 文件保存的写命令,伪客户端执行命令的效果和带网络连接的客户端执行命令的效果完全一样。

AOF重写机制

Redis提供的AOF重写机制【通过压缩 AOF 文件的方式】来避免AOF文件越写越大

因为AOF 持久化是通过保存被执行的写命令到日志文件,那么时间一长,或者本身业务上写命令很多,就会造成AOF文件体积越来越大。

AOF文件过大不仅影响写入,在恢复数据过程也会很慢,带来性能问题。

为什么AOF重写可以减小AOF文件大小?

AOF重写的实现机制是 Redis 服务器通过创建一个新的 AOF 文件,新旧两个 AOF 文件所保存的数据库状态相同,但新 AOF 文件不会包含任何浪费空间的冗余命令,所以新 AOF 文件的体积通常会比旧 AOF 文件的体积小很多。 重写是先读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。 新AOF文件的生成并非是在原AOF文件的基础上进行操作得到的

可以看出在进行AOF重写后,新的AOF文件,只记录了 【set xiaoxu code2】命令,之前的对xiaoxu这个键的写入都没记录,保留的是最终的命令,这样就就相当于对原始AOF文件进行了压缩,如果是大量这种,经过AOF重新后压缩效果会非常可观!

后台重写

AOF 日志由主进程写入完成的,为了避免阻塞主线程,重写过程是由【子进程执行bgrewriteaof】来完成的。

好处是:

  1. 子进程进行 AOF重写期间,主进程可以继续处理命令请求,避免阻塞主进程
  2. 子进程带有主进程的数据副本,操作效率更高

后台重写是在什么时候触发的呢?

arduino 复制代码
 //当前 AOF 文件大小和最后一次 AOF 重写后的大小之间的比率
auto-aof-rewrite-percentage 100 

 //AOF文件大于64M
auto-aof-rewrite-min-size 64mb  

 //或者用户手动通过调用 bgrewriteaof手动触发 

数据副本?怎么跟RDB的bgsave命令原理很像,是的,更详细的可以翻到RDB章节去看写时复制

因为是用子进程,操作系统会使用「写时复制」的技术:

fork子进程时,子进程会拷贝父进程的页表,即虚实映射关系,而不会拷贝物理内存。

子进程复制了父进程页表,也能共享访问父进程的内存数据,达到共享内存的效果

fork()的子进程在进行 AOF 重写期间,主进程还需要继续处理命令,而新的命令可能对现有的数据进行修改, 会让当前数据库的数据和重写后的 AOF 文件中的数据不一致,这该怎么办呢?

这里会引入「AOF 缓冲区」和 「AOF 重写缓冲区」两个缓冲区的概念,当然AOF缓冲区(aof_buf)在上面有提到过。

我们来看看是在什么时候怎么用到的

从图中基本可以知道,当子进程在执行AOF重写(bgrewriteaof)时, 主进程需要执行以下三个工作:

  1. 处理客户端的命令请求;
  2. 将写命令追加到AOF缓冲区(aof_buf);
  3. 将写命令追加到AOF重写缓冲区

在整个AOF后台重写过程中,只有信号处理函数执行和 写时复制 时会对主进程造成阻塞,在其他时候,AOF 后台重写不会阻塞主进程。

混合持久化

重启 Redis 时,如果使用 RDB 来恢复内存状态,会丢失大量数据。而如果只使用 AOF 日志重放,那么效率又太过于低下。

Redis 4.0 提供了混合持久化方案,将 RDB 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自 RDB 持久化开始到持久化结束这段时间发生的增量 AOF 日志,通常这部分日志很小。

混合持久化的持久化文件是什么样的?如何加载恢复?

混合持久化同样也是通过AOF重写的bgrewriteaof完成的,不同的是当开启混合持久化时,fork出的子进程先将共享的内存副本全量的以RDB方式写入aof文件。

然后在将aof_rewrite_buf重写缓冲区的增量命令以AOF方式写入到文件,写入完成后通知主进程更新统计信息,并将新的含有RDB格式和AOF格式的AOF文件替换旧的的AOF文件,文件内容是以 RDB 的格式写入到 AOF 文件的开头,之后的数据再以 AOF 的格式化追加的文件的末尾。

因此Redis重启的时候,也是通过加载 aof 文件进行恢复数据:先加载 rdb 内容再加载剩余的 aof内容。

我们可以通过配置来开启混合持久化:

bash 复制代码
aof-use-rdb-preamble yes  # yes:开启,no:关闭

常见面试问题

这里列举几个常见的关于RDB和AOF的面试题,只要你理解了它两的原理,你都能灵活回答的,因为问题是围绕这些基础换了种方式问而已。

1:Redis 正在进行RDB 此时有大量写入,更新操作会怎么样,如何保证数据一致性?

主线程和 fork 的 bgsave 子进程相互不影响。如果主线程要修改一块数据(例如图中的键值对 C),那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据,这就是前面提到的写时复制技术。

2:在进行RDB快照操作的这段时间,如果发生服务崩溃怎么办?

服务崩溃的情况,将以上一次完整的RDB快照文件作为恢复内存数据的参考 , Redis服务会在磁盘上创建一个临时文件进行数据操作,待操作成功后才会用这个临时文件替换掉上一次的备份。

3:AOF重写会阻塞主进程吗?

在aof在重写时,在fork进程时是会阻塞主进程的

4:AOF重写时,有新数据写入,这时候怎么让新下记录不丢失?

由于AOF的重写是由Redis主线程之外一个子线程执行,是在AOF写入的时候,会重新建立一个AOF重写缓冲区,当用户对数据库进行操作时,会把用户的操作追加到AOF重写缓冲区和AOF缓冲区中,此时AOF文件写入操作会同时从AOF缓冲区和AOF重写缓冲区两个地方读入数据,这样就保证了用户的添加修改操作的不丢失

5:在AOF重写日志整个过程时,主线程有哪些地方会被阻塞?

  • fork子进程:fork子进程,fork这个瞬间一定是会阻塞主线程的

  • AOF重写中父进程有写入的场景 :可能会产生阻塞风险

  • 主进程有bigkey写入时,操作系统会创建页面的副本,并拷贝原有的数据,会对主线程阻塞

👨👩 朋友,希望本文对你有帮助~

🌐 欢迎点赞 👍、收藏 💙、关注 💡 三连支持一下~🎈

我是小许,下期见~🙇💻

相关推荐
NiNg_1_23442 分钟前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
Chrikk2 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue2 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man2 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
customer084 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml45 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
水月梦镜花6 小时前
redis:list列表命令和内部编码
数据库·redis·list