跳至主要內容

Redis-基础篇-④持久化

holic-x...大约 25 分钟RedisRedis

Redis-基础篇-④持久化

学习核心

  • Redis数据丢失场景概念
  • AOF持久化
  • RDB快照

学习资料

持久化

什么是持久化?

​ Redis是内存数据库,当程序重启或者服务崩溃时,数据就会丢失。如果业务场景希望重启之后数据还在(可自动恢复之前的数据),则需要引入持久化机制,即把数据保存到可永久保存的存储设备中。

​ 结合实际业务场景分析,虽然Redis作为缓存的场景比较多,但是其本身也支持用作存储场景(通过持久化机制)。考虑一个场景,如果Redis崩溃后数据都不在了,恰好这个时候有大量请求涌入,那么此时缓存为空就会有大量请求落在数据库中,如果数据库性能无法支撑则可能会导致服务异常。

持久化方式?

​ Redis 提供两种方式进行持久化:

  • RDB (Redis Database Backup):记录Redis某个时刻的全部数据。其本质是数据快照,直接保存二进制数据到磁盘,后续通过加载RDB文件恢复数据
  • AOF(Append Only File):记录执行的每条命令,重启之后通过重放命令来恢复数据。其本质是记录操作日志,后续通过日志重放恢复数据

​ 结合两种的方式实现分析,可对比其区别:

  • 文件类型:RDB生成的是二进制文件(快照),AOF生成的是文本文件(追加日志)
    • 相同数据量下,RDB体积更小(因为RDB是记录的二进制紧凑型数据)
  • 恢复速度:RDB是数据快照,可以直接加载;而AOF文件恢复,相当于重放情况,RDB显然会更快
  • 数据完整性/安全性:
    • 缓存宕机时,RDB容易丢失较多数据;AOF可根据策略配置刷盘频率,控制数据丢失风险
    • AOF记录了每条日志,RDB是间隔一段时间记录一次,用AOF恢复数据通常会更为完整
  • 操作成本:
    • RDB的每一次操作都是全量保存,操作成本较高,一般设置间隔几分钟保存一次数据
    • AOF是基于数据追加的形式,操作成本较低,可设置刷盘频率

AOF VS RBD 的选择 =》结合实际业务场景选择(如果只能选一种,则在性能和可靠之间做选择)

​ 如果业务本身只是缓存数据且并不是一个海量访问,可以不用开持久化。

​ 如果对数据非常重视,可以同时开启RDB和AOF,同时开启的情况下RDB只是个备份,实际用的是AOF来进行加载。流程参考下图所示

image-20240717181148590

问题思考:如果同时开启RDB和AOF,启动时会加载哪一个 ?=》有AOF加载AOF

​ 此处选择用AOF而不用 RDB 去恢复数据的原因在于:开启 了AOF表名意在要求数据的强一致性,那么不会用RDB来加载(因为可能 RDB 会少更多的数据)

​ 且需要注意到是RDB的快照触发时机:虽然可以通过fork出的子进程来做全量快照,但是如果每一秒一次,会导致很大的性能开销,可能这一秒的快照都没完成,下一秒又fork出一个子进程来做快照,所以RDB的快照触发间隔是比较难确定的,原则上就是不能太短,一般都是几分钟以上。在实际源码中,会判断如果上一轮RDB是否完成,如果没有完成这一轮RDB也是不会开始的

AOF

1.AOF概念核心

​ Redis中的AOF(Append Only FIle):通过保存写操作命令到日志的持久化方式(注意此处只记录写操作,读操作命令不会被记录,因为记录读操作没有意义)。在Redis中AOF持久化功能默认不开启,需要修改redis.conf配置

appendonly yes  # 表示是否开启AOF持久化(默认no,关闭)
appendfilename "appendonly.aof"  # AOF持久化文件名称

image-20240718082402030

​ Redis 是先执行写命令后记录命令到AOF日志,这种设计思路有两个好处:

  • 避免额外的检查开销:如果是先记录日志后执行写命令,如果当前命令有问题而又没有进行检查的话,恢复数据的时候就会异常。因此先确保写命令执行成功后记录到日志,进而避免额外的检查开销
  • 不会阻塞当前写操作命令的执行

​ 当然,AOF持久化功能也有潜在的风险:进一步理解分析,这两个潜在的风险都有一个共性:和AOF日志写回硬盘的时机有关

  • 存在数据丢失风险:执行写命令和记录AOF日志是两个过程,当Redis还没来得及将命令追加到日志就发生了宕机,那么这部分数据就会有丢失风险
  • 可能会给下一个命令带来阻塞风险:执行命令和记录日志两个操作都是在主进程中执行的,这两个操作是同步的。虽然是先执行命令后记录日志,只是不会阻塞当前命令,但可能会阻塞下一个命令的执行。如果在将日志内容写入到硬盘时,服务器的硬盘的 I/O 压力太大,就会导致写硬盘的速度很慢,进而导致阻塞,致使后续的命令无法执行

