Redis(三)持久化

2024 年 4 月 29 日 星期一(已编辑)
/
87
这篇文章上次修改于 2024 年 7 月 22 日 星期一,可能部分内容已经不适用,如有疑问可询问作者。

Redis(三)持久化

使用缓存的时候,我们经常需要对内存中的数据进行持久化也就是将内存中的数据写入到硬盘中。大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了做数据同步(比如 Redis 集群的主从节点通过 RDB 文件同步数据)。

Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持 3 种持久化方式:

  • 快照(snapshotting,RDB)
  • 只追加文件(append-only file,AOF)
  • RDB 和 AOF 的混合持久化(Redis 4.0 新增)

RDB 持久化

什么是 RDB 持久化

RDB持久化过程中,Redis会生成一个压缩过的二进制文件,该文件包含了某个时间点上数据库中所有的键值对。

快照持久化是 Redis 默认采用的持久化方式,在 redis.conf 配置文件中默认有此下配置:

save 900 1
save 300 10
save 60 10000
  • 900 秒之内,对数据库进行了至少 1 次修改;
  • 300 秒之内,对数据库进行了至少 10 次修改;
  • 60 秒之内,对数据库进行了至少 10000 次修改。

这是自动化持久化配置,满足上面条件就会自动执行 bgsave,创建子进程来生成 RDB 快照文件。

Redis默认是bgsave,还可以选择使用save

save和bgsave区别:

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

RDB优点✨✨✨

  • RDB 文件存储的内容是经过压缩的二进制数据, 保存着某个时间点的数据集,文件很小,适合做数据的备份,灾难恢复;
  • 使用 RDB 文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快。而 AOF 则需要依次执行每个写命令,速度非常慢。与 AOF 相比,恢复大数据集的时候,RDB 速度更快。

RDB缺点✨✨✨

  • Redis 的快照是全量快照,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中,执行的频率不能太频繁,否则会影响 Redis 性能,因此,在服务器发生故障时,丢失的数据会比 AOF 持久化的方式更多

  • bgsave过程中,主线程修改的数据可能丢失;

    • 执行 bgsave 命令的时候,会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个;
    • 如果主线程(父进程)要修改共享数据里的某一块数据时,就会发生写时复制(copy-on-write),于是这块数据的物理内存就会被复制一份,然后主线程在这个数据副本进行修改操作。与此同时,bgsave 子进程可以继续把原来的数据写入到 RDB 文件
    • 而主线程刚修改的数据,是没办法在这一时间写入 RDB 文件的,只能交由下一次的 bgsave 快照。
  • RDB 文件是以特定的二进制格式保存的,并且在 Redis 版本演进中有多个版本的 RDB,所以存在老版本的 Redis 服务不兼容新版本的 RDB 格式的问题。

AOF 持久化

什么是 AOF 持久化?

Redis 每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里,然后重启 Redis 的时候,先去读取这个文件里的命令,并且执行它,就相当于恢复了缓存数据。这种保存写操作命令到日志的持久化方式,就是 Redis 里的 AOF(Append Only File) 持久化功能,注意只会记录写操作命令,读操作命令是不会被记录的,因为没意义。

默认情况下 Redis 没有开启 AOF(append only file)方式的持久化(Redis 6.0 之后已经默认是开启了),可以通过 appendonly 参数开启:

appendonly yes

AOF 为什么是在执行完命令之后记录日志?

  • 避免额外的检查开销,AOF 记录日志不会对命令进行语法检查;
  • 在命令执行完之后再记录,不会阻塞当前的命令执行。

这样也带来了风险:

  • 如果刚执行完命令 Redis 就宕机会导致对应的修改丢失;
  • 可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)。

AOF 持久化功能怎么实现?✨

AOF 持久化功能的实现可以简单分为 5 步:

  1. 命令追加(append):所有的写命令会追加到 AOF 缓冲区中。
  2. 文件写入(write):将 AOF 缓冲区的数据写入到 AOF 文件中。这一步需要调用write函数(系统调用),write将数据写入到了系统内核缓冲区之后直接返回了(延迟写)。注意!!!此时并没有同步到磁盘。
  3. 文件同步(fsync):AOF 缓冲区根据对应的持久化方式( fsync 策略)向硬盘做同步操作。这一步需要调用 fsync 函数(系统调用), fsync 针对单个文件操作,对其进行强制硬盘同步,fsync 将阻塞直到写入磁盘完成后返回,保证了数据持久化。
  4. 文件重写(rewrite):随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
  5. 重启加载(load):当 Redis 重启时,可以加载 AOF 文件进行数据恢复。

AOF持久化方式

