AOF持久化与前面提到的RDB持久化不同,RDB持久化是保存数据库中的键值对来记录数据库状态不同,AOF持久化则是通过保存Redis服务器所执行的些命令来记录数据库的状态,保存在AOF文件中,在AOF文件中,除了用于指定数据库的SELECT命令是服务器自动添加的以外,其他都是客户端发送的命令。
AOF持久化的实现
AOF持久化功能的实现分为了命令追加(Append)、文件写入、文件同步(Async)这三个步骤。
命令追加
当AOF持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾,该缓冲池也是RedisServer的一部分
struct redisServer{
//。。。
//AOF缓冲区
sds aof_buf;
//....
};
举个栗子
set msg 123
当执行完上述这个命令时,就会将这条命令变成协议形式,然后追加到aof_buf缓冲区的末尾(此时还没有写入AOF文件中)
AOF文件的写入与同步
Redis的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数。
服务器每次结束一个事件循环的之前,会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面,这个过程伪代码表示如下
def eventLoop(); //事件循环,即整个Redis进程
while True:
//处理文件事件,接收用户命令和处理命令后返回结果给用户
//再处理的时候,aof_buf缓冲池可能会添加新内容
processFileEvents();
//处理时间事件
processTimeEvents();
//考虑是否将aof_buf缓冲中的内容写入和保存到AOF文件里面
flushAppendOnlyFile();
End While
End eventLoop;
flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项(fsync代表同步)的值来决定,各个不同值产生的行为如下面所示
下面是配置文件里面的信息
appendfsync选项 | flushAppendOnlyFile函数 |
---|---|
always | 将aof_buf缓冲区中的所有内容写入并同步到AOF文件 |
everysec(默认值) | 将aof_buf缓冲区的所有内容写入到AOF文件,如果上次同步AOF文件的事件距离现在超过1秒钟,那么再次对AOF文件进行同步,并且这个同步操作是由一个线程专门负责执行的 |
no | 将aof_buf缓冲区的所有内容写入到AOF中,但并不对AOF文件进行同步,何时同步由操作系统来决定 |
3种方式的比较
- 当为awlays时,服务器在每个事件循环都会将aof_buf缓冲区的所有内容写入到AOF文件中,并且立刻同步AOF文件(即保存,只有保存了,宕机才不会丢失),所以always效率是最低的,但安全性是最高的,顶多只会丢失一个事件循环里面的命令
- 当为everysec时,服务器在每个事件循环都会将aof_buf缓冲区的所有内容写入到AOF文件中,此时不会立刻同步,而是看上一次同步的时间是否相隔一秒以上,每隔一秒才会对AOF文件进行同步(保存),效率会比较快,安全性上,最多会丢失1秒钟的数据
- 当为no时,服务器在每个事件循环都会将aof_buf缓冲区的所有内容写入到AOF文件钟,何时进行同步交由操作系统进行决定,效率最快,但安全性上也最低(完全由操作系统决定损失多少,总的来说会损失上次同步后的所有数据)
即always只要进入了flushAppendOnlyFile就会将缓冲区的内容同步到AOF文件中**(这是最安全的,但也是最慢的,因为访问磁盘次数最多),而everysec则是每隔一秒以上才会进行同步到AOF文件中(这个比较保守,会比always快,但会比always危险一点,假如有一次Loop循环少于1秒完成,那么这个Loop循环的命令会保留在aof_buf缓冲中,不会同步到AOF文件中,假如下一次Loop循环发生宕机,命令来不及写入AOF文件里,那么这个就恢复不了了)**,no选项是最快的,他自己不会决定AOF何时同步aof_buf里面的数据,全部交由操作系统来决定,这种方式是最快,但是最不安全的。
AOF文件的载入与数据还原
AOF文件载入
AOF文件里面包含了重建数据库状态所需的所有写命令(只有写),所以服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态。
详细步骤如下:
- 首先创建一个不带网络连接的伪客户端(fake client):因为Redis命令只能在客户端那里执行,所以需要客户端才可以执行AOF保存的Redis命令,不带网络连接是因为命令就在本地,不需要网络连接来传输命令
- 从AOF文件中分析并且读取出一条写命令
- 使用伪客户端去执行读出的写命令
- 判断AOF文件里面的所有写命令是否已经被处理完,如果未处理完,返回步骤2,直到命令处理完为止
但服务器首先读入的命令是切换数据库命令,之后才是一系列的写命令。
AOF重写
AOF持久化是通过保存被执行的写命令来记录数据库状态的,随着服务器运行时间的增长,AOF文件中的内容也是会越来越多的,文件的体积会越来越大,如果不加以控制,体积过大的AOF文件就很可能对Redis服务器、甚至整个计算机都会造成影响,并且AOF文件的体积越大,数据库还原的时间也是会越来越久。
//redis命令
rpush list 1 2
rpush list 3 4
rpush list 5 6
lpop list
lpop list
假如redis执行了上面的命令,那么AOF文件中,就保留有5条命令,这5条命令完全可以由1条命令去替代,产生了冗余命令的问题
rpush list 1 2 3 4
为了解决体积膨胀问题,Redis提供了AOF文件重写功能(rewrite),该功能会创建一个新的AOF文件来替代现有的AOF文件,新的AOF文件不含有浪费AOF文件空间的冗余命令,所以新的AOF文件的体积会比旧的小很多。
AOF文件重写的实现
虽然文件重写是将新生成的AOF文件替代旧的AOF文件,但其实这整个重写过程不需要对旧的AOF文件进行读取,该功能是通过读取服务器当前数据库的状态去实现的。
AOF文件重写实现的原理是从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令。
def aof_rewrite(new_aof_file_name)
//创建新的AOF文件
f = create_file(new_aof_file_name)
//遍历数据库
for db in redisServer.db;
//忽略空的数据库
if db.is_empty();continue
//数据库不为空,先写入切换命令
f.write_command("SELECT"+db.id);
//遍历数据库中的所有键
for key in db:
//忽略已经过期的键
if key.is_expired();continue
//根据键的类型对键进行重写
if key.type == String;
rewrite_string(key);
else if key.type == List;
rewrite_list(key);
else if key.type == Hash;
rewrite_hash(key);
else if key.type == Set;
rewrite_set(key);
else if key.type == ZSet;
rewrite_zset(key);
//如果键带有过期时间,过期时间也要被重写进去
if key.have_expire_time();
rewrite_expire_time(key)
//写入完毕
f.close()
End aof_rewrite;
根据当前键值对的类型去调用不同的方法进行写入,里面的步骤大体都是一样的,比如说String类型,则是调用GET命令获取值,然后再重写SET命令,将重写的SET命令写入AOF文件中,其他类型也是一样,调用命令获取值,然后重写对应命令即可,这样就减少了很多的冗余命令。
注意
在实际中,为了避免执行命令时造成客户端输入缓冲区溢出(AOF文件重写时,会占用输入缓冲区,当前用户的输入缓冲区会受影响),所以在对可以存储多个元素的键时,会检查包含的元素数量,如果数量超过了服务器架构里面的默认值,即redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,重写日志的时候将使用多条命令去储存,而不单单使用一条命令
AOF后台重写(BGREWRITEAOF命令)
aof_rewrite函数可以创建一个新AOF文件,但由于不是异步的,会对线程造成阻塞,特别是当AOF写入的命令特别多,次数频繁时,阻塞的现象越严重,因为Redis又是单线程的,所以会在AOF文件重写时,服务器端可能无法处理客户端发来的命令请求。
所以Redis也开启了一个进程来进行AOF文件重写,这样做有以下优点
- 开启了另一个进程去进行重写,不会对父进程造成影响
- 由于进程的数据不共享,所以可以在不加锁的情况下,保证数据的安全
但同时也出现了问题,假如使用子进程去进行重写AOF,如果在重写期间,父进程继续处理命令,新的命令可能会对当前数据库进行修改,造成了数据库状态和AOF文件里面的命令状态不一致的问题。
举个栗子
set msg v1
set msg v2
set msg v3
//创建子进程,执行AOF文件重写
//重写未完成,继续修改,因为文件重写,不能利用将命令放回旧文件中,
set msg v4
set msg v5
//AOF文件重写完成,此时才可以进行刷新aof_buffer,里面保存的是set msg v3
//下一步如果不进行修改了,那就不再触发AOF,AOF保存的命令也是错的
所以为了解决这个问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后就会开始使用,当Redis执行完一个写命令之后,他会同时将这个写命令发送给AOF缓冲区(aof_buffer)和AOF重写缓冲区。
所以,在创建子进程执行AOF命令之后,需要进行三个动作,分别如下
- 执行客户端发送来的命令
- 将执行后的写命令追加到AOF缓冲区中(aof_buffer)
- 将执行后的写命令追加到AOF重写缓冲区中
这样就可以保证
- AOF缓冲区的内容可以定期被写入和同步到现有AOF文件中,对现有的AOF文件不会造成影响
- 另一方面就是从创建子进程开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区中,用于追加
当子进程完成AOF重写工作之后,此时会向父进程那里发送一个信号,父进程在接到该信号之后,再调用一个信号处理函数,执行以下工作
- 将AOF重写缓冲区中的所有内容写入到新创建的AOF文件中,这是新的AOF文件所保存的数据库状态将和服务器当前的数据库状态一致
- 对新的AOF文件进行改名,原子地覆盖现有的AOF文件,完成新旧两个AOF文件的替换。
总的来说,AOF使用了后台重写缓解了一点阻塞影响,但是在信号处理函数的时候,还是会造成阻塞(不过一般会比后台重写阻塞影响小)
重点
- AOF文件通过保存命令的方式来记录服务器的数据库状态
- AOF文件中的所有命令都会以Redis命令请求协议的格式保存
- 命令请求会先保存到AOF缓冲区里面,之后再根据appendfsync选项来进行写入和同步到AOF文件中
- appendfsync选项的不同值对AOF持久化功能的安全性和Redis服务器的效率有影响
- AOF重写会产生新的AOF文件,这个文件会过滤掉冗余命令,变成一条Redis,不过对可以存储多个元素的键值对,有长度限制,超过长度就会变成多条Redis
- AOF重写无须读取旧的AOF文件
- 执行BGREWRITEAOF命令时,如果碰到BGSAVE,不会执行BGSAVE,如果执行BGSAVE期间,碰到BGREWRITEAOF,会延缓执行BGREWRITEAOF(前面RDB提到过)