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