「重学Redis」RDB和AOF原理剖析

943 阅读11分钟

Redis持久化

Redis作为键值对内存数据库,在一般情况下出来用户的操作数据都是存在内存中

有点常识的都知道,掉电了,内存数据就没了,裂开

所以需要有持久化机制,存你的数据,在你再成功的启动Redis服务的时候恢复你之前的数据

Redis提供了两种持久化机制:RDB和AOF

RBD

RDB简单理解就是快照,执行RDB的时候,就咔嚓一下把执行那个时刻,Redis内的所有数据全都存下来,生成一个RDB文件,在Redis服务启动时会到相应的目录中找到RDB文件,读取并恢复数据。

在Redis的配置中,RDB是默认开启的,生成的配置文件名为dump.rdb(这些都是Redis配置文件中的默认配置,下面会有介绍)

时效性

看了RDB的描述,容易产生一个问题:持久化是要有时间的,比如有10g的数据,不可能一微秒就存下来,假设可能需要10分钟。那问题来了,这中间10分钟的数据发生了修改怎么办?

答:RDB是时效性的,只存指令执行那个时刻的数据,后面更新的不存。所以在存的过程中,就是上面那十分钟的那个数据,会丢失,这是RDB的机制,也是RDB的一个缺点。

触发RDB

Redis提供了两种方式用来触发RDB,用户指令下发与配置条件触发

save

直接通过在Redis client输出save指令就可以执行RDB持久化 但是save命令是阻塞的,Redis是单线程服务,一旦阻塞,其他的请求就没办法执行 所以最好不要用这个save,至少在生产环境别用

bgsave

还有个指令bgsave,这是一个不阻塞的,在后台执行RDB持久化 bgsave会创建一个子进程,子进程负责持久化,主进程继续处理服务 创建子进程的过程也是会阻塞的,但是一般很快

shutdown

当收到客户端的shutdown指令时,Redis会执行一个save指令。执行完成后关闭服务器。

配置条件触发

# Redis配置文件中关于RDB条件的配置

# 默认情况下

# 900s内至少达到一条写命令
save 900 1  
# 900s内至少达到一条写命令
save 300 10  
# 900s内至少达到一条写命令
save 60 10000  

配置save 900 1的大意是:当900秒内如果发生了1次写指令,就会触发RDB持久化

需要注意的是虽然此处配置文件写的是save,但是真正执行的其实是bgsave

其他配置

# Redis配置文件中关于RDB的相关的其他配置

# 当bgsave快照操作出错时停止写数据到磁盘,这样后面写错做均会失败
stop-writes-on-bgsave-error yes

# 是否压缩rdb文件
rdbcompression yes

# rdb文件的名称
dbfilename redis-6379.rdb

# rdb文件保存目录
dir ~/redis/

原理:fork和cow

前面说到,进行RDB的时候,由于Redis是单线程,为了保证进程能够进行在持久化的同时继续处理其他服务,Redis会创建出一个子进程,由子进程来完成持久化的任务,父进程则继续提供服务。如果此时父进程对数据进行操作,是不会影响子进程的持久化的

这个过程就可以理解成是fork,相当于影分身一个自己出来

那么问题来了:

为什么父进程的操作不会影响copy出来的子进程?难道数据也是一模一样拷贝一份吗?

如果是这样,要是父进程占10G内存,复制一个子进程出来不就总共20G内存了吗?

那不是裂开了。。。

答案当然是否定的,如果是这种方式,虽然却是能做到父进程修改数据,不影响子进程,但是这样做的代价太大了,空间消耗大且时间久

所以就有了COW(copy on write)

当子进程使用父进程已经创建好的数据时发生copy on write即先将相同的地址空间数据拷贝到另外一个地址中,然后再对此变量赋值,这样子进程可以看到父进程的数据,但是在应用程序中修改数据时确无法改变父进程的数据

ok,看不懂。。。

翻译下就是:数据在机器上有个实际的内存地址和空间,然后进程内部有自己的逻辑空间,指针的形式将逻辑空间与内存空间关联起来,那么fork出来之后其实复制的是指针,这样数据其实都是同一份。如果父进程中改了数据,那么会先把原地址的数据copy出来然后创建一个新的区域去存这个数据,然后子进程指针指向这个新地址,然后直接改原地址的那个数据的值,这样就做到了父子进程数据隔离了。

这样操作,既达到了数据隔离,速度还很快,占用的空间也很小。

图解:

image-20210204142341926

当然COW不是完美的

在fork的时候,linux内核会把父进程用的那些内存也设置为只读的(readonly),这样子进程和父进程要是都去读取这些数据是没啥问题的。

但是如果有人要改数据,那就要触发内核的一个页异常中断,中断啊,你想想,咯噔一下,要让cpu知道有人要改这个数据了,然后进行cow,那如果有很多很多要修改,疯狂触发cpu中断,对整体的性能会有很大影响。

不过呢,好在Redis读操作还是较多的,如果出现了很多的写,那么还是会对性能有点点影响的。

AOF

RDB的好处是:速度快

但是坏处很明显:RDB时,fork出来的子进程只会存那个时刻前的数据,RDB过程中的增量数据会丢了

Redis还有另一种持久化方式:AOF(Append Only File)相对于RDB,AOF更侧重数据的实时性,丢的数据少

AOF是以类似日志文件的形式,不停的记录用户的操作,恢复的时候直接读取文件中的操作记录,达到数据恢复的效果,类似于MySQL的binlog

