Redis(数据存储在内存中)支持 RDB 和 AOF 两种持久化(和 MySQL 里的持久性是一回事,把数据存储在硬盘上,重启进程 / 主机后数据仍然存在 —— 持久;把数据存储在内存上,重启进程 / 主机后数据消失 —— 不持久)机制,持久化功能有效地避免因进程退出造成数据丢失问题,当下次重启时利用之前持久化的文件即可实现数据恢复。
Redis 为了保证速度快,数据肯定还得存储在内存中。但是为了持久化,数据还得想办法存储在硬盘上。所以,最后决定在内存中和硬盘上都存储数据,这样的两份数据在理论上(实际上可能存在一个小的概率有差异,具体取决于如何进行持久化)是完全相同的。当要插入一个新的数据时,就需要把这个数据同时写入到内存和硬盘。当查询某个数据时,直接从内存中读取,硬盘的数据只是在 Redis 重启时,用来恢复内存中的数据。代价就是消耗了更多的空间,同一份数据存储了两遍,但毕竟硬盘价格便宜,开销不会带来太大的成本。而且实际上具体怎么写硬盘还有不同的策略,是可以保证整体的效率足够高的。
假设我自己的电脑上有很多的学习资料,虽然现在是把这些学习资料保存在硬盘上(持久化保存),但是万一我的电脑出现故障(相较于 CPU、显卡、内存来说,硬盘是最容易出问题的,尤其是机械硬盘)。可以拿另一块移动硬盘来作为备份用的硬盘:
- 定期备份(每个月将电脑硬盘上的学习资料整体的备份到这个备份盘中)—— RDB
- 实时备份(只要下载了一个新的学习资料,就立即把这份学习资料往备份盘中拷贝一份)—— AOF
一、RDB
RDB(Redis DataBase)持久化是把定期的将当前的进程数据生成快照保存到硬盘的过程,触发 RDB 持久化过程分为手动触发和自动触发。
1、触发机制
手动触发分别对应 save 和 bgsave 命令:
- save 命令:阻塞当前 Redis 服务器,直到 RDB 过程完成为止,对于内存比较大的实例造成长时间阻塞。(一半不建议使用)
- bgsave(background save)命令:Redis 进程执行 fork 操作创建子进程,RDB 持久化过程由子进程负责,完成后自动结束。阻塞只发生在 fork 阶段,一般时间很短。不会影响 Redis 服务器处理其他客户端的请求和命令。(此处 Redis 使用的是 “多进程” 的方式来完成的并发编程,来完成 bgsave 的实现)
Redis 内部的所有涉及 RDB 的操作都采用类似 bgsave 的方式。
如果插入新的 key,而此时不手动执行 bgsave,直接重新启动 Redis 服务器,那么刚刚插入的数据在重启之后仍然存在。所以说,Redis 生成快照操作不仅仅是手动执行命令才触发,也可以自动触发,也就是下面的第 3 点。
除了手动触发之外,Redis 运行自动触发 RDB 持久化机制,这个触发机制才是在实战中有价值的。
- 使用 save 配置。如 "save m n" 表示 m 秒内数据集发生了 n 次修改,自动 RDB 持久化。
- 从节点进行全量复制(主从复制)操作时,主节点自动进行 RDB 持久化生成快照,随后将 RDB 快照文件内容发送给从节点。
- 如果执行 shutdown 命令(service redis-server restart,正常关闭)关闭 Redis 时,或者通过正常流程重新启动 Redis 服务器,那么此时 Redis 服务器会在退出时自动执行 RDB 持久化。但如果是异常重启(kill -9 或者服务器掉电),那么此时 Redis 服务器来不及生成 rdb,内存中尚未保存到快照中的数据,就会随着重启而丢失。
并不是说 Redis 客户端这边插入了数据,rdb 文件中的数据就会立即更新的。插入几个键值对后,没有运行手动触发的命令,也达不到自动触发的条件,那么就不会更新。
如果修改成如下内容:
对于 Redis 来说,配置文件发生修改后,一定要重新启动服务器才能生效。(当然,如果想要立即生效,也可以通过命令的方式进行修改)
同时满足两个条件即可触发快照的生成:
如果是修改成以下内容:
那么将会关闭自动生成快照这个功能。
如果把 rdb 文件故意改坏会怎么样?
手动把 rdb 文件内容改坏,如果是通过 service redis-server restart 重启,就会在 Redis 服务器退出时重新生成 rdb 快照,那么刚才改坏的文件就会被替换掉。
而如果是通过 kill 进程的方式,再重新启动 Redis 服务器,此时 rdb 文件就还是错的。但看起来 Redis 好像没有受到什么影响,还是能正常启动,能正确获取到 key。那是因为刚才修改的位置应该正好是文件的末尾,对前面的内容没有什么影响。但如果是修改了中间位置的内容,那么 Redis 服务器就启动不了了。
此时,Redis 服务器挂了,可以看看 Redis 日志,了解一下发生了什么。
打开该文件,可以看到:
也就是在 rdb 恢复数据的过程中出现了问题。
rdb 文件是二进制的,如果直接把坏了的 rdb 文件交给 Redis 服务器去使用,那么得到的结果是不可预期的。所以,Redis 也提供了 rdb 文件的检查工具,可以先通过检查工具来检查一下 rdb 文件格式是否符合要求。
5.0 版本中,检查工具和 Redis 服务器是同一个可执行程序,可以在运行时加入不同的选项,从而使用其中不同的功能。运行时加入 rdb 文件作为命令行参数,那么此时就是以检查工具的方式来运行,不会真的启动 Redis 服务器。
2、流程说明
bgsave 是主流的 RDB 持久化方式,根据下图来了解它的运作流程。
bgsave 命令的运作流程:
- 执行 bgsave 命令,Redis 父进程(Redis 服务器)判断当前进是否存在其他正在执行的子进程,比如:RDB / AOF 子进程,如果存在 bgsave 命令直接返回。
- 如果没有其它的工作子进程,父进程通过执行 fork 这样的系统调用来创建一个子进程(该场景中的绝大部分内存数据是不需要改变的,所以在短时间内父进程中不会有大批的内存数据变化,因此子进程的 “写时拷贝” 并不会触发很多次),fork 过程中父进程会阻塞,通过 info stats 命令查看 latest_fork_usec 选项,可以获取最近一次 fork 操作的耗时,单位为微秒。
- 父进程 fork 完成后,bgsave 命令返回 "Background saving started" 信息,并不再阻塞父进程,可以继续响应其他命令。
- 子进程创建 RDB 文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。执行 lastsave 命令可以获取最后一次生成 RDB 的时间,对应 info 统计的 rdb_last_save_time 选项。
- 进程发送信号给父进程表示完成,父进程更新统计信息,子进程就可以结束销毁了。
bgsave 操作流程是创建子进程,子进程完成持久化操作(持久化速度太快了(数据少),难以观察到子进程),持久化会把数据写入到新的文件中,然后使用新的文件替换旧的文件(这个容易观察)。可以通过 Linux 的 stat 命令来查看文件的 inode 编号:
这两个文件不再是同一个文件了,只不过文件内容是一样的。inode 编号就相当于文件的身份标识。如果是直接使用 save 命令,那么此时是不会触发子进程和文件替换逻辑的,会直接在当前进程中往刚才的同一文件中写入数据。
文件系统典型的组织方式(ext4)主要是把整个文件系统分成三个大的部分:
- 超级块(存放一些管理信息)
- inode 区(存放 inode 节点,每个文件都会分配一个 inode 数据结构,包含文件的各种元数据)
- block 区(存放文件的数据内容)
3、RDB 文件的处理
- 保存:RDB 文件(把内存中的数据以压缩(需要消耗一定的 cpu 资源,但是能节省存储空间)的形式保存到这个二进制文件中)保存在 dir 配置指定的目录(默认 /var/lib/redis/)下,文件名通过 dbfilename 配置(默认 dump.rdb)指定。可以通过执行 config set dir {newDir} 和 config set dbfilename {newFilename} 运行期间动态执行,当下次运行时 RDB 文件会保存到新目录。
- 压缩:Redis 默认采用 LZF 算法对生成的 RDB 文件做压缩处理,压缩后的文件远远小于内存大小,默认开启,可以通过参数 config set rdbcompression {yes|no} 动态修改。
虽然压缩 RDB 会消耗 CPU,但可以大幅降低文件的体积,方便保存到硬盘或通过网络发送到从节点,因此建议开启。
- 校验:如果 Redis 启动时加载到损坏的 RDB 文件会拒绝启动。这时可以使用 Redis 提供的 redis-check-dump 工具检测 RDB 文件并获取对应的错误报告。
当执行生成 rdb 镜像操作时,此时就会把要生成的快照数据先保存到一个临时文件中。当这个快照生成完毕后,再删除之前的 rdb 文件,把新生成的临时 rdb 文件名改成刚才的 dump.rdb。也就保证了,rdb 文件自始至终只有一个。
执行 flushall 命令会自动情况 rdb 文件。
4、RDB 的优缺点
(1)优点
- RDB 是⼀个紧凑压缩的二进制文件,代表 Redis 在某个时间点上的数据快照。非常适用于备份,全量复制等场景。比如每 6 小时执行 bgsave 备份,并把 RDB 文件复制到远程机器或者文件系统中(如 hdfs)用于灾备。
- Redis 加载 RDB 恢复数据远远快于 AOF 的方式。 (二进制的方式则直接将数据读取到内存中,按照字节的格式取出来放到结构体 / 对象即可,文本方式组织数据则需要进行一系列的字符串切分操作)
(2)缺点
- RDB 方式数据没办法做到实时持久化 / 秒级持久化(在两次生成快照之间,实时数据可能会随着重启而丢失),这就导致快照里的数据和当前实时的数据情况可能存在偏差。因为 bgsave 每次运行都要执行 fork 创建子进程,属于重量级操作,频繁执行成本过高。
- RDB 文件使用特定二进制格式保存,Redis 版本演进过程中有多个 RDB 版本,兼容性可能有风险(旧版本的 Redis 的 rdb 文件放到新版本的 Redis 中不一定能实现。但一般来说,实际工作中 Redis 版本都是统一的,实在不行也可以通过写一个程序的方式来直接遍历旧的 Redis 中的所有 key,把数据取出来插入到新的 Redis 服务器中即可)。
二、AOF
AOF(Append Only File)持久化(类似于 MySQL 中的 binlog,会把用户的每个操作都记录到文件中):以独立日志的方式记录每次写命令,重启时再重新执行 AOF 文件中的命令达到恢复数据的目的。AOF 的主要作用是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式。
Redis 重新启动时,又读 RDB、又读 AOF,到底以哪个为准呢?
当开启 AOF 时,rdb 就不生效了,启动的时候就不再读取 rdb 文件内容。
1、使用 AOF
开启 AOF 功能需要设置配置:appendonly yes,默认是关闭状态。AOF 文件名通过 appendfilename 配置(默认是 appendonly.aof)设置。
保存目录同 RDB 持久化方式一致,通过 dir 配置指定(/var/lib/redis)。AOF 的工作流程操作:命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load),如下图所示:
AOF 工作流程:
- 所有的写入命令会追加到 aof_buf(内存中的缓冲区,大大降低了写硬盘的次数)中。
- AOF 缓冲区根据对应的策略向硬盘做同步操作。
- 随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
- 当 Redis 服务器启动时,可以加载 AOF 文件进行数据恢复。
注意:写硬盘的时候,写入硬盘数据的多少对于性能的影响不是很大,但是写入硬盘的次数则影响很大了。
硬盘上读写数据,顺序读写的速度还是比较快的(但还是比内存要慢很多),随机访问则速度比较慢。AOF 是每次将新的数据写入到原有文件的末尾,属于顺序写入。
2、命令写入
AOF 命令写入的内容直接是文本协议格式。
每次进行的操作都会被记录到文本文件中,通过一些特殊符号作为分隔符,来对命令的细节做出区分。
此处遵守 Redis 格式协议,Redis 选择文本协议可能的原因:
- 文本协议具备较好的兼容性。
- 实现简单。
- 具备可读性。
AOF 过程中为什么需要 aof_buf 这个缓冲区?
Redis 虽然是使用单线程响应命令,但是速度很快,因为它只是操作内存。引入 AOF 之后,又要写内存,又要写硬盘,同时还要保持之前的速度。实际上,这并没有影响到 Redis 处理请求的速度。AOF 机制并非是直接让工作线程把数据写入硬盘,而是先写入一个内存中的缓冲区,积累一波之后再统一写入。如果每次写 AOF 文件都直接同步硬盘,性能从内存的读写变成 IO 读写,必然会下降。先写入缓冲区可以有效减少 IO 次数,同时,Redis 还可以提供多种缓冲区同步(刷新)策略,让用户根据自己的实际情况和需求来做出合理的平衡。
如果把数据写入到缓冲区中,其本质还是在内存中。万一此时突然进程挂了或者主机掉点了,那缓冲区中的数据是否就丢了呢?
是的。缓冲区中没来得及写入硬盘的数据是会丢失的。
- 缓冲区刷新频率越高,性能影响就越大,同时数据的可靠性就越高。
- 缓冲区刷新频率越低,性能影响就越小,同时数据的可靠性就越低。
3、文件同步
Redis 提供了多种 AOF 缓冲区同步文件策略,由参数 appendfsync 控制,不同值的含义如下表所示。
AOF 缓冲区同步文件策略:
默认采用 everysec 配置:
系统调用 write 和 fsync 说明:
- write 操作会触发延迟写(delayed write)机制。Linux 在内核提供页缓冲区用来提供硬盘 IO 性能。write 操作在写入系统缓冲区后立即返回。同步硬盘操作依赖于系统调度机制,例如:缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失。
- Fsync 针对单个文件操作,做强制硬盘同步,fsync 将阻塞直到数据写入到硬盘。
- 配置为 always 时,每次写入都要同步 AOF 文件,性能很差,在⼀般的 SATA 硬盘上,只能⽀持大约几百 TPS 写入。除非是非常重要的数据,否则不建议配置。
- 配置为 no 时,由于操作系统同步策略不可控,虽然提高了性能,但数据丢失风险大增,除非数据重要程度很低,一般不建议配置。
- 配置为 everysec,是默认配置,也是推荐配置,兼顾了数据安全性和性能。理论上最多丢失 1s 的数据。
4、重写机制
随着命令不断写入 AOF,文件持续增长,体积会越来越大,会影响到 Redis 下次启动的启动时间(Redis 在启动时需要读取 AOF 文件内容,该文件记录了中间的过程,但实际上 Redis 在重启时只关注最终结果)。为了解决这个问题,Redis 引入 AOF 重写机制(能够针对 AOF 文件进行整理操作,剔除其中的冗余操作并合并一些操作,以此达到给文件 “瘦身” 的效果)压缩文件体积。AOF 文件重写是把 Redis 进程内的数据转化为写命令同步到新的 AOF 文件。
重写后的 AOF 为什么可以变小?
- 进程内已超时的数据不再写入文件。
- 旧的 AOF 中的无效命令,例如 del、hdel、srem 等重写后将会删除,只需要保留数据的最终版本。
多条写操作合并为⼀条,例如 lpush list a、lpush list b、lpush list 从可以合并为 lpush list a b c。较小的 AOF 文件一方面降低了硬盘空间占用,一方面可以提升启动 Redis 时数据恢复的速度。
AOF 重写过程可以手动触发和自动触发:
- 手动触发:调用 bgrewriteaof 命令。
- 自动触发:根据 auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 参数确定自动触发时机。
- auto-aof-rewrite-min-size:表示触发重写时 AOF 的最小文件大小,默认为 64MB。
- auto-aof-rewrite-percentage:代表当前 AOF 占用大小相比较上次重写时增加的比例。
当触发 AOF 重写时,下图介绍它的运行流程。
AOF 重写流程:
- 执行 AOF 重写请求。如果当前进程正在执行 AOF 重写,请求不执行。如果当前进程正在执行 bgsave 操作,重写命令延迟到 bgsave 完成之后再执行。
- 父进程执行 fork 创建子进程。
- 重写。a. 父进程 fork 之后,继续响应其他命令,仍然负责接收客户端的新的请求。父进程还是会把这些请求产生的 AOF 数据先写入到缓冲区中并根据 appendfsync 策略同步到硬盘,保证旧 AOF 文件机制正确。b. 子进程只有 fork 之前的所有内存信息,父进程中需要将 fork 之后这段时间的修改操作写入 AOF 重写缓冲区中。重写的时候不关心 AOF 文件中原来有什么,只关心内存中最终的数据状态。(内存中的数据的状态,就已经相当于是把 AOF 文件结果整理后的模样了)
-
在创建子进程的一瞬间,子进程就根据内存快照,将命令合并到新的 AOF 文件中,也就是继承了当前父进程的内存状态。因此,子进程里的内存数据是父进程 fork 之前的状态。fork 后新来的请求对内存造成的修改是子进程感知不到的。此时,父进程这里又准备了一个 aof_rewrite_buf 缓冲区(专门存放 fork 后收到的数据)。
-
子进程完成重写:
-
子进程这边把 AOF 新文件数据写入完成后,子进程会通过发送信号通知父进程。
-
父进程再把 aof_rewrite_buf 缓冲区中的内容也追加到新 AOF 文件里。
-
用新 AOF 文件代替旧 AOF 文件。
此处子进程写数据的过程非常类似于 RDB 生成一个镜像快照,只不过 RDB 是按照二进制的方式来生成的,AOF 重写则是按照 AOF 这里要求的文本格式来生成的。二者目的都是为了把当前内存中的所有数据状态记录到文件中。
如果在执行 bgrewriteaof 的时候,发现当前 Redis 已经正在进行 AOF 重写了,会怎样呢?
此时不会再次执行 AOF 重写,而是直接返回了。
如果在执行 bgrewriteaof 的时候,发现当前 Redis 在生成 rdb 文件的快照,会怎样呢?
此时 AOF 重写操作就会等待 rdb 快照生成完毕之后再进行执行 AOF 重写。
为什么 RDB 对于 fork 之后的新数据就直接置之不理了呢,而不选择采用和 AOF 一样的处理机制呢?
RDB 本身的设计理念就是用来 “定期备份” 的。只要是定期备份就难以和最新数据保持一致。
实时备份不一定就比定期备份更好,具体还是要看实际场景需求。现在的系统中,系统资源一般都是比较充裕的,AOF 的开销并不大,所以一般来说,AOF 的适用场景更多一些。
父进程 fork 完毕之后,就已经让子进程写新的 AOF 文件了。并且随着时间的推移,子进程很快就写完了新的文件,要让新的 AOF 文件代替旧的 AOF 文件。父进程此时还在继续写这个即将消亡的旧的 AOF 文件是否还有意义呢?
需要考虑到极端情况:假设在重写的过程中,重写了一半服务器突然挂了,此时子进程内存的数据就会丢失,新的 AOF 文件内容还不完整,所以如果父进程不坚持写旧的 AOF 文件,那么重启就无法保证数据的完整性了。
AOF 本来是按照文本的方式来写入文件的,但是文本的方式来写文件,后续加载的成本较高。所以,Redis 就引入了 “混合持久化” 的方式,结合了 rdb 和 aof 的特点。按照 aof 的方式,将每一个请求 / 操作都记录入文件中。在触发 aof 重写之后,就会把当前内存的状态按照 rdb 的二进制格式写入到新的 aof 文件中。
后续再进行的操作仍然是按照 aof 文本的方式追加到文件后面:
5、启动时数据恢复
当 Redis 启动时,会根据 RDB 和 AOF 文件的内容,进行数据恢复,如下图所示。
Redis 根据持久化文件进行数据恢复:
三、RDB 和 AOF 的区别和联系
- RDB 和 AOF 是 Redis 提供了两种持久化方案。
- RDB 视为内存的快照,产生的内容更为紧凑,占用空间较小,恢复时速度更快。但产⽣ RDB 的开销较大,不适合进行实时持久化,一般用于冷备和主从复制。
- AOF 视为对修改命令保存,在恢复时需要重放命令,并且有重写机制来定期压缩 AOF 文件。
- RDB 和 AOF 都使用 fork 创建子进程,利用 Linux 子进程拥有父进程内存快照的特点进行持久化,尽可能不影响主进程继续处理后续命令。