image-20240718083427810

2.写回策略

Redis 写入 AOF 日志 过程分析

​ Redis 写回AOF日志的过程分析如下:

执行写操作命令=》追加命令到server.aof_buf缓冲区=》调用write()将缓冲区内容写到AOF文件(拷贝到内核缓冲区page cache)=》内核将内核缓冲区数据写入硬盘

image-20240718084139264

  • 【步骤1】Redis 执行完写操作后会将命令追加到server.aof_buf缓冲区
  • 【步骤2】通过write()系统调用,将缓冲区的数据写入到AOF文件(此时数据还没有写入到硬盘,而是拷贝到内核缓冲区page cache,等待内核将数据写入硬盘)
  • 【步骤3】由内核决定具体内核缓冲区的数据什么时候写入硬盘

Redis 的3种写回硬盘的策略

Redis提供了3种写回硬盘的策略,控制的是上述【步骤3】的过程,在redis.conf配置文件中的appendfsync配置项中可配置3种参数:

  • Always:每次写操作命令执行完,同步将AOF日志数据写回硬盘
  • Everysec:每次写操作命令执行完,先将命令写入到AOF文件的内核缓冲区,然后每隔1s将缓冲区中的内容写回到硬盘
  • No:不由Redis控制写回硬盘的时机,而是转交给操作系统进行控制。即每次写操作命令执行完,先将命令写入到AOF文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘

针对【主进程阻塞】和【数据丢失】问题,这3种写回策略都无法做到完美解决,因为本身这两个问题就是对立的,无法完美兼得(首先要理解这两个问题产生的场景)

  • 【主进程阻塞】:执行写操作命令和记录日志这两个操作都是在主进程完成的,这两个是一个同步的操作。如果在执行记录日志时发生了IO阻塞也就会导致主进程阻塞
  • 【数据丢失】:在数据还没有及时写回到硬盘时,如果这个过程中Redis发生宕机,就会发生数据丢失

针对这两个问题,3种策略的执行效果分析如下

  • Always:可最大程度保证数据不丢失,但是每执行一次写操作命令就同步写回,不可避免对主进程造成影响
  • No:由操作系统决定何时将AOF日志内容写回硬盘,相对于Always策略而言其性能较好,但由于操作系统写回硬盘的时机不可预知,一旦服务器宕机,就会丢失不定数量的数据
  • Everysec:是一种折中方案,它避免了Always策略的性能开销,一定程度上也比No策略更能避免数据丢失。与此同时,每秒回写则意味着如果服务器发生宕机时,上一秒的写操作命令还没写回硬盘,则这部分数据会丢失

Redis 的3种写回硬盘的策略 对比

​ 对于这三种写回硬盘的策略场景选择,主要从【高可靠】和【高性能】两者之间进行择选

写回策略写回时机优点缺点
Always同步写回可靠性高,最大程度保证数据不丢失每个写命令都要写回硬盘,性能开销大
Everysec每秒写回性能适中宕机时会丢失1s内的数据
No由操作系统控制写回性能好可靠性较差,宕机时丢失数据不定量

写回策略的实现

​ 深入源码分析,实际上这三种策略只是在控制fsync()函数的调用时机。当应用程序向文件写入数据时,内核通常先将数据复制到内核缓冲区中,然后排入队列,然后由内核决定何时写入硬盘

image-20240718184722730

  • Always策略:每次写入AOF文件数据之后,执行fsync()函数
  • Everysec策略:创建一个异步任务来执行fsync()函数
  • No策略:永不执行fsync()函数,由操作系统决定写入时机

3.AOF 重写机制

重写机制

​ AOF 日志是一个文件,随着执行的写操作命令越来越多,文件的大小会越来越大。如果当 AOF 日志文件过大就会带来性能问题,比如重启 Redis 后,需要读 AOF 文件的内容以恢复数据,如果文件过大,整个恢复的过程就会很慢。

​ 所以,Redis 为了避免 AOF 文件越写越大,提供了 AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。

AOF 重写机制是在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件(可以理解为新建一个文件记录当前数据库的最新键值对信息,然后用这个新的AOF文件覆盖旧文件以达到压缩的目的)

​ 可以结合案例分析:在没有使用重写机制前,假设前后执行了「set name noob」和「set name noob007」这两个命令的话,就会将这两个命令记录到 AOF 文件,如果同一个key被修改多次,那么相应也会记录到对应的AOF日志。

