我们日常开发中,使用Redis的普遍场景就是用作缓存。也就是把后端数据库的数据存储在内存中,然后从内存读取数据,响应速度会非常的快。并且使用缓存还会降低数据库的访问压力。但是这里也有一个绝对不能忽视的问题:一旦服务器宕机,内存中的数据就会全部丢失。
为了保证数据的持久性,Redis提供了两种持久化方案:AOF日志和RDB快照。我们可以根据实际情况,在项目中灵活配置。
下面我们首先来看看AOF日志。
AOF(Append Only File),它记录了Redis收到的每一条命令,并以文本形式保存。
Redis默认不开启AOF持久化方式,我们可以修改redis.conf
文件配置开启:
# 开启aof机制
appendonly yes
# aof文件名
appendfilename "appendonly.aof"
# 写入策略 默认 everysec
# appendfsync always
appendfsync everysec
# appendfsync no
# 自动重写配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# 保存目录
dir ./
AOF是写后日志。跟MySQL的写前日志(WAL)相反。写前日志指的是,在实际写数据前,先把修改的数据记录到日志文件,以便故障时进行恢复。写后的意思是,Redis先执行命令,把数据写入内存,然后再记录日志到磁盘。
看到这里,我们就要想了,为什么AOF要先执行命令,再记日志呢?
Redis为了避免额外的性能开销,再向AOF里面记日志的时候,并不会先去检查命令的语法正确性,而是先让系统执行命令,只有执行成功之后,这条才会被记录下来,否则,系统就会报错。所以写后日志好处之一就是,防止出现错误命令的问题。
此外,另一个好处就是,因为是在命令执行之后,才去记录日志,所以不会阻塞当前的写操作。
当然了写后日志也会带来一定风险。
首先第一个:数据丢失。如果执行完一个命令,还没来得及写日志系统就发生宕机了,此时就会发生数据丢失的风险。
其次,AOF日志是在主线程中执行的,如果在日志写入磁盘的时候,磁盘写压力大,就会导致写盘很慢。我们都知道,redis是单线程的,如果主线程发生阻塞,就导致后续的操作都无法进行。
那么如何解决这两个风险呢?
聪明的你应该发现了,这两个风险都跟AOF写回磁盘的时机相关。如果我们能找到一种合适的时机,这两个风险是不是就能避免呢?我们继续看。
AOF的持久化实现分为三个步骤:
aof_buf
缓冲区的末尾;aof_buf
缓冲区的内容写入和保存到 AOF 文件,具体的写回策略由 appendfsync
选项的值来确定;在Redis的配置文件中,有这样几个配置:
# appendfsync always
appendfsync everysec
# appendfsync no
我们来总结一下这三种策略:
always策略
的性能开销,也降低了no策略
的丢失风险,最多可能会丢失1s的数据,它算是在二者之间取了个折中。三种我们策略应该如何选择呢?
注意,到这里没有结束哦。我们虽然按照系统的需求选择了写回策略,但是AOF是以文件的形式记录接收到的命令的,这时候随着写入命令的不断增加,AOF文件的体积会变得越来越大。
如果AOF太大,再往里面追加命令的时候,效率就会降低。而一旦发生宕机,用AOF恢复的速度也会非常慢。
为了避免这种问题,接下来继续AOF的重写机制。
简单点说就是根据原有的AOF文件,重新创建一个新的AOF文件,只不过这个新的AOF文件比原来的更小。
那么Redis是怎么把文件变小的呢?
原来,Redis的重写机制具有多变一的功能,也就是检查数据库的键值对,记录下键值对的最终状态,从而实现对某个键值对多次操作产生的多条命令压缩为一条的效果。
我们知道,AOF文件是以追加的方式,记录接收到的命令。当对一个键值对反复修改时,就会记录多条命令。然而在重写的时候只是记录下了当前的最新状态,这样就实现了多变一。
看到这,大家可能会问了,既然redis是单线程的,它既要执行写入命令,同时又要同步日志到磁盘,这里又冒出来一个重写机制,但是它响应的速度依然很快,这到底是咋回事呢?
Redis为了避免阻塞主线程,导致数据库性能下降。就会创建一个子线程—bgrewriteaof
,由子线程完成重写过程。
重写过程:
bgrewriteaof
子线程;同时也会把主线程的内存拷贝一份给bgrewriteaof
子线程,这里的拷贝指的是子进程复制了父进程页表,此时子线程可以共享访问父进程的内存数据了;重写触发的时机:
bgrewriteaof
指令auto-aof-rewrite-min-size
:AOF重写时文件的最小大小,默认为64MB;auto-aof-rewrite-percentage
:重写百分比,当前AOF文件比上一次重写后AOF文件的增量大小,和上一次重写后AOF文件大小的比值。到这里AOF日志基本就介绍完了,接下来我们继续看看另一种持久化方法:内存快照。
RDB(Redis DataBase)内存快照,是redis默认的持久化方式。具体就是将某一时刻的内存数据以文件的形式保存到磁盘上。
请注意,这里是保存的是数据!!! 不是操作。所以,在数据恢复的时候,我们就可以直接把RDB文件读入内存,快速完成恢复。
# 备份的频率:900秒内至少一个键被更改则进行快照
save 900 1
save 300 10
save 60 10000
# 快照创建出错后,是否继续执行写命令
stop-writes-on-bgsave-error yes
# 是否对快照文件进行压缩
rdbcompression yes
# 文件名称
dbfilename dump.rdb
# 文件保存位置
dir ./
首先我们要确认的是,我们在给内存数据做快照的时候,做的是全量快照,因为我们的数据都在内存中,为了确保可靠性,就必须把内存中的所有数据都记录到磁盘中。
Redis给我们提供了两个命令来创建快照:分别是 save
和 bgsave
。
这个时候,我们就可以通过bgsave
命令来执行全量快照,这样既提供了数据的可靠性保证,同时也避免了对redis的性能影响。
接下来,我们需要关注一个问题。在对内存数据做快照时,这些数据还能被修改吗?
如果能修改,意味着Redis还能正常处理写操作,否则的话,就要等所有快照写完才能执行,这会大大降低性能。
这里我们先给出答案:在对内存做快照时,这些数据肯定还是可以被修改的。
RDB采用写时复制(COW,copy on write)策略。在执行快照的同时,正常处理写操作。
简单来说,Redis 在持久化时会调用glibc
的函数fork
,产生一个子进程,快照持久化此时就交给子进程来处理,父进程则继续处理客户端请求。
子进程在做持久化的时候,不会对现有的内存数据结构进行修改,它只是进行遍历读取,然后序列化写到磁盘中。但是父进程不一样,它必须持续接受客户端请求,然后对内存数据结构进行修改。
如下图所示:如果主线程对数据是读操作,那么,主线程和子进程相互不影响。如果主线程要修改一块数据,那么这块数据就会被复制一份,生成该数据的副本。然后主线程对这个副本进行修改。
这既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。
为了提高系统的可靠性,防止宕机导致的数据丢失,我们肯定希望快照的时间越短越好。我们可能会想,通过bgsave子线程来执行快照,这样既不会阻塞主线程,同时也尽可能地少丢失数据。但是这样真的是完美的吗?
答案是否定的。虽然 bgsave 执行时不阻塞主线程,但是如果频繁的执行全量快照也会有两方面的开销:
那我们该如何处理呢?
此时,我们可以做增量快照,也就是,在做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。
但是,这么做的前提是,我们需要记住哪些数据被修改了。这会带来额外的空间开销问题。
AOF每次记录的是操作命令,一般需要持久化的数据量不大。只要不是设置的always
方式,对性能不会造成太大影响。但是在数据恢复时,需要把所有的命令都执行一遍。如果操作日志很多,redis恢复的速度就会很慢,可能会影响到正常使用。
而RDB快照的方式就弥补了这一点,它每次记录的是数据,redis在故障恢复的时候速度就会很快。但是,RDB的问题是,它执行快照的频率不好控制,如果频率太快会对系统带来性能影响,如果频率太慢就会造成更多的数据丢失。
那么,有没有方法既能利用 RDB 的快速恢复,又能以较小的开销做到尽量少丢数据呢?
当然有,下面继续Redis 4.0 混合持久化。
混合持久化,就是将RDB文件的内容和增量的AOF日志文件存在一起。
简单来说,内存快照是以一定频率执行的,那么在两次快照之间,使用AOF 日志记录发生的增量操作。如下图所示:
T1 和 T2 时刻的修改,用 AOF 日志记录,等到第二次做全量快照时,就可以清空 AOF 日志,因为此时的修改都已经记录到快照中了,恢复时就不再用日志了。
这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势,提高了数据恢复效率,以及数据的可靠性。
如果你还想看更多优质原创文章,欢迎关注我的公众号「ShawnBlog」。