redis有两种持久化方式,分别是RDB持久化和AOF持久化,RDB持久化通过保存数据库中的键值对来保存数据库的状态,而AOF持久化是通过保存服务器执行的写命令来保存数据库的状态。
1、RDB持久化
RDB持久化既可以手动执行,也可以根据服务器的配置选项定期执行,该功能可以将某个时间点上的数据库保存到一个RDB文件中。
1.1 RDB手动执行
手动执行有两个命令可以生成RDB文件,分别是:
(1)SAVE
SAVE命令会阻塞服务器进程,直到RDB文件创建完毕为止。在服务器阻塞期间,服务器不能处理任何命令请求。
(2)BGSAVE
BGSAVE命令会fork出一个子进程,然后由子进程负责创建RDB文件,服务器进程继续处理命令请求。子进程在把所有键值对保存到RDB文件之后,会向父进程发送一个退出的信号,父进程每次时间事件调用serverCron函数时,都会以非阻塞的方式调用wait()函数,判断子进程是否发送出信号,从而处理一些子进程执行结束之后的善后工作。在BGSAVE命令执行期间,服务器处理SAVE、BGSAVE和BGREWRITEAOF三个命令和平时有所不同。
- 在BGSAVE命令执行期间,客户端发送的SAVE命令和BGSAVE命令会被拒绝执行,BGREWRITEAOF命令会被延迟到BGSAVE命令执行完毕之后执行。
- 在BGREWRITEAOF命令正在执行,那么客户端发送的BGSAVE命令会被服务器拒绝。
BGREWRITEAOF和BGSAVE都由子进程去执行,他两不能同时执行的原因只是一个性能方面的考虑,并发出两个子进程会执行大量的磁盘IO操作。需要注意的是,在redis启动时,因为AOF的更新频率要比RDB的更新频率高,所以,如果AOF功能开启,服务器会优先使用AOF来还原数据库的状态。只有在AOF持久化功能处于关闭时,服务器才会使用RDB文件来还原数据库的状态。
1.2 RDB定期执行
由于BGSAVE命令是在子进程中执行的,所以redis允许用户通过设置服务器的配置选项save让服务器没隔一段时间自动执行一次BGSAVE。举个例子:
save 900 1
save 300 10
save 60 10000
只要满足以下三个条件中的任何一个,BGSAVE命令就会被执行:
- 900s内,对服务器至少执行了一次写修改
- 300s内,对服务器进行了至少10次写修改
- 60s内,对服务器进行了至少10000次修改
此外,服务器还维护着一个dirty计数器和一个lastsave属性:
- dirty计数器记录距离上一次成功执行SAVE或者BGSAVE命令之后,服务器对数据库状态执行了多少次修改;
- lastsave属性是一个UNIX时间戳,记录了服务器上一次成功执行SAVE或者BGSAVE的时间。
BGSAVE的定期执行主要是在时间事件serverCron函数中调用的,serverCron函数每次调用时,都会检查BASAVE执行的条件是否满足,如果满足设置的条件,则会后台执行BGSAVE命令。
2、AOF持久化
2.1 AOF持久化的实现
服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器的server.aof_buf缓冲区的末尾。服务器在每次结束一个事件循环之前都会调用beforeSleep函数,该函数都会调用flushAppendOnlyFile这个函数,考虑是否要将server.aof_buf的缓冲区的内容写入和保存到AOF文件中。该函数的行为由配置的appendfsync选项的值来决定。
- always:每个事件循环都将缓冲区的内容写到aof文件中,并且同步aof文件,即执行操作系统的fsync函数;
- everysec:每个事件循环都将缓冲区的内容写到aof文件中,并且每隔1s就在子线程中对aof文件进行同步一次,执行一次fsync函数;
- no:每个事件循环都将缓冲区的内容写到aof文件中,至于何时对aof文件进行同步,由操作系统决定。
2.2 AOF数据的载入
服务器在启动时,初始化的最后一步就是要从磁盘中加载持久化的文件,调用的函数为loadDataFromDisk,其流程如下:
由于RDB文件的持久化频率远低于AOF持久化的频率,AOF持久化一般是每次进入事件循环之前,都会进行一次AOF持久化,而RDB持久化只有在满足一定的条件之后才会进行。因此从文件中加载数据库的数据时,如果打开了AOF持久化的功能,那么优先从AOF文件中读取文件初始化数据库。读取AOF文件并还原数据库状态的详细步骤如下:
(1)创建一个不带网络连接的伪客户端。因为redis的命令只能在客户端的上下文中执行,所以redis使用一个不带网络连接的伪客户端来执行aof文件保存的写命令;
(2)从aof文件分析并读取一条写命令;
(3)使用伪客户端执行读出的写命令;
(4)一直执行步骤(2)和步骤(3)直到AOF的写命令全部执行完。
2.3 AOF重写
因为AOF持久化是通过保存被执行的写命令来记录数据库状态的,所以随着服务器运行,AOF文件的内容会越来越多,文件的体积会越来越大。为了解决AOF文件膨胀问题,redis提供了AOF文件重写功能。通过该功能,redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新文件中不会包含任何冗余的命令。
(1)AOF重写的两种方式
AOF重写既可以通过手动执行,也可以通过配置文件配置好参数定期执行:
- 手动执行:主要通过BGREWRITEAOF这个命令来实现
- 定期执行:主要通过判断配置文件的auto-aof-rewrite-percentage这个参数,当满足条件时,AOF会进行自动后台重写。
(2)AOF后台重写
由于AOF重写会进行大量的写入操作,而redis服务器采用单线程实现,如果服务器在主线程中进行AOF重写,在重写期间,服务器将无法处理客户端发来的命令请求。所以redis决定将AOF重写程序放到子进程中执行。这样做有两个好处:
- 子进程对AOF重写期间,服务器进程可以继续处理命令请求。
- 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以避免使用锁的情况下,保证数据的安全性。
(3)AOF重写缓冲区
不过使用子进程也有一个问题,因为子进程在执行AOF重写期间,服务器还要处理客户端请求,而新的命令会对现有数据进行修改,从而使得当前的数据库状态和AOF保存的数据库状态不一致。为了解决这个问题,redis服务器设置了一个AOF重写缓冲区server.aof_rewrite_buf_blocks,这个缓冲区在创建子进程后使用,当redis执行完了一个写命令之后,如果正在执行AOF重写,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区,这样可以保证:
- 父进程中AOF缓冲区的内容会定期写入和同步到AOF文件中,对现有的AOF文件的处理工作会如常进行
- 创建子进程开始,服务器执行的所有写命令都会记录到AOF重写缓冲区里面
当子进程完成重写之后,它会向父进程发送一个信号,父进程收到这个信号后,会调用一个信号处理函数,并执行以下工作:
- 将AOF重写缓冲区的所有内容写到新的AOF文件中
- 对新的AOF文件进行改名,原子地覆盖现有的AOF文件,完成新旧两个AOF文件的替换。
这个信号处理完成之后,父进程就可以像往常一样接收命令请求了。在整个AOF后台重写过程中,只有信号处理函数会对服务器进程造成阻塞,其它时候都不会阻塞父进程,这将AOF重写对服务器性能造成的影响降到了最低。