​ 在引入AOF 重写机制后,就会读取每个key最新的value(最新的键值对信息),然后用一条命令例如【set key value】记录到新的AOF文件中。在重写工作完成后,就会用这个新的AOF文件覆盖现有的AOF文件,以此达到压缩AOF文件的目的。因为此处关心的是最新的键值对信息,对于key中间修改的旧命令是没有必要记录了,所以只用一条最新的键值记录来记录某个key。

image-20240718191347417

​ 重写机制的设计核心在于,尽管某个键值被多次修改,最终只需要记录最新的记录将其转换为命令写入到新AOF文件中,以此减少AOF文件中命令数量,进而达到压缩文件的目的。

​ 此处“先写新文件后覆盖”的设计点在于尽量减少重写对现有文件的影响。因为 如果AOF 重写过程中失败了,现有的 AOF 文件就会造成污染,可能无法用于恢复使用。所以 AOF 重写过程,先重写到新的 AOF 文件,重写失败的话,就直接删除这个文件就好,不会对现有的 AOF 文件造成影响。

后台重写

(1)AOF 重写流程分析

​ 写入 AOF 日志的操作虽然是在主进程完成的,因为它写入的内容不多,所以一般不太影响命令的操作。但是在触发 AOF 重写时,比如当 AOF 文件大于 64M 时,就会对 AOF 文件进行重写,重写是需要读取所有缓存的键值对数据,并为每个键值对生成一条命令,然后将其写入到新的 AOF 文件,重写完成后需将现在的 AOF 文件替换掉。这个过程其实是很耗时的,所以重写的操作不能放在主进程里

​ Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的,这么做可以达到两个好处:

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

为何使用子进程而不使用线程?

​ 此处使用子进程而不是线程,因为如果是使用线程,多线程之间会共享内存,在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能。

​ 而使用子进程,在创建子进程时,父子进程是共享内存数据的,不过这个共享的内存只能以只读的方式,而当父子进程任意一方修改了该共享内存,就会发生「写时复制」,于是父子进程就有了独立的数据副本,不用加锁来保证数据安全,性能也相对较好

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

​ 主进程在通过 fork 系统调用生成 bgrewriteaof 子进程时,操作系统会把主进程的「页表」复制一份给子进程,这个页表记录着虚拟地址和物理地址映射关系,而不会复制物理内存,即两者的虚拟空间不同,但其对应的物理空间是同一个。如此一来,子进程共享了父进程的物理内存数据了,进而节约物理内存资源**,页表对应的页表项的属性会标记该物理内存的权限为只读**。

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

image-20240718193306898

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

​ 此外,操作系统复制父进程页表的时候,父进程也是阻塞中的,不过页表的大小相比实际的物理内存小很多,所以通常复制页表的过程是比较快的不过,如果父进程的内存数据非常大,那自然页表也会很大,这时父进程在通过 fork 创建子进程的时候,阻塞的时间也越久

父进程阻塞问题?

基于上述分析,会有两个阶段会导致阻塞父进程:

  • 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
  • 创建完子进程之后,如果子进程或者父进程修改了共享数据,就会发生写时复制,这期间会拷贝物理内存,如果内存越大,自然阻塞的时间也越长;

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

​ 但是子进程重写过程中,主进程依然可以正常处理命令。如果此时主进程修改了已经存在 key-value,就会发生写时复制,此处只会复制主进程修改的物理内存数据,没修改的物理内存还是与子进程共享的。所以如果这个阶段修改的是一个 bigkey,也就是数据量比较大的 key-value 的时候,复制的物理内存数据的过程就会比较耗时,有阻塞主进程的风险。

重写过程中主进程修改了已存在的key-value,则此时key-value数据在子进程的内存数据和主进程内存数据不一致该如何处理?

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

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

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

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

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

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

​ 信号函数执行完后,主进程就可以继续像往常一样处理命令了。在整个 AOF 后台重写过程中,除了发生写时复制会对主进程造成阻塞,还有信号处理函数执行时也会对主进程造成阻塞,在其他时候,AOF 后台重写都不会阻塞主进程

image-20240718195709473