在 Redis 的配置文件中存在三种不同的 AOF 持久化方式( fsync策略),它们分别是:

  1. appendfsync always:主线程调用 write 执行写操作后,后台线程( aof_fsync 线程)立即会调用 fsync 函数同步 AOF 文件(刷盘),fsync 完成后线程返回,这样会严重降低 Redis 的性能(write+ fsync)。
  2. appendfsync everysec:主线程调用 write 执行写操作后立即返回,由后台线程( aof_fsync 线程)每秒钟调用 fsync 函数(系统调用)同步一次 AOF 文件(write+fsyncfsync间隔为 1 秒)
  3. appendfsync no:主线程调用 write 执行写操作后立即返回,让操作系统决定何时进行同步,Linux 下一般为 30 秒一次(write但不fsyncfsync 的时机由操作系统决定)。

可以看出:这 3 种持久化方式的主要区别在于 fsync 同步 AOF 文件的时机(刷盘)

AOF重写机制✨✨

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

AOF 重写机制是在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」(注意不是直接覆盖,如果重写失败直接覆盖可能污染现有的 AOF 文件),等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。

由于 AOF 重写会进行大量的写入操作,为了避免对 Redis 正常处理命令请求造成影响,Redis 将 AOF 重写程序放到子进程里执行。

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

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

当子进程完成 AOF 重写工作后,会向主进程发送一条信号,信号是进程间通讯的一种方式,且是异步的。

主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作:

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

信号函数执行完后,主进程就可以继续像往常一样处理命令了。

AOF 重写程序放到子进程的好处?

  • 子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程;
  • 子进程带有主进程的数据副本,可以使用写时复制,节约物理内存资源。

AOF重写为什么使用子进程而不是线程?

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

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

详细解释写时复制

主进程在通过 fork 系统调用生成 bgrewriteaof 子进程时,操作系统会把主进程的「页表」复制一份给子进程,这个页表记录着虚拟地址和物理地址映射关系,而不会复制物理内存,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。

这样一来,子进程就共享了父进程的物理内存数据了,这样能够节约物理内存资源,页表对应的页表项的属性会标记该物理内存的权限为只读

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

AOF重写会阻塞父进程吗?

  • 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
  • 创建完子进程后,如果子进程或者父进程修改了共享数据,就会发生写时复制,这期间会拷贝物理内存,如果内存越大,自然阻塞的时间也越长;
  • 子进程完成 AOF 重写工作后,调用的信号处理函数执行时也会对主进程造成阻塞。

