Redis-基础篇-④持久化
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来进行加载。流程参考下图所示
问题思考:如果同时开启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持久化文件名称
Redis 是先执行写命令后记录命令到AOF日志,这种设计思路有两个好处:
- 避免额外的检查开销:如果是先记录日志后执行写命令,如果当前命令有问题而又没有进行检查的话,恢复数据的时候就会异常。因此先确保写命令执行成功后记录到日志,进而避免额外的检查开销
- 不会阻塞当前写操作命令的执行
当然,AOF持久化功能也有潜在的风险:进一步理解分析,这两个潜在的风险都有一个共性:和AOF日志写回硬盘的时机有关
- 存在数据丢失风险:执行写命令和记录AOF日志是两个过程,当Redis还没来得及将命令追加到日志就发生了宕机,那么这部分数据就会有丢失风险
- 可能会给下一个命令带来阻塞风险:执行命令和记录日志两个操作都是在主进程中执行的,这两个操作是同步的。虽然是先执行命令后记录日志,只是不会阻塞当前命令,但可能会阻塞下一个命令的执行。如果在将日志内容写入到硬盘时,服务器的硬盘的 I/O 压力太大,就会导致写硬盘的速度很慢,进而导致阻塞,致使后续的命令无法执行
2.写回策略
Redis 写入 AOF 日志 过程分析
Redis 写回AOF日志的过程分析如下:
执行写操作命令=》追加命令到server.aof_buf缓冲区=》调用write()将缓冲区内容写到AOF文件(拷贝到内核缓冲区page cache)=》内核将内核缓冲区数据写入硬盘
- 【步骤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()
函数的调用时机。当应用程序向文件写入数据时,内核通常先将数据复制到内核缓冲区中,然后排入队列,然后由内核决定何时写入硬盘
- 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。
重写机制的设计核心在于,尽管某个键值被多次修改,最终只需要记录最新的记录将其转换为命令写入到新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)」
写时复制顾名思义,在发生写操作的时候,操作系统才会去复制物理内存,这样是为了防止 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 后台重写都不会阻塞主进程
(2)AOF 重写流程核心(一次拷贝,两处日志)
重写的核心在于读取最新的记录,然后写入新的AOF日志中,所有重写操作完成之后用新AOF日志覆盖掉原有的AOF日志
- 一次拷贝:为了减少对主流程的影响,重写发生时,主进程会fork一个子进程,子进程和主进程共享Redis物理内存,让子进程将这些Redis数据写入重写日志
- 两处日志:重写发生时,当有新的写入命令执行,主进程会分别写入【AOF缓冲】和【AOF重写缓冲】
- 【AOF缓冲】用于保证此时发生宕机时,原来的AOF日志也是完整的,可用于恢复
- 【AOF重写缓冲】用于保证新的AOF文件,不会丢失最新的写入操作
RDB
1.RDB概念核心
如何使用RDB?
Redis 提供了两个命令来生成 RDB 文件,分别是 save
和 bgsave
,其区别就在于是否在「主线程」里执行:
- 执行 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 期间,主线程处理的操作命令,可以使得数据更少的丢失