开启与配置

默认情况下,Redis不开启AOF,在配置文件中可以手动开启

# Redis配置文件中关于AOF的启动配置

#开启AOF持久化,默认关闭
appendonly yes

其他配置

# Redis配置文件中关于AOF的其他基础配置

#AOF文件名称(默认)
appendfilename "appendonly.AOF"

#AOF文件存放目录
dir "/data/dbs/Redis/abcd"

重写

对于日志形式的记录普遍都有一个问题,就是面对日益增长的读写量,日志文件的大小也会随着不断地增长

举个例子:Redis服务开了10年,如果没有特殊的处理,AOF文件的大小会一直涨,10T、20T...即便这样存下来了数据,对于数据的恢复也是非常痛苦的,如此大的数据恢复,也需要花费很长时间

所以需要有个机制,既要能保证AOF的优点,且能解决上面说的问题,这就是Redis的重写机制Rewrite

对于Rewrite,Redis随着版本的迭代已经演化出了不同的形式

4.0前

之前的Redis重写是通过删除抵消与合并命令来达到重写的。

举个例子:Redis执行了10年,假设一直都在执行incr命令

incr num 1
incr num 2
incr num 3
incr num 4
incr num 5
incr num 6
...
incr num 100000000000000000000000000000000000000000000000000000000000000

像这种情况,Redis可以直接重写为:

set num 100000000000000000000000000000000000000000000000000000000000000

在进行AOF 重写期间,也会fork一个子进程去进行,而主进程继续接收命令。子进程根据当前的内存快照,对原AOF文件进行扫描,并把新的结果写入一个新的AOF文件(与RDB一样,保证任意时刻文件的完整性)。最后用新的AOF文件替换旧的AOF文件。

那么问题又来了:

上面说了,AOF重写时也是fork出一个子进程,根据当前内存快照进行持久化,那不是和RDB一样了吗

持久化过程中的数据一样会丢失啊?

当然这个问题是有解决方法的,针对这个情况,Redis专门提供了重写缓冲区(aof_rewrite_buff)在AOF重写期间,主进程接收到新的写命令后,不仅会添加到旧的AOF文件,还会添加到重写缓存区。在子进程完成了重写后,会先把重写缓存区的数据添加到新的AOF中,再去替换掉旧的AOF文件。

重写期间还将读写记到老AOF文件中的目的是如果重写过程出现问题,老的AOF文件还能用

重写流程如图:

image-20210204163447380

4.0后

Redis在4.0后使用了新的重写机制,将RDB与AOF两种方式进行了混合

fork出的子进程先将共享的内存副本全量的以RDB方式写入aof文件,然后在将重写缓冲区的增量命令以AOF方式写入到文件,写入完成后通知主进程更新统计信息,并将新的含有RDB格式和AOF格式的AOF文件替换旧的的AOF文件。

触发AOF

和RDB一样,AOF也有触发方式

bgrewriteaof

和bgsave类似,在后台执行AOF操作,也会fork子进程进行操作,不阻塞主进程

配置条件触发
# Redis配置文件中关于AOF的重写配置

# 当前AOF文件大小和最后一次重写后的大小之间的比率。如默认100代表当前AOF文件是上次重写的两倍时候才重写。
# 这里的数字是比例,超出了原大小比例的100%,也就是两倍就重写
auto-aof-rewrite-percentage 100   

# 只有当AOF文件大小大于该值时候才可能重写,默认是64MB
auto-aof-rewrite-min-size 64mb       

策略

涉及到IO的操作,也会有一个非常普遍的问题需要考虑:什么时候我们向文件中写数据?上面已经描述了AOF的原理,Redis的读写操作会追加到AOF文件中,难道每条命令一执行马上就直接写入文件?访问量小的时候还可以,当访问量巨大时,这种方式的文件写入,存在很大的性能问题。

所以Redis的AOF实际写入流程是:当开启AOF后,服务端每执行一次写操作就会把该条命令追加到一个单独的AOF缓冲区的末尾,然后把AOF缓冲区的内容写入AOF文件里

这里需要注意的是此处的AOF缓冲区要和重写时的重写缓存区区分,并非一个概念。

#AOF持久化策略,默认为no
appendfsync no

appendfsync有三个配置项:

  1. always:这种最极端,每执行一个事件就把AOF缓冲区的内容强制性的写入硬盘上的AOF文件里。
  2. everysec:每隔一秒才会进行一次文件同步把内存缓冲区里的AOF缓存数据真正写入AOF文件里。
  3. no:默认系统的缓存区写入磁盘的机制,不做程序强制,数据安全性和完整性差一些。

所以AOF即使丢失了数据,也是可以接受的范围内

修复文件

既然是IO操作,数据在写入文件必然是有一个过程的,假设在写入set num 1,这条指令的过程中,出现了问题,只写入了set num,这样就会导致AOF文件中出现异常的恢复语句,AOF在恢复读取文件时会出错。

针对此情况,Redis提供了一个工具redis-check-aof用于自动检查修复AOF文件中的异常

# 使用很简单
redis-check-aof --fix appendonly.aof

同理,也可用redis-check-rdb工具来修复dump.rdb

数据恢复

Redis中,AOF恢复的优先级最高,如果同时配置了RDB和AOF,会只进行AOF的恢复

如果只配置 RDB,Redis会去配置的dir下面找文件并恢复数据。