AOF优点✨✨✨

  • AOF 支持秒级数据丢失(取决 fsync 策略,如果是 everysec,最多丢失 1 秒的数据),仅仅是追加命令到 AOF 文件,操作轻量
  • AOF 以一种易于理解和解析的格式包含所有操作的日志。你可以轻松地导出 AOF 文件进行分析,你也可以直接操作 AOF 文件来解决一些问题。(可读性好

AOF缺点✨✨✨

  • AOF文件大;

  • AOF 恢复需要依次执行每个写命令,速度非常慢。

混合持久化

RDB 和 AOF 各有优势,但是选择上:

  • Redis 保存的数据丢失一些也没什么影响的话,可以选择使用 RDB;

  • 不建议单独使用 AOF,因为时不时地创建一个 RDB 快照可以进行数据库备份、更快的重启以及解决 AOF 引擎错误;

  • 如果保存的数据要求安全性比较高的话,建议同时开启 RDB 和 AOF 持久化或者开启 RDB 和 AOF 混合持久化。

Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。

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

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

优点:

  • 重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快

  • 加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失

缺点:

  • AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

问题自测

Redis持久化时对过期键会如何处理

RDB分为生成阶段和加载阶段,

  • 生成阶段会对key进行过期检查,过期的key不会保存到RDB文件中;
  • 加载阶段看服务器是主服务器还是从服务器,
    • 如果是主服务器,在载入RDB文件时,程序会对文件中保存的键进行检查,过期键不会被载入到数据库中;
    • 如果从服务器,在载入RDB文件时,不论键是否过期都会被载入到数据库中。但由于主从服务器在进行数据同步时,从服务器的数据会被清空。过期键对载入RDB文件的从服务器也不会造成影响。

AOF文件分写入阶段和AOF重写阶段。

  • 写入阶段如果数据库某个过期键还没被删除,AOF文件会保留此过期键,当此过期键被删除后,Redis会向AOF文件追加一条DEL命令来显式地删除该键值。
  • 重写阶段会对Redis中的键值对进行检查,已过期的键不会被保存到重写后的AOF文件中。

Redis主从模式中,对过期键会如何处理

从库不会进行过期扫描,即使从库中的key过期了,如果有客户端访问从库时,依然可以得到key对应的值。

从库的过期键处理依靠主服务器控制,主库在key到期时,会在AOF文件里增加一条deI指令,同步到所有的从库,从库通过执行这条del指令来删除过期的key。

RDB和AOF本质区别是什么

如果是讲区别可以从文件类型,文件恢复速度,安全性来进行回答,但本质区别就是RDB是使用快照进行持久化,AOF是日志。其他可以列举的区别点都是能从快照与日志中的对比找出的

  • 文件类型:RDB生成的是二进制文件(快照),AOF生成的是文本文件(追加日志)
  • 安全性:缓存宕机时,RDB容易丢失较多的数据,AOF根据策略决定(默认的everysec可以保证最多有一秒的丢失)
  • 文件恢复速度:由于RDB是二进制文件,所以恢复速度也比AOF更快
  • 操作的开销:每一次RDB保存都是一次全量保存,操作比较重,通常设置至少5分钟保存一次数据。而AOF的刷盘是一次追加操作,操作比较轻,通常设置策略为每一秒进行一次刷盘

RDB持久化的触发时机

  • 调用save或者bgsave命令
  • 根据我们配置周期进行
  • Redis关闭之前
  • 主从全量复制发送RDB文件等。

简单描述AOF重写流程

  1. 当AOF重写触发那一刻,主进程就会fork出一个子进程,然后这个子进程读取Redis DB中的数据,以字符串命令的格式写入到新AOF文件中

  2. 如果这个时候Redis接收到了新的写入命令,那么主进程会将这些"增量数据"写入到AOF重写缓冲区中(也会写入AOF 缓冲区,两个都写保证都是可用AOF文件,重写成功才会用新的替换)

  3. 在子进程将数据都写入到新AOF文件后,主进程会通过管道将AOF重写缓冲区里面的数据发送给子进程,子进程再将这一份数据追加到新AOF文件中,保证新AOF文件的完整性

AOF重写你觉得有什么不足之处么

我认为主要有3点不足之处:

  1. 额外的CPU开销:
    • 在重写时,主进程需要将新的写入数据写入到AOF重写缓冲(aof_rewrite_buf)
    • 主进程需要通过管道向子进程发送AOF重写缓冲的数据
    • 子进程还需要将这些数据写入到新的AOF日志中
  2. 额外的内存开销:在重写时,AOF缓冲和AOF重写缓冲中的数据都是一样的(浪费了一份)
  3. 额外的磁盘开销:在重写时,AOF缓冲需要刷入日的AOF日志,AOF重写缓冲也需要刷入到新的AOF日志,导致在重写时磁盘多占一份数据

针对AOF重写的不足,有什么优化思路呢

改进之处:

  • 在Redis7.0版本,对AOF重写作出了优化,提出了MP-AOF方案,原来的AOF重写缓冲被移除,AOF日志也分成了Base AOF日志,Incr AOF日志MP-AOF: Multi Part AOF one BASE AOF + many INCR AOFs
  • Base AOF日志记录重写之前的命令;Incr AOF日志记录重写时新的写入命令(正常AOF刷盘的时候写的是Incr AOF)
  • 当重写发生时,主进程fork出一个子进程,对Base AOF日志进行重写(将当前内存数据写入到新的Base AOF日志);如果此时有新的写入命令,会由主进程写入到aof_buf,再将缓冲数据刷入新的Incr AOF日志。这样新的Incr AOF日志+新的Base AOF日志就构成了完整的新的AOF日志
  • 子进程重写结束时,主进程会负责更新manifest文件,将新生成的BASE AOF和INCR AOF信息加进清单并将之前的BASE AOF和INCR AOF标记为HISTORY。(mianfest用于追踪管理AOF文件)
  • 这些HISTORY文件默认会被Redis异步删除(unlink),一旦manifest文件更新完成,就代表着整个AOFRW流程结束

总结:

其实在Redis7.0版本,就使用MP-AOF方案对AOF重写做了优化,核心其实就是去掉原来的重写缓冲,同时将AOF日志拆分为Base AOF日志,Incr AOF日志,由manifest来管理。重写时,还是开一个子进程,对Base AOF日志进行重写,但是新命令会往新的Incr AOF日志写,Incr AOF日志+新的Base AOF日志就构成了完整的新的AOF日志。

Redis大Key对持久化的影响

当 AOF 写回策略配置了 Always 策略,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的。

AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 fork() 函数创建一个子进程来处理任务。会有两个阶段会导致阻塞父进程(主线程):

  • 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
  • 创建完子进程后,如果父进程修改了共享数据中的大 Key,就会发生写时复制,这期间会拷贝物理内存,由于大 Key 占用的物理内存会很大,那么在复制物理内存这一过程,就会比较耗时,所以有可能会阻塞父进程。

大 key 除了会影响持久化之外,还会有以下的影响:

  • 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
  • 引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
  • 阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
  • 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大。

如何避免大 Key 呢?

最好在设计阶段,就把大 key 拆分成一个一个小 key。或者,定时检查 Redis 是否存在大 key ,如果该大 key 是可以删除的,不要使用 DEL 命令删除,因为该命令删除过程会阻塞主线程,而是用 unlink 命令(Redis 4.0+)删除大 key,因为该命令的删除过程是异步的,不会阻塞主线程。

参考:

小林coding (xiaolincoding.com) JavaGuide

Redis 设计与实现 — Redis 设计与实现 (redisbook.com)

写时复制(COW)详解-CSDN博客

Redis持久化RDB和AOF优缺点是什么,怎么实现的?我应该用哪一个? - 架构师技术栈 - SegmentFault 思否

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...