持久化就是把内存的数据写到磁盘中,防止服务器宕机导致内存数据丢失。
Redis 支持两种方式的持久化,一种是RDB
的方式,一种是AOF
的方式。
RDB 就是 Redis DataBase
的缩写,中文名为快照 / 内存快照,RDB持久化是把当前进程数据生成快照保存到磁盘上的过程,由于是某一时刻的快照,那么快照的值要早于或等于内存的值。
触发RDB持久化的方式有两种,分别是手动触发和自动触发
手动触发分别对应 save 和 bgsave 命令。
bgsave流程图如下所示:
执行过程:
bgsave
命令bgsave
命令直接返回。fork
操作创建子进程,fork 操作过程中父进程会阻塞。fork
完成后,父进程继续接收并处理客户端的请求,而子进程开始将内存中的数据写进硬盘的临时数据。在以下四种情况时会自动触发。
redis.conf
中配置 save m n
,即在 m 秒内有 n 次修改时,自动触发 bgsave
生成 rdb 文件。bgsave
操作,生成当时的快照发送到从节点。debug reload
命令重新加载 redis 时如果没有开启 aof 持久化,那么也会触发 bgsave
。bgsave
操作。RDB 中的核心思路是 Copy-on-Write
(写时复制),来保证在进行快照操作的这段时间,需要压缩写入磁盘上的数据在内存中不会发生变化。在正常的快照操作中,一方面 Redis 主进程 会 fork 一个新的快照进程专门来做这个事情,这样保证了 Redis 服务 不会停止对客户端包括写请求在内的任何响应。另一方面,在 fork 过程中发生的数据变化会以副本的方式存放在另一个新的内存区域,待快照操作结束后才会同步到原来的内存区域。
举个例子:如果主线程对这些数据也都是读操作(例如图中的键值对A),那么,主线程 和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据(例如图中的键值对C),那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入到 rdb 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。
在没有将数据全部写入到磁盘前,这次快照操作都不算成功。如果出现了服务崩溃的情况,将以上一次完整的 RDB 快照文件 作为回复内存数据的参考。也就是说,在快照操作过程中不能影响上一次的备份数据。Redis服务会在磁盘上创建一个临时文件进行数据操作,待操作成功后才会用这个临时文件替换掉上一次的备份。
显然是不可以的,虽然 bgsave 执行时不会阻塞主线程,但是,如果频繁的执行全量快照,也会带来两方面的开销:
优点:
缺点:
Redis 是“写后”日志,Redis 先执行命令,把数据写入内存,然后才记录日志。日志里记录的是Redis收到的每一条命令,这些命令是以文本形式保存。
PS:大多数的数据库采用的是写前日志(WAL),例如MySQL,通过写前日志和两阶段提交,实现数据和逻辑的一致性。
而 AOF 日志采用写后日志,即先写内存,后写日志。
为什么采用写后日志?
Redis 要求高性能,采用写日志的有两方面好处:
这种方式存在一定风险:
AOF日志记录 Redis 的每个写指令,步骤分为:命令追加(append)、文件写入(write)和 文件同步(sync)。
Always
,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
Everysec
,每秒写回:每个写命令执行完,只是先把日志写到 AOF文件 的内容缓冲区,每隔 1s 把缓冲区的内容写入磁盘;
No
,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内容缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
上面的三种策略体现了一个重要原则:trade-off,取舍,指在性能和可靠性保证之间做取舍。
关于 AOF 的同步策略是涉及到操作系统的 write 函数 和 fsync 函数的,在《Redis设计与实现》是这样说明的。
为了提高文件写入效率,在现代操作系统中,当用户调用write函数,将一些数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区的空间被填满或超过了指定时限后,才真正将缓冲区的数据写入到磁盘里。
这样的操作虽然提高了效率,但也为数据写入带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失。为此,系统提供了fsync、fdatasync同步函数,可以强制操作系统立刻将缓冲区中的数据写入到硬盘中,从而确保写入数据的安全性。
Redis 通过创建一个新的 AOF 文件来替换现有的 AOF ,新旧两个 AOF 文件保存的数据相同,但新 AOF 文件中没有冗余命令。
AOF重写过程是由后台进程bgrewriteaof
来完成的。主线程 fork 出后台的 bgrewriteaof
子进程,fork 会把主线程的内存拷贝一份给bgrewriteaof
子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof
子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
所以 AOF 在重写时,在 fork 进程时是会阻塞主线程的。
有两个配置项控制AOF重写的触发:
auto-aof-rewrite-min-size
:表示运行AOF重写时 文件的最小大小,默认为64MB。auto-aof-rewrite-percentage
:这个值的计算方式是,当前 aof 文件大小和上一次重写后 aof 文件大小的差值,再除以上一次重写后 aof 文件大小。也就是当前 aof文件 比上一次重写后 aof文件 的增量大小,和上一次重写后 aof文件 大小的比值。重写过程总结为:“一个拷贝,两处日志”。在 fork 出子进程时的拷贝,以及在重写时,如果有新数据写入,主线程就会将命令记录到两个 aof 日志内存缓冲区中。如果 AOF 写回策略配置的是 always ,则直接将命令写回旧的日志文件,并且保存一份命令至 AOF 重写缓冲区,这些操作对新的日志文件是不存在影响的。(旧的日志文件:主线程使用的日志文件,新的日志文件: bgrewriteaof 进程使用的日志文件)。
而在 bgrewriteaof 子进程完成对日志文件的重写操作后,会提示主线程已经完成重写操作,主线程会将 AOF 重写缓冲区中的命令追加到新的日志文件后面。这时候在高并发的情况下,AOF 重写缓冲区积累可能会很大,这样就会造成阻塞,Redis后来通过 Linux 管道技术让 aof 重写期间就能同时进行回放,这样 aof 重写结束后只需回放少量剩余数据即可。
最后通过修改文件名的方式,保证文件切换的原子性。
在 AOF 重写日志期间发生宕机的话,因为日志文件还没切换,所以恢复数据时,用的还是旧的日志文件。
流程如下:
那么为什么会优先加载 aof 文件呢?
因为 aof 保存的数据更完整,因为 aof 基本上最多损失 1s 的数据。