![cover](/upload/image-dzuc.png)
Redis-4_持久化
概览
AOF
AOF(Append Only File,追加式文件) 日志是写后日志,“写后”的意思是 Redis 是先执行命令,把数据写入 内存,然后才记录日志,如下图所示:
采用”写后“的原因?
因为AOF日志存储的是执行的指令,例如“set testkey testvalue”,实际文件存储的是:
*3 $3 set $7 testkey $9 testvalue
为了避免额外的检查开销,Redis 在向 AOF 里面记录日志的时候,并不会先去对这 些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误 的命令,Redis 在使用日志恢复数据时,就可能会出错。
而写后日志这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志 中,否则,系统就会直接向客户端报错。所以,Redis 使用写后日志这一方式的一大好处 是,可以避免出现记录错误命令的情况。
写回策略
AOF 机制给我们提供了三个选择,也就是 AOF 配置项 appendfsync 的三个可选值
Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓 冲区,由操作系统决定何时将缓冲区内容写回磁盘。
三种写回策略刷磁盘的方式:
always:对于每个写命令,Redis都会在处理完命令后立即向AOF文件追加这个命令,并且主线程通过调用fsync()确保这个命令被同步到磁盘上。这种模式提供了最高的数据安全性,但因为fsync()是一个磁盘密集型操作,可能会导致性能问题,特别是在高写入负载下。由于是主线程操作fsync()命令,如果磁盘响应较慢,每个写操作都可能会阻塞主线程,影响Redis的响应时间。
everysec:Redis会在处理完写命令后追加命令到AOF缓冲区,由后台线程每秒调用一次fsync()同步这些数据到磁盘。这种方式提供了一个平衡的性能和数据安全性的选择。
no:Redis仅将数据追加到AOF缓冲区,依赖操作系统决定何时将数据写入磁盘。这提供了最高的性能,但在发生故障时可能会丢失数据。
总结:always配置可能导致主线程因fsync()调用而阻塞,而everysec则通过后台线程减少了这种阻塞。无论哪种appendfsync配置,写入AOF缓冲区的操作都是在主线程执行的;区别在于数据何时以及如何被fsync()到磁盘。
AOF重写
AOF 重写时,Redis 会先执行一个内存拷贝,用于重写;然后,使用两个 日志保证在重写过程中,新写入的数据不会丢失。而且,因为 Redis 采用额外的线程进行 数据重写,所以,这个过程并不会阻塞主线程。
详解:
AOF 重写机制就是在重写时,Redis 根据数据库的现状创建一个新的 AOF 文 件,也就是说,读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写 入。比如说,当读取了键值对“testkey”: “testvalue”之后,重写机制会记录 set testkey testvalue 这条命令。这样,当需要恢复时,可以重新执行该命令,实 现“testkey”: “testvalue”的写入。
文件变小的原因是因为”多变一“
AOF 重写会阻塞吗?
和 AOF 日志由主线程写回不同,重写过程是由后台线程 bgrewriteaof 来完成的,这也是
为了避免阻塞主线程,导致数据库性能下降。
“一个拷贝”就是指,每次执行重写时,主线程 fork 出后台的 bgrewriteaof 子进程。此 时,fork 会把主线程的内存拷贝一份给 bgrewriteaof 子进程,这里面就包含了数据库的 最新数据。然后,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数 据写成操作,记入重写日志。
“两处日志”又是什么呢?
因为主线程未阻塞,仍然可以处理新来的操作。此时,如果有写操作,第一处日志就是指 正在使用的 AOF 日志,Redis 会把这个操作写到它的缓冲区。这样一来,即使宕机了,这 个 AOF 日志的操作仍然是齐全的,可以用于恢复。
而第二处日志,就是指新的 AOF 重写日志。这个操作也会被写到重写日志的缓冲区。这 样,重写日志也不会丢失最新的操作。等到拷贝数据的所有操作记录重写完成后,重写日 志记录的这些最新操作也会写入新的 AOF 文件,以保证数据库最新状态的记录。此时,我 们就可以用新的 AOF 文件替代旧文件了。
潜在风险
两种情况可能会导致主线程阻塞
风险一:Redis 主线程 fork 创建 bgrewriteaof 子进程时,内核需要创建用于管理子进程 的相关数据结构,这些数据结构在操作系统中通常叫作进程控制块(Process Control Block,简称为 PCB)。内核要把主线程的 PCB 内容拷贝给子进程。这个创建和拷贝过程 由内核执行,是会阻塞主线程的。而且,在拷贝过程中,子进程要拷贝父进程的页表,这 个过程的耗时和 Redis 实例的内存大小有关。如果 Redis 实例内存大,页表就会大,fork 执行时间就会长,这就会给主线程带来阻塞风险。
风险二:bgrewriteaof 子进程会和主线程共享内存。当主线程收到新写或修改的操作时, 主线程会申请新的内存空间,用来保存新写或修改的数据,如果操作的是 bigkey,也就是 数据量大的集合类型数据,那么,主线程会因为申请大空间而面临阻塞风险。因为操作系 统在分配内存空间时,有查找和锁的开销,这就会导致阻塞。
RDB
RDB(Redis Database,内存快照)的方式说白了就是“内存全量快照”,将Redis存储的数据做一次快照。
Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave。
save:在主线程中执行,会导致阻塞;
bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。
bgsave
Redis 采用操作系统提 供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,可以正常处理写操作。
bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。 bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。
此时,如果主线程对这些数据也都是读操作(例如图中的键值对 A),那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据(例如图中的键值对 C), 那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本 数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。
RDB和AOF的区别和缺点
RDB:在指定的时间间隔内生成内存数据的快照,并保存到磁盘上。这种方式可以在Redis重启时快速恢复数据。但是,如果Redis异常停止,自上次快照以来的所有数据变动都会丢失。
AOF:记录每次写操作命令,并追加到文件中。Redis重启时通过重新执行这些命令来恢复数据。AOF提供了更好的数据安全性,但是文件大小会不断增长,且恢复速度比RDB慢。
混合持久化
Redis 4.0 中提出了一个混合使用 AOF 日志和 RDB 内存快照的方法。简单来说,内存快照以一 定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。
混合持久化模式
混合持久化模式的设计目的是结合AOF和RDB的优点,提高数据安全性的同时也保证恢复速度。在这种模式下,Redis会定期地生成RDB快照,并追加到AOF文件的头部。这样,当需要恢复数据时,Redis首先加载RDB文件来快速恢复大部分数据,然后再执行RDB快照之后的AOF记录来恢复最近的数据变更。
这种混合方式并不是仅针对RDB的改进,而是AOF持久化方式的一种优化。通过这种方式,可以解决纯AOF模式下数据文件体积大和恢复慢的问题,同时也比纯RDB模式提供了更好的数据安全性。
优点:
混合持久化结合RDB和AOF的有点,开头为RDB格式,是的Redis可以更快速的启动,同时结合了AOF的优先,减少大量数据丢失的风险
AOF只存储上一次快照之后修改的内容,文件大小缩减
缺点:
AOF文件添加了RDB格式的内容,AOF的可读性差
配置混合持久化
在Redis配置文件中,可以通过配置aof-use-rdb-preamble参数为yes来启用混合持久化模式。这告诉Redis在AOF重写的时候,使用RDB格式的数据作为AOF文件的前缀。Redis 5.0默认开启。
aof-use-rdb-preamble yes
这种混合持久化模式自Redis 4.0以来成为了推荐的持久化策略,特别是在数据安全性和恢复速度都很关键的场景下。
启动加载过程
混合持久化的加载流程如下:
判断是否开启AOF持久化,开启继续执行后续流程,未开启执行加载RDB文件的流程;
判断appendonly.aof文件是否存在,文件存在则执行后续流程;
判断AOF文件开头是RDB的格式,先加载RDB内容再加载剩余的AOF内容;
判断AOF文件开头不是RDB的格式,直接以AOF格式加载整个文件。
淘汰策略
Redis 4.0 之前一共实现了 6 种内存淘汰策略,在 4.0 之后,又增加了 2 种策略。我们可 以按照是否会进行数据淘汰把它们分成两类:
不进行数据淘汰的策略,只有 noeviction 这一种。 会进行淘汰的 7 种其他策略。
会进行淘汰的 7 种策略,我们可以再进一步根据淘汰候选数据集的范围把它们分成两类:
在设置了过期时间的数据中进行淘汰,包括 volatile-random、volatile-ttl、volatile- lru、volatile-lfu(Redis 4.0 后新增)四种。
在所有数据范围内进行淘汰,包括 allkeys-lru、allkeys-random、allkeys-lfu(Redis 4.0 后新增)三种。
noeviction:不会进行数据淘汰
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile- lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
volatile-lfu:从已设置过期时间的数据集中挑选最少使用的数据淘汰
allkeys- lru:从所有数据集中挑选最近最少使用的数据淘汰
allkeys- random:从所有数据集中挑选任意数据淘汰
allkeys- lfu:从所有数据集中挑选最少使用的数据淘汰
LRU(Least Recently Used):最近最少使用,侧重于数据的时效性,根据数据项的访问时间来进行排序,淘汰那些最长时间未被访问的数据项
LFU(Least Frequently Used):最不经常使用,侧重于数据的访问频次,根据数据项的访问频率来进行排序,淘汰那些访问次数最少的数据项。
Redis 并没有采用数据每 被访问一次,就给对应的 counter 值加 1 的计数规则,而是采用了一个更优化的计数规 则。
简单来说,LFU 策略实现的计数规则是:每当数据被访问一次时,用计数器当前的值乘以配置项 lfu_log_factor 再加 1,再取其倒数,得到一个 p 值;然后,把这个 p 值和 一个取值范围在(0,1)间的随机数 r 值比大小,只有 p 值大于 r 值时,计数器才加 1。
不同的lfu_log_factor数值在不同的访问量,得到的counter值。
当 lfu_log_factor 取值为 1 时,实际访问次数为 100K 后,counter 值就达到 255 了,无法再区分实际访问次数更多的数据了。而当 lfu_log_factor 取值为 100 时,当 实际访问次数为 10M 时,counter 值才达到 255,此时,实际访问次数小于 10M 的不同 数据都可以通过 counter 值区分出来。
删除策略
惰性删除策略
当一个数据的过期时间到了以后,并不会立即删除数据,而是等到再有请求来读写这个数据时,对数据进行检查,如果发现数据已经过期了,再删除这个数据。
这个策略的好处是尽量减少删除操作对 CPU 资源的使用,对于用不到的数据,就不再浪费 时间进行检查和删除了。但是,这个策略会导致大量已经过期的数据留存在内存中,占用 较多的内存资源。
定期删除策略
Redis 每隔一段时间(默认 100ms),就会随机选出一定数量的数 据,检查它们是否过期,并把其中过期的数据删除,这样就可以及时释放一些内存