Redis核心技术03-持久化
# Redis核心技术03-持久化
Redis的数据全部在内存里,如果突然发生宕机,数据就会全部丢失,所以肯定需要一份安全机制来保证数据不会因为突发情况而丢失,这种机制就是Redis持久化机制。
Redis的持久化则是将内存中的数据备份到硬盘中,在服务器重启时可以恢复。
持久化机制有两种,第一种是快照
,第二种是AOF
日志
# 快照
定义:
快照(RDB)是某一个时间点将Redis的内存数据全量
写入一个临时文件,当写入完成后,用该临时文件替换上一次持久化生成的文件,这样就完成了一次持久化过程。
特点:
- 快照是一次全量备份。
- 快照是内存数据的二进制序列化形式,在存储上非常紧凑。
# 快照原理
Redis是单线程的,而这个线程比较繁忙,它要同时负责多个客户端套接字的并发读写操作和内存数据结构的逻辑读写。
现在由于需要持久化机制来保证数据,所以Redis需要一边响应请求,一边进行持久化操作,持久化操作必定涉及到文件IO操作,而文件IO操作就会严重拖累服务器的性能。
现在我们试想一个问题:比如有一个较大的数据,现在Redis对其进行持久化操作,辛辛苦苦持久化快要结束了,突然来了一个请求,把这个较大的数据给删除了,那岂不是很难受。也就是我们希望在拍照的时候,数据不要“动”。但是又存在一个问题那就是,为了快照而暂停写操作,肯定是不能接受的
解决这个问题Redis引入了操作系统的多进程COW(Copy on Write)来实现持久化。
# COW
Redis在持久化时会调用glibc的函数fork产生一个子进程,父进程继续处理客户端的请求,而这个产生的子进程就进行Redis的持久化操作。在产生这个子进程的那一刻,父子进程是共享内存里面的代码段和数据段的。相关流程如下:
1、客户端发送bgsave命令,此刻会判断是否有其它子进程,如果没有就会进行fork操作。
2、Redis父进程通过fork操作创建子进程,在 fork 操作过程中父进程会阻塞。
3、Redis父进程fork 操作完成后,bgsave 命令返回 Background saving started
信息并不再阻塞 Redis 父进程,可以继续响应其他命令了。
4、子进程根据父进程创建那一刻所共享的内存进行持久化操作。
此时,如果主线程对这些数据也都是读操作(例如图中的键值对 A),那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据(例如图中的键值对 C),那么,这块数据就会被复制一份,生成该数据的副本(键值对 C’)。然后,主线程在这个数据副本上进行修改。同时,bgsave 子进程可以继续把原来的数据(键值对 C)写入 RDB 文件。
通过上述操作,现在父子进程各司其责,父进程持续服务客户端请求,然后对内存数据结果进行不间断修改,子进程就做持久化,不会修改现有的内存数据结构。
# 快照的频率
由于快照是某一刻的全量数据,那么就势必会涉及到我们多久拍一次照的问题。
试想一下,如果你在T1时刻拍了一张照片,又在T2时刻拍了一张照片,如果Redis在T1和T2某个Q时间点宕机了,而T1到Q时刻可能已经发生了许多数据的变化,如果使用T1时刻快照进行恢复,很多数据就丢失了。所以这个快照的频率非常重要。
那么我们是不是让快照的频率越小越好呢?频率越小,意味着那么发生上诉的情况,我们丢失的数据也不会太多。而且快照是子进程在后台执行的,不会阻塞主线程。
虽然 bgsave 执行时不阻塞主线程,但是,如果频繁地执行全量快照,也会带来两方面的开销。
一方面,频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。
另一方面,bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是,fork 这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。如果频繁 fork 出 bgsave 子进程,这就会频繁阻塞主线程了(所以,在 Redis 中如果有一个 bgsave 在运行,就不会再启动第二个 bgsave 子进程)。
这也是为什么bgsave会去判断是否存在其他子进程的原因。
# 增量快照
所谓增量快照,就是指,做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。
在第一次做完全量快照后,T1 和 T2 时刻如果再做快照,我们只需要将被修改的数据写入快照文件就行。但是,这么做的前提是,我们需要记住哪些数据被修改了。你可不要小瞧这个“记住”功能,它需要我们使用额外的元数据信息去记录哪些数据被修改了,这会带来额外的空间开销问题。
如果对每一个修改的值都做一个记录,如果有 1 万个被修改的键值对,我们就需要有 1 万条额外的记录。而且,有的时候,键值对非常小,比如只有 32 字节,而记录它被修改的元数据信息,可能就需要 8 字节。这样的话,记录这个功能所需的存储空间远远大于了本身存储键值的空间是不太值得的。
虽然跟 AOF 相比,快照的恢复速度快,但是,快照的频率不好把握,如果频率太低,两次快照间一旦宕机,就可能有比较多的数据丢失。如果频率太高,又会产生额外开销。
# AOF日志和内存快照混合
简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。
这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。
设置的参数是: aof-use-rdb-preamble yes
# AOF
快照最大的问题就在于它提供的持久化策略是不安全的,不适合做实时持久化,所以 Redis 提供了第二套持久化方式:AOF 来解决这个问题。
AOF(Append Only File)日志是连续的增量备份,记录的是内存数据修改的指令记录文本。
# AOF原理
试想一个问题:如果你正在做一套题,突然你的答题卡损害了,现在你要快速恢复你的答题记录该这么办呢?
如果你有一份做题记录的草稿纸,是不是立马把草稿纸的内容重新抄写到答题卡上就恢复了。
AOF原理类似如此,Reids收到客户端修改指令之后,进行一系列操作之后,如果没有问题,就立即将该指令文本存储到AOF日志中,先执行指令再将日志存盘。
为什么先执行指令再记录日志呢
为了避免额外的检查开销,Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。
而写后日志这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错。
所以,Redis 使用写后日志这一方式的一大好处是,可以避免出现记录错误命令的情况。AOF 还有一个好处:它是在命令执行后才记录日志,所以不会阻塞当前的写操作。
那么通过对一个空的Redis实例顺序执行我们日志中的指令,就可以恢复Redis当前实例内存数据结构的状态。
# 三种写回策略
AOF是存在两个潜在风险的:
首先,如果刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就有丢失的风险。
其次,AOF 虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险。这是因为,AOF 日志也是在主线程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了。
上诉两个风险都与AOF写回磁盘时机相关,所以如果我们要避免这两个风险,就需要控制AOF 日志写回磁盘的时机。
AOF机制给我们提供了三个选择,也就是 AOF 配置项 appendfsync 的三个可选值。
Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
针对避免主线程阻塞和减少数据丢失问题,这三种写回策略都无法做到两全其美:
- “同步写回”可以做到基本不丢数据,但是它在每一个写命令后都有一个慢速的落盘操作,不可避免地会影响主线程性能;
- “每秒写回”采用一秒写回一次的频率,避免了“同步写回”的性能开销,虽然减少了对系统性能的影响,但是如果发生宕机,上一秒内未落盘的命令操作仍然会丢失。所以,这只能算是,在避免影响主线程性能和避免数据丢失两者间取了个折中。
- 虽然“操作系统控制的写回”在写完缓冲区后,就可以继续执行后续的命令,但是落盘的时机已经不在 Redis 手中了,只要 AOF 记录没有写回磁盘,一旦宕机对应的数据就丢失了;
总结一下就是:想要获得高性能,就选择 No 策略;如果想要得到高可靠性保证,就选择 Always 策略;如果允许数据有一点丢失,又希望性能别受太大影响的话,那么就选择 Everysec 策略。
# AOF重写
随着命令的不断写入,AOF 文件会越来越庞大,会带来一定的性能问题:
一是,文件系统本身对文件大小有限制,无法保存过大的文件;
二是,如果文件太大,之后再往里面追加命令记录的话,效率也会变低;
三是,如果发生宕机,AOF 中记录的命令要一个个被重新执行,用于故障恢复,如果日志文件太大,整个恢复过程就会非常缓慢,这就会影响到 Redis 的正常使用。
为了解决这些问题,Redis 提供了 “文件重写”功能。
重写 AOF 文件最直观的表现是导致 AOF 文件减小,重写时,Redis 主要做了如下几件事情让 AOF 文件减小:
- 已过期的数据不在写入文件。
- 保留最终命令。例如
set key1 value1
、set key1 value2
、....set key1 valuen
,类似于这样的命令,只需要保留最后一个即可。 - 删除无用的命令。例如
set key1 valuel;del key1
,这样的命令也是可以不用写入文件中的。 - 多条命令合并成一条命令。例如
lpush list a、lpush list b、lpush list c
,可以转化为lpush list a b c
# AOF重写会阻塞吗
AOF的写是由主线程进行的,而AOF的重写是由后台子进程 bgrewriteaof 来完成的,这也是为了避免阻塞主线程,导致数据库性能下降。
这里的阻塞是指子进程不会阻塞主进程。
重写过程为一个拷贝、两处日志
一个拷贝是指:fork子进程时,子进程是会拷贝父进程的页表,即虚实映射关系,而不会拷贝物理内存。子进程复制了父进程页表,也能共享访问父进程的内存数据了,此时,类似于有了父进程的所有内存数据。
两处日志分别是:
- 因为主线程未阻塞,仍然可以处理新来的操作。此时,如果有写操作,第一处日志就是指正在使用的 AOF 日志,Redis 会把这个操作写到它的缓冲区。这样一来,即使宕机了,这个 AOF 日志的操作仍然是齐全的,可以用于恢复。
- 而第二处日志,就是指新的 AOF 重写日志。这个操作也会被写到重写日志的缓冲区。这样,重写日志也不会丢失最新的操作。等到拷贝数据的所有操作记录重写完成后,重写日志记录的这些最新操作也会写入新的 AOF 文件,以保证数据库最新状态的记录。此时,我们就可以用新的 AOF 文件替代旧文件了。
# 参考
Redis核心技术与实战 (opens new window)
Redis深入历险核心原理与应用实践