(2)AOF 重写流程核心(一次拷贝,两处日志

​ 重写的核心在于读取最新的记录,然后写入新的AOF日志中,所有重写操作完成之后用新AOF日志覆盖掉原有的AOF日志

  • 一次拷贝:为了减少对主流程的影响,重写发生时,主进程会fork一个子进程,子进程和主进程共享Redis物理内存,让子进程将这些Redis数据写入重写日志
  • 两处日志:重写发生时,当有新的写入命令执行,主进程会分别写入【AOF缓冲】和【AOF重写缓冲】
    • 【AOF缓冲】用于保证此时发生宕机时,原来的AOF日志也是完整的,可用于恢复
    • 【AOF重写缓冲】用于保证新的AOF文件,不会丢失最新的写入操作

RDB

1.RDB概念核心

如何使用RDB?

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

  • 执行 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程
  • 执行 bgsave 命令,会创建一个子进程来生成 RDB 文件,可以避免主线程的阻塞

​ RDB 文件的加载工作是在服务器启动时自动执行的,Redis 并没有提供专门用于加载 RDB 文件的命令。Redis 还可以通过配置文件的选项来实现每隔一段时间自动执行一次 bgsave 命令,默认会提供以下配置:

save 900 1
save 300 10
save 60 10000

​ 别看选项名叫 save,实际上执行的是 bgsave 命令,也就是会创建子进程来生成 RDB 快照文件。只要满足上面条件的任意一个,就会执行 bgsave,其含义分别是:

  • 900 秒之内,对数据库进行了至少 1 次修改;
  • 300 秒之内,对数据库进行了至少 10 次修改;
  • 60 秒之内,对数据库进行了至少 10000 次修改;
# RDB 存储配置(配置存储路径和存储文件名称)
dbfilename dump.rdb
dir /User/xxx/code/redis

​ Redis 的快照是全量快照,即每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。可以认为执行快照是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响。如果频率太低,服务器故障时,丢失的数据会更多。

​ 通常可能设置至少 5 分钟才保存一次快照,这时如果 Redis 出现宕机等情况,则意味着最多可能丢失 5 分钟数据。

​ RDB 快照的缺点:在服务器发生故障时,丢失的数据会比 AOF 持久化的方式更多,因为 RDB 快照是全量快照的方式,因此执行的频率不能太频繁,否则会影响 Redis 性能,而 AOF 日志可以以秒级的方式记录操作命令,所以丢失的数据就相对更少

Redis时通过fork一个子进程的方式来进行RDB,配合写时复制技术(相当于异步执行),和主进程互不干扰,将对执行流程的影响降到最低

什么时候进行持久化?

​ Redis 持久化会在下面几种情况进行:

方式1:主动执行命令save

# 执行bgsave指令
save
# output
Ok

方式2:主动执行命令bgsave

# 执行bgsave指令
bgsave
# output
Background saving started

方式3:达到持久化阈值

​ Redis 可以配置持久化策略,达到策略就出发持久化。比较推荐的是后台save,尽可能减少对主流程的影响,当达到阈值之后,由周期函数出发持久化

方式4:程序正常关闭的时候执行

​ 在关闭时,Redis会启动一次阻塞式持久化,以记录更全的数据

执行快照时,数据能被修改吗?

​ 对于bgsave场景:执行 bgsave 过程中,由于是交给子进程来构建 RDB 文件,主线程还是可以继续工作的,此时主线程可以修改数据吗?如果不可以修改数据的话,那这样性能一下就降低了很多。如果可以修改数据,又是如何做到呢?=》执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的

​ 关键的技术就在于**写时复制技术(Copy-On-Write, COW)。**执行 bgsave 命令的时候,会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个(写时复制这块概念可以参考上述AOF概念梳理中的后台重写机制进行复盘)

​ 创建 bgsave 子进程后,由于共享父进程的所有内存数据,于是就可以直接读取主线程(父进程)里的内存数据,并将数据写入到 RDB 文件。当主线程(父进程)对这些共享的内存数据也都是只读操作,那么,主线程(父进程)和 bgsave 子进程相互不影响。

​ 但是,如果主线程(父进程)要修改共享数据里的某一块数据(比如键值对 A)时,就会发生写时复制,于是这块数据的物理内存就会被复制一份(键值对 A',然后主线程在这个数据副本(键值对 A')进行修改操作。与此同时,bgsave 子进程可以继续把原来的数据(键值对 A)写入到 RDB 文件

bgsave 的执行过程

​ Redis 使用 bgsave 对当前内存中的所有数据做快照,这个操作是由 bgsave 子进程在后台完成的,执行时不会阻塞主线程,这就使得主线程同时可以修改数据。

​ 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重写基础上做了一些改动

  • 使用RDB持久化函数将内存数据写入到AOF文件中(数据格式是RDB格式)
  • 重写期间新写入的命令会追加到新的AOF文件中(数据格式是AOF格式)
  • 此时新的AOF文件就是由RDB格式和AOF格式组合成的日志文件

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

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

​ 混合使用的优点在于:重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,加载的时候速度会很快。加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,此处的AOF内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3