redis 数据持久化
作者:w08e
数据持久化三连问
redis 宕机数据会丢失吗
回答话术
先说结论,如果我们没开启任何持久化机制,那么会丢失全部数据,否则只会丢失部分数据,丢失数据的多少取决于持久化配置。Redis 提供了两套持久化机制,RDB 快照和 AOF 日志文件追加。
RDB 它会根据情况定期的 Fork 出一个子进程,生成当前数据库的全量快照。对于 RDB 快照,假如我们在 RDB 快照生成后宕机,那么会丢失快照生成期间全部增量数据,如果在连快照都没成功生成,那么就会丢掉全部数据。
另一个是 AOF,它通过向 AOF 日志文件追加每一条执行过的指令实现。而当我们仅开启了 AOF 时,丢失数据的多少取决于我们设置的刷盘策略:当设置为每条指令执行后都刷盘 Always
,我们最多丢失一条指令;当设置为每秒刷一次盘的 Eversec
时,最多丢失一秒内的数据;当设置为非主动刷盘的 No
时,则可能丢失上次刷盘后到现在的全部数据。
考虑到两种模式各有优缺点,没有一个适中的解决方案,为此,Redis 在 4.0 以后允许通过 aof‐use‐rdb‐preamble
配置开启混合持久化。
在开启混合持久化模式时,AOF 重写过程中,Redis 会将持久化数据以 RDB 格式写入 AOF 文件的开头,随后将后续的数据以 AOF 格式追加到文件末尾。恢复数据时,Redis 首先加载 RDB 数据,然后加载 AOF 增量数据。
问题详解
1. RDB
RDB 是 Redis 提供的持久化机制之一,它通过将内存中的数据保存到磁盘中的二进制文件来实现。
我们可以通过 SAVE
或 BGSAVE
指令主动触发快照的生成,也可以通过配置文件中的 save
配置快照的自动生成条件。
在现实中出于性能考虑,我们不可能非常频繁的保存快照,因此要防止数据丢失,最终还是主要依靠 AOF 实现。
2. AOF
AOF 是 Redis 提供的另一套持久化机制。当每个写命令被执行完毕后,它们会被追加写入 AOF 日志文件的末尾。当 Redis 宕机以后,就可以通过 AOF 日志重放这些命令来恢复数据。
不过,每次命令执行后,数据会先写入 AOF 缓存,然后再写入操作系统缓存,最后才会根据刷盘策略真正的写入磁盘。因此刷盘策略真正决定了 Redis 宕机时会丢失多少数据:
Always
:每执行一条指令就刷一次盘,宕机时最多丢失一条指令。Eversec
:每秒刷一次盘,宕机时最多丢失一秒内的数据。No
:不主动刷盘,由操作系统自己完成,宕机时最多丢失从上一次刷盘到宕机时的全部数据。
3. 混合持久化
混合持久化是在 AOF 基础上的优化措施,严格来说还是 AOF,Redis 官方文档里面持久化方式其实还是只有 AOF 和 RDB 两种。
由于 RDB 快照模式会丢失增量数据,AOF 文件较大会影响 Redis 数据恢复的时间,因此 Redis 在 4.0 以后允许通过 aof‐use‐rdb‐preamble
配置开启混合持久化。
当 AOF 重写时,它将会先生成当前时间的 RDB 快照,然后将其写入新的 AOF 文件头部位置,接着再把增量数据追加到这个新 AOF 文件中。如此一来,当 Redis 通过 AOF 文件恢复数据时,将会先加载 RDB,然后再重放后半部分的增量数据。这样就可以大幅度提高数据恢复的速度。
3.1. 查看混合持久化是否打开
根据 Redis 版本的不同,有些默认是开启的,有些默认是关闭状态,我们可以通过两种方式开启混合持久化配置。
127.0.0.1:6379> config get aof-use-rdb-preamble
1) "aof-use-rdb-preamble"
2) "no"
3.2. 通过命令行开启
可以通过 config set aof-use-rdb-preamble yes
命令行的方式开启混合持久化配置,注意开启后再查一次配置。
127.0.0.1:6379> config get aof-use-rdb-preamble
1) "aof-use-rdb-preamble"
2) "no"
127.0.0.1:6379> config set aof-use-rdb-preamble yes
OK
127.0.0.1:6379> config get aof-use-rdb-preamble
1) "aof-use-rdb-preamble"
2) "yes"
3.3. 文件配置开启
我们可以在 Redis 的配置文件 redis.conf
中开启,开启后确保万无一失需要再通过 config get aof-use-rdb-preamble
查询下。
# Redis can create append-only base files in either RDB or AOF formats. Using
# the RDB format is always faster and more efficient, and disabling it is only
# supported for backward compatibility purposes.
aof-use-rdb-preamble yes
Redis的RDB是怎么实现的?
回答话术
当通过 BGSAVE
指令生成 RDB 的时候,Redis 会 fork
出一个子进程,它会基于写时复制机制,在不阻塞主线程的情况下,将此刻的数据库全量数据保存为二进制快照。
具体的来说,在最开始的时候 fork
子进程的时候,操作系统会为其拷贝父进程的内存页,但是此时两者都指向同一块物理内存。当主进程发生写操作时,会真正的将父进程的数据拷贝到独立的物理内存中。此时父子进程的物理内存彼此独立,互不干涉,父进程继续处理增量数据,而子进程则根据拷贝出来的旧数据生成 RDB 快照。
相对 AOF,RDB 生成的二进制文件更小,数据恢复起来更快,并且整个流程中完全不会阻塞主进程,但是对应的,由于写时复制,在最坏的情况下可能会占用双倍内存,并且无法保存增量数据。
问题详解
1. 如何生成 RDB 文件
我们可以通过 SAVE
命令在主进程中阻塞的生成 RDB 文件,或者通过 BGSAVE
命令指定在子进程中生成 RDB 文件。
此外,也可以在数据库中通过 save
选项,指定当在一定的时间范围内执行了多少次修改时生成 RDB 文件。比如 save 120 10000
表示当在 120 秒内发生了 10000 次修改时,就通过 BGSAVE
在后台生成一分 RDB 文件。
2. 实现原理
当 Redis 在 fork 出一个子进程去生成 RDB 文件时,主进程依然还在不同的接受指令并操作数据。与 AOF 不同,由于 RDB 是快照,因此实际上它不会同步增量数据,不过也不会影响父进程的操作。Redis 通过写时复制机制实现这样的效果。
简单的来说,当 fork 子进程后,虽然子进程会从父进程中拷贝内存页,但是实际上的内存页依然还是指向了原本的物理内存。而当父进程修改了内存后,才会真正在物理内存中复制一遍父进程的数据,并让子进程的内存页指向这块物理内存,这就是写时复制。
根据这个原理,当子进程在生成 RDB 文件时,若父进程发生写操作,由于写时复制,子进程会得到一份父进程内存页的拷贝。此时子进程读取的是此时父进程内存数据的拷贝,而父进程继续操作他原本的那份内存,两者互不干涉。
我们都知道,在 Linux 系统中内存页一页是 4KB,由于是以内存页为单位的复制,所以在这个过程中,只要不是所有内存页上的数据都发生了修改,那么生成 RDB 文件过程中占用的额外内存是比较低的。
3. 与 AOF 的区别
RDB 即 Redis Database,它本质上就是某个时刻的全量数据快照。它以二进制的方式保存了某个时刻 Redis 数据库中的全部数据,Redis 启动后只需要将其加载进内存即可恢复数据。
相比起 AOF,它的优点是:
- 文件更小:由于 RDB 里面存放的是非常紧凑的二进制数据,因此相比起 AOF 文件,它占用的空间更小,因此也更适合用来频繁的进行全量备份。
- 加载更快:Redis 只需要将 RDB 加载进内存即可恢复数据,相比起还需要重放指令的 AOF,它要快得多。
- 不阻塞主进程:生成 RDB 的过程可以完全在子进程中完成,因而不需要在主线程进行任何 IO 操作。
对应的,它的缺点是:
- RDB 需要 fork 出子进程去完成这个任务,当比较频繁的生成 RDB 文件时,会对 CPU 带来比较大的压力;
- 无法保存增量数据。
Redis的AOF是怎么实现的?
回答话术
AOF 即 Append Only File
,它是 Redis 提供的一种持久化机制。
其原理是每当服务器执行写指令时,将命令追加到 AOF 日志文件。当 Redis 重新启动时,他会在本地启动一个伪客户端,并按顺序重新发送日志中的命令以恢复数据。
Redis 的 AOF 日志和 MySQL 的 binlog 有点像,当执行一个命令后,数据会先写入 AOF 缓冲区,再写入操作系统缓冲区,最后根据刷盘策略调用 fsync
函数将数据刷入磁盘。Redis 默认提供三种刷盘策略:Always(每个命令后都刷盘)、Everysec(每秒刷一次盘)、No(等到操作系统缓冲区满或定期刷盘)。
当 AOF 日志越来越大的时候,会触发 AOF 重写。举个例子,假如在 Redis 中对 1 递增了 99 次,那么 AOF 文件会记录一百条命令,但是实际上我们恢复数据的时候只需要一个最终值 100,中间的步骤都是不需要的。基于这个原理,在 Redis 重写的过程中,它会开启一个子进程扫描数据库,并生成一个新的 AOF 文件去替换旧的文件。这个文件将会比原本的文件精简,并且哪怕这个过程中 Redis 挂了,也不会影响已有的 AOF 文件。
不过,在子进程进行 AOF 重写的过程中,由于主进程还在不停的接受新的指令,因此它除了需要写自己的 AOF 缓冲区外,还需要将其写到 AOF 重写缓冲区中,以此实现重写过程中的增量数据同步。
问题详解
1. 保存哪些命令
在 AOF 文件中,只会保存写指令,或者更准确点说,只会保存修改数据的指令。
比如,我们依次执行了下述指令:
RPUSH list 1 2 3 4
LRANGE list 0 -1
KEYS *
LPUSH list 1
那么,最终只会保存两条:
RPUSH list 1 2 3 4
LPUSH list 1
2. 数据的保存格式
值得注意的是,在 Redis 中执行的命令并不会原模原样的保存到 AOF ,而是以一种比较特殊 $[长度] + [指令]
的格式保存。
比如,我们执行一个简单的命令 SET name open8gu
,那么 AOF 中对应的内容如下:
*3
$3
SET
$4
name
$7
open8gu
上述命令的含义如下:
*3
表示接下来有 3 个参数;$3
表示接下来的参数的长度为 3,SET
是命令名称;$4
表示下一个参数的长度为 4,name
是 Key 的名称;$7
表示下一个参数的长度为 7,open8gu
是要设置的值。
3. 写日志的时机
当 Redis 接收到一条指令的时候,它会先执行指令,然后再写 AOF 日志。
这个逻辑与我们熟悉的 MySQL 中的 binlog 不同,后者是写前日志(Write Ahead Log, WAL),即先写日志再保存数据,而 AOF 日志则是写后日志,即先保存数据再写日志。
这种处理方式的优点是:
- 可以确保写入 AOF 日志指令都是没有错误的可执行的指令,避免写日志时还需要进行额外的语法/类型检查,或者等出错后回滚日志。
- 不因为写日志而阻塞当前指令的执行。
不过对应的缺点也很明显:
- 如果执行完指令实例突然挂了,那 AOF 日志中就不会记录这条指令。
- 由于 Redis 的大多数命令都由单个线程执行,因此可能因为写日志而阻塞后一条指令的执行。
4. 日志的刷盘策略
实际上,将指令数据写入磁盘的时候,并不是一步完成的:
- 当执行了写指令后,数据首先被写入 Redis 自己的 AOF 缓冲区;
- 随后,Redis 会调用操作系统的
write
函数,将数据从 AOF 缓冲区写入操作系统缓冲区; - 最后,再由 Redis 调用
fsync
函数或操作系统自己刷盘,让内核缓冲区中的数据真正写入磁盘。
第三步即我们通常说的“日志刷盘”。在日志真正的刷到磁盘之前,数据仍然仅保存在内存里,此时一旦服务器宕机,数据将会永久性的丢失。因此,何时刷盘是整个持久化流程的关键点。
在 MySQL 中,我们可以通过 binlog_sync
来指定 binlog 的刷盘策略,而在 Redis 中,我们可以通过 appendfsync
配置项指定 AOF 日志的的刷盘策略:
AOF_FSYNC_NO
:AOF 缓冲区有数据时(即执行一个命令后),调用write
函数写入操作系统缓冲区,然后操作系统定期(在 Linux 中通常是 30 秒)或缓冲区满后再自动写入磁盘。AOF_FSYNC_EVERYSEC
:AOF 缓冲区有数据时(即执行一个命令后),调用write
函数写入操作系统缓冲区,然后每一秒钟调用一次fsync
将数据写入磁盘。AOF_FSYNC_ALWAYS
:AOF 缓冲区有数据时(即执行一个命令后),立刻调用fsync
将数据写入磁盘。
这三种配置方式各有优劣,它们会很大程度上的影响 Redis 的性能:
指令 | 时机 | 性能 | 宕机时丢失的数据 |
---|---|---|---|
AOF_FSYNC_NO |
不主动刷盘,由操作系统自己决定刷盘时机 | 高 | 所有未写入磁盘的数据 |
AOF_FSYNC_EVERYSEC |
每秒保存一次 | 中 | 一秒内的数据 |
AOF_FSYNC_ALWAYS |
每个命令执行后保存一次 | 低 | 一条指令的数据 |
总的来说,核心问题在于如何取舍可靠性与性能:
- 如果你的系统对数据可靠性要求极高,不允许数据丢失,那么你应该选择
ALWAYS
。 - 如果你的系统更在乎性能,而不在意丢失一些数据,那么你可以选择
NO
。 - 如果你想要在两者间取得平衡,那么你可以选择
EVERSEC
。
5. AOF 重写
5.1. 实现原理
随着写入操作的进行,AOF 文件会变得越来越大,而这其中的大多数数据是没必要保存的。
比如,你把 1 递增到 100,那么最终 AOF 会记录这一过程中的全部 100 条指令。然而实际上我们只需要最终的值 100 即可。
因此,AOF 提供了一种重写机制,即当 AOF 文件膨胀到一定程度时,Redis 将直接重新扫描当前数据库中的数据,然后把它们重写到一个新 AOF 文件中,并替换旧的 AOF 文件,这个新的 AOF 文件会比原本的文件更小。
在这个过程中,Redis 实际上完全不会重新读取原有的 AOF 文件。
5.2. 触发条件
在 2.4 版本以后,当你开启 AOF 功能,Redis 会在满足下述三个条件的时候自动触发 AOF 重写:
- 当前没有正在执行的 AOF 重写或 RDB 生成操作。
- 当前的 AOF 文件大于
server.aof_rewrite_min_size
配置。 - 当前 AOF 的文件大小增幅达到设置的比例(比如比上一次重写后的文件大了 50%)。
除此之外,你也可以可以通过 BGREWRITEAOF
命令手动触发 AOF 重写。
5.3. 后台重写
作为一个非常重的 IO 操作,AOF 重写会长时间的阻塞线程,因此 Reids 会通过操作系统的 fork
函数分离出一个子进程 bgrewriteaof
来完成。
使用子进程的好处在于:
- 子进程进行 AOF 重写时,主进程可以正常执行,避免阻塞。
- 由于子进程带有主进程的数据副本,因此不需要像线程通过加锁控制对数据的访问。
5.4. 增量数据的同步
由于 AOF 重写基于父子进程,因此也带来一个问题:当子进程进行 AOF 重写时,主进程仍然还在接受指令修改数据,因此重写的 AOF 文件数据与实时数据就可能不一致。
对此,以 7.0 版本为分界线,Redis 采用了不同的处理方式:
5.4.1. 7.0 版本之前
在 7.0 之前,Redis 采用让主进程同时写两份 AOF 文件的方式来处理这个问题。
简单的来说,当子进程在进行 AOF 重写时,如果主进程接受了一个写指令,那么它在执行后,既要将这个指令追加到 AOF 缓冲区中,也需要将其加入 AOF 重写缓存中,相当于同时写两份文件。
当子进程完成 AOF 重写后,它会向父进程发送完成信号,此时父进程将阻塞的将 AOF 重写缓存区中的数据全部写入新的 AOF 文件中,然后使用新的 AOF 文件覆盖旧的 AOF 文件。至此, AOF 重写就完成了。
5.4.2. 7.0 版本之后
在 7.0 之后,当开始 AOF 重写的时候,主进程直接将增量数据写到一个全新的增量 AOF 文件中,等到子进程重写完 AOF,主进程再将增量 AOF 文件与重写后的 AOF 文件合并,并替换旧的 AOF 文件。
相比起 7.0 之前,对于同一个增量命令,主进程主需要往增量 AOF 文件里面写一次即可,不必再向子进程正在重写的 AOF 文件里面另外再写一条数据了。
需要注意的是,虽然我们称新的 AOF 文件为“增量 AOF”文件,不过对于主进程来说两个 AOF 文件没啥区别,只是从某一条命令开始换了一个文件写罢了,这条命令之前的数据全部在旧 AOF 文件,而这条命令及以后的数据都在新 AOF 文件里,这个过程依然受刷盘策略控制。
6. 数据的恢复
当 Redis 重新启动时,它会创建一个本地的伪客户端,这个客户端将会读取 AOF 日志,并且在还原出命令后发送给 Redis 服务端,直到全部的命令都执行完毕为止。
另外,根据官网文档,如果 Redis 在写 AOF 日志的过程中宕机,或者由于磁盘已满等不可抗力最终导致 AOF 日志出错,那么当重启时,Redis 会丢弃最后一个写入失败的指定,或者如果情况更糟糕,则可以通过 redis-check-aof
工具尝试修复它。