Redis 持久化策略深度解析
Redis 持久化策略
一、AOF(Append Only File)
1.1 AOF 的写后日志机制
说到日志,我们比较熟悉的是数据库的写前日志(Write Ahead Log, WAL),也就是说,在实际写数据前,先把修改的数据记到日志文件中,以便故障时进行恢复。不过,AOF 日志正好相反,它是写后日志,”写后”的意思是 Redis 是先执行命令,把数据写入内存,然后才记录日志。
1.1.1 为什么采用写后日志
为了避免额外的检查开销,Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。
而写后日志这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错。所以,Redis 使用写后日志这一方式的一大好处是,可以避免出现记录错误命令的情况。
除此之外,AOF 还有一个好处:它是在命令执行后才记录日志,所以不会阻塞当前的写操作。
1.1.2 AOF 的潜在风险
AOF 也有两个潜在的风险:
风险一:数据丢失
如果刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就有丢失的风险。如果此时 Redis 是用作缓存,还可以从后端数据库重新读入数据进行恢复,但是,如果 Redis 是直接用作数据库的话,此时,因为命令没有记入日志,所以就无法用日志进行恢复了。
风险二:阻塞风险
AOF 虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险。这是因为,AOF 日志也是在主线程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了。
1.2 AOF 写回策略
对于阻塞风险问题,AOF 机制给我们提供了三个选择,也就是 AOF 配置项 appendfsync 的三个可选值:
- Always(同步写回):每个写命令执行完,立马同步地将日志写回磁盘
- Everysec(每秒写回):每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘
- No(操作系统控制的写回):每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
策略选择建议:
- 想要获得高性能,就选择 No 策略
- 如果想要得到高可靠性保证,就选择 Always 策略
- 如果允许数据有一点丢失,又希望性能别受太大影响的话,那么就选择 Everysec 策略
1.3 AOF 重写机制
1.3.1 为什么需要重写
随着接收的写命令越来越多,AOF 文件会越来越大。这也就意味着,我们一定要小心 AOF 文件过大带来的性能问题。
AOF 如果一直写,而不重写的话,会导致 AOF 文件越来越大,所以当 AOF 文件的大小超过设定的阈值之后,Redis 就会启用 AOF 重写机制来压缩 AOF 文件。
1.3.2 重写机制的原理
具体的说,就是在重写的时候,读取当前数据库中的所有键值对,然后每一个键值对用一条命令记录到新的 AOF 文件中。等到全部记录完成之后,就会用新的 AOF 文件替换掉现有的 AOF 文件。
重写机制具有”多变一”功能。所谓的”多变一”,也就是说,旧日志文件中的多条命令,在重写后的新日志中变成了一条命令。这就相当于压缩了 AOF 文件了。
1.4 AOF 重写过程详解
和 AOF 日志由主线程写回不同,重写过程是由后台子进程 bgrewriteaof 来完成的,这也是为了避免阻塞主线程,导致数据库性能下降。
我把重写的过程总结为”一个拷贝,两处日志”。
1.4.1 一个拷贝
“一个拷贝”就是指,每次执行重写时,主线程 fork 出后台的 bgrewriteaof 子进程。此时,fork 会把主线程的内存拷贝一份给 bgrewriteaof 子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
1.4.2 两处日志
“两处日志”又是什么呢?
第一处日志: 因为主线程未阻塞,仍然可以处理新来的操作。此时,如果有写操作,第一处日志就是指正在使用的 AOF 日志,Redis 会把这个操作写到它的缓冲区。这样一来,即使宕机了,这个 AOF 日志的操作仍然是齐全的,可以用于恢复。
第二处日志: 就是指新的 AOF 重写日志。这个操作也会被写到重写日志的缓冲区。这样,重写日志也不会丢失最新的操作。等到拷贝数据的所有操作记录重写完成后,重写日志记录的这些最新操作也会写入新的 AOF 文件,以保证数据库最新状态的记录。此时,我们就可以用新的 AOF 文件替代旧文件了。
1.4.3 重写过程总结
总结来说,每次 AOF 重写时,Redis 会先执行一个内存拷贝,用于重写;然后,使用两个日志保证在重写过程中,新写入的数据不会丢失。而且,因为 Redis 采用额外的线程进行数据重写,所以,这个过程并不会阻塞主线程。
1.5 Fork 子进程的阻塞风险
但是不代表不会阻塞主线程。首先 fork 子进程,fork 这个瞬间一定是会阻塞主线程的(注意,fork 时并不会一次性拷贝所有内存数据给子进程,老师文章写的是拷贝所有内存数据给子进程,我个人认为是有歧义的)。
1.5.1 写时复制机制(Copy On Write)
fork 采用操作系统提供的写时复制(Copy On Write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成的长时间阻塞问题,但 fork 子进程需要拷贝进程必要的数据结构,其中有一项就是拷贝内存页表(虚拟内存和物理内存的映射索引表),这个拷贝过程会消耗大量 CPU 资源,拷贝完成之前整个进程是会阻塞的,阻塞时间取决于整个实例的内存大小,实例越大,内存页表越大,fork 阻塞时间越久。
拷贝内存页表完成后,子进程与父进程指向相同的内存地址空间,也就是说此时虽然产生了子进程,但是并没有申请与父进程相同的内存大小。
1.5.2 真正的内存分离时机
那什么时候父子进程才会真正内存分离呢?”写时复制”顾名思义,就是在写发生时,才真正拷贝内存真正的数据,这个过程中,父进程写操作会触发页复制,可能也会产生阻塞的风险,特别是父进程此时操作的是一个 bigkey,特别是大页(2 MB)复制慢,会增加阻塞风险,所以 Redis 推荐关闭 Huge Page。
二、RDB(Redis DataBase)
2.1 RDB 的优势
用 AOF 方法进行故障恢复的时候,需要逐一把操作日志都执行一遍。如果操作日志非常多,Redis 就会恢复得很缓慢,影响到正常使用。
RDB 就是 Redis DataBase 的缩写,和 AOF 相比,RDB 记录的是某一时刻的数据,并不是操作,所以,在做数据恢复时,我们可以直接把 RDB 文件读入内存,很快地完成恢复。
2.2 RDB 生成命令
Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave:
- save:在主线程中执行,会导致阻塞
- bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置
我们可以通过 bgsave 命令来执行全量快照,这既提供了数据的可靠性保证,也避免了对 Redis 的性能影响。
2.3 写时复制技术(Copy-On-Write, COW)
Redis 就会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。
简单来说,bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。
2.3.1 读操作场景
如果主线程对这些数据也都是读操作(例如图中的键值对 A),那么,主线程和 bgsave 子进程相互不影响。
2.3.2 写操作场景
但是,如果主线程要修改一块数据(例如图中的键值对 C),那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。
这既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。
2.4 RDB 快照总结
到这里,我们就解决了对”哪些数据做快照”以及”做快照时数据能否修改”这两大问题:
- Redis 会使用
bgsave对当前内存中的所有数据做快照 - 这个操作是子进程在后台完成的,这就允许主线程同时可以修改数据
2.5 RDB 快照频率的权衡
虽然跟 AOF 相比,快照的恢复速度快,但是,快照的频率不好把握:
- 如果频率太低,两次快照间一旦宕机,就可能有比较多的数据丢失
- 如果频率太高,又会产生额外开销
那么,还有什么方法既能利用 RDB 的快速恢复,又能以较小的开销做到尽量少丢数据呢?
三、混合持久化
3.1 混合持久化的提出
Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。
这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。
3.2 混合持久化的工作原理
如图所示,T1 和 T2 时刻的修改,用 AOF 日志记录,等到第二次做全量快照时,就可以清空 AOF 日志,因为此时的修改都已经记录到快照中了,恢复时就不再用日志了。
这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势。
3.3 混合持久化的实现
因为 AOF 和 RDB 各有其优缺点:
- AOF:丢失数据少,但是恢复速度慢
- RDB:恢复速度快,但是执行快照的频率不好控制,频率低丢失的数据多,频率高会影响性能
所以优化之后,就是让 AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
3.4 混合持久化的优缺点
优点:
- 加载的速度快
- 数据丢失的更少
缺点:
- 使得 AOF 文件的可读性变差
- 兼容性差,不能用于 Redis 4.0 版本之前
四、持久化策略对比总结
| 持久化方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| AOF | 数据丢失少,可读性好 | 文件大,恢复速度慢 | 对数据安全性要求高的场景 |
| RDB | 文件小,恢复速度快 | 可能丢失较多数据 | 可以容忍一定数据丢失的场景 |
| 混合持久化 | 恢复快,数据丢失少 | 可读性差,兼容性问题 | Redis 4.0+ 推荐使用 |
建议:
- 如果对数据安全性要求很高,建议同时开启 AOF 和 RDB
- 如果可以容忍数分钟的数据丢失,可以只使用 RDB
- Redis 4.0+ 版本推荐使用混合持久化,兼顾性能和数据安全