除了RDB持久化功能外,Redis还提供了AOF持久化功能。与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态。
举个例子:
[wy@iZbp15ck5g7od1op3kl8xnZ ~]$ redis-cli
127.0.0.1:6379> set msg "hello"
OK
127.0.0.1:6379> sadd fruits "apple" "banana" "cherry"
(integer) 3
127.0.0.1:6379> rpush numbers 128 256 512
(integer) 3
127.0.0.1:6379>
RDB持久化保存数据库的方法就是将msg,fruits,numbers三个键的键值对保存到RDB文件中,而AOF持久化保存数据库状态的方法则是将服务器执行的set,sadd,rpush三个命令保存到AOF文件中。
被写入AOF文件的所有命令都是以Redis命令请求协议格式保存的,是纯文本格式,可以直接查看AOF文件。
redis7前AOF持久化会生成一个appendonly.aof基础文件,保存路径和rdb路径一样。
redis7会创建一个appendonlydir,然后在这个文件夹中由对应的aof文件。
appendonly.aof.1.base.rdb作为基础文件。
appendonly.aof.1.incr.of,appendonly.aof.2.incr.of作为增量文件。
appendonly.aof.manifest作为清单文件。
在这个AOF文件里面,除了用于指定数据库的select命令是服务器自动添加的之外,其他的都是通过客户端发送的命令。
服务器在启动时,可以通过载入和执行AOF文件中保存的命令来还原服务器关闭之前数据库的状态。
AOF持久化功能的实现可以分为命令追加(append),文件写入,文件同步(sync)三个步骤。
当AOF持久化功能处于打开状态时,服务器在执行完一个命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区末尾。
struct redisServer
{
...
sds aof_buf; /* AOF buffer, written before entering the event loop */
...
};
举个例子:
redis服务器进程就是一个事件循环(loop),这个循环中的文件时间负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样定时运行的函数。
因为服务器在处理文件事件时可能会执行命令,使得一些内容被追加到aof_buf缓冲区中,所以在服务器每次结束一个时间循环前,都会调用flushAppendOnlyFile函数。
def eventLoop():
while True:
#处理文件事件,接受命令并且回复命令
#处理命令请求时可能会由新内容加到aof_buf缓冲区中
processFileEvents()
#处理时间事件
processTimeEvents()
#是否将aof_buf缓冲区中的数据写入和保存到aof文件中
flushAppendOnlyFile()
flushAppendOnlyFile函数行为由服务器配置的appendfsync选项的值来决定,各个不同值产生的行为如下表:
如果用户没有主动为appendfsync选项设置值,那么appendfsync选项的默认值为everysec。
文件的写入和同步:
为了提高文件写入的效率,在现代操作系统中,当用户调用write函数,将一些数据写入到文件的时候,操作系统通常会将写入的数据暂时保存在一个内存缓冲区中,等到缓冲区的空间被填满,或者超过指定的时限之后,才真正将缓冲区的数据写到磁盘中。
这种做法虽然提高效率,但也为写入数据带来了安全问题,如果计算机发生停机,会导致缓冲区里的数据丢失。
操作系统提供了fsync和fdatasync两个同步函数。它们强制让操作系统立即将缓冲区中的数据写入到磁盘里面。,从而确保写入数据的安全性。
AOF持久化的效率和安全性:
服务器配置appendfsync选项的值直接决定AOF持久化功能和安全性。
- 当appendfsync的值为always时,服务器在每一个事件循环都要将aof_buf缓冲区重的所有内容写入到AOF文件,并且同步到AOF文件,所以always的效率时最慢的一个。但是就安全性来说,always也是最安全的。因为即使出现故障停机,AOF持久化也只会丢失一个事件循环中的所有命令数据。
- 当appendfsync的值为everysec时,服务器在每个事件循环都要将aof_buf缓冲区的所有内容写入到AOF文件中,并且每隔一秒就要在子线程中对AOF文件进行一次同步。从效率上来讲,everysec模式足够快,并且就算出现故障停机,数据库也只丢失一秒中的命令数据。
- 当appendfsync的值为no时,服务器在每个事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件中,至于何时对AOF文件进行同步,则由操作系统控制。因为处于no模式下flushAppendOnlyFile调用无须执行同步操作,所以该模式下的AOF文件写入是最块的。不过因为这种模式会在系统缓存中累积一段时间的写入数据,所以该模式下单次同步时长是最长的。从平摊操作的角度来看,no模式和everysec模式效率类似。当出现故障停机时,使用no模式的服务器将丢失上次同步AOF文件之后的所有命令数据。
因为AOF文件里面包含了重建数据库状态的所有写命令,所以服务器只要读入并且重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭前的数据库状态。
步骤如下:
因为AOF持久化通过保存被执行的命令来记录数据库的状态,所以随着服务器运行时间的流逝,AOF文件中的内容会越来越多,文件的体积也会越来越大,如果不加以控制,AOF文件会第Redis服务器,甚至整个宿主计算机照成影响。并且AOF文件体积过大,使用AOF文件来进行数据恢复需要的时间会越来越多。
举个例子:
为了记录list键的状态,AOF文件就需要保存六条命令。在实际的应用程度来说,写命令执行次数和频率会高得多,所以造成的问题也会严重得多。
为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写(rewrite)功能。通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但是新AOF文件不会包含任何浪费空间的冗余命令,所以新的AOF文件的体积会比旧的AOF文件体积小得多。
虽然Redis将生成新的AOF文件替换旧AOF文件的功能命名为"AOF文件的重写",但实际上,AOF文件的重写并不需要对现有的AOF文件进行任何读取,分析或者写入操作,这个功能是通过读取现当前数据库状态实现的。
对于下面的情况:
旧的AOF文件为了保存当前键list的状态,需要在AOF文件中写入六条命令。
如果服务器想尽量少的命令来记录list键的状态,那么简单高效的方法不是去读取和分析现有的AOF文件的内容,而是直接从数据库中读取键list的值。然后用一条RPUSH list "C" "D" "E" "F" "G"命令来代替保存在AOF文件中的六条命令,这样就可以将保存list键所需的命令从六条减少为1条。
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
#写入select命令,指定数据库号码
f.write_command("SELECT", db,id)
#遍历数据库中所有的键
for key in db:
#忽略已经过期的键
if key.is_expire(): continue
if key.type == String:
rewrite_string(key)
elif key.type == List:
rewrite_list(key)
elif key.type == Hash:
rewrite_hash(key)
elif key.type == Set:
rewrite_set(key)
elif key.type == Sortedset:
rewrite_sorted_set(key)
# 如果键带有过期时间,过期时间也会被重写
if key.have_expire_time():
rewrite_expire_time(key)
f.close()
def rewrite_string(key):
#使用get命令获取字符串的值
val = Get(key)
#使用set命令重写字符串的键
f.write_command(SET, key, value)
def rewrite_list(key):
#使用lrange命令获取列表键包含的元素
item1, item2, ..., itemN = LRANGE(key, 0, -1)
#使用RPUSH命令重写列表键
f.write_command(RPUSH, key, item1, item2, ... , itemN)
def rewrite_hash(key):
#使用HGETALL命令获取哈希键
field1, value1, fleid2, value2, ..., ..., fieldN, valueN = HGETALL(key)
#使用HMSET命令重写哈希键
f.write_command(HMSET, key, field1, value1, field2, value2, ..., ..., fieldN, valueN)
def rewrite_set(key):
#使用SMEMBERS命令获取集合键包含的所有元素
elem1, elem2, ..., elemN = SMEMBERS(key)
#使用SADD命令重写集合键
f.write_command(SADD, key, elem1, elem2,...,elemN)
def rewrite_sorted_set(key):
#使用ZRANGE命令获取有序集合键包含的所有元素
member1, score1, member2, score2,...,..., memberN, scoreN = ZRANGE(key, 0, -1, "WITHSOCRES")
#使用ZADD命令重写有序集合键
f.write_command(ZADD, key, score1, member1, score2, member2, ..., ..., scoreN, memberN)
def rewrite_expire_time(key):
#获取毫秒级精度过期时间戳
timestamp = get_expire_time_unixstamp(key)
#使用PEXPIREAT命令重写键的过期时间
f.write_command(PEXPIREAT, key, timestamp)
因为aof_rewrite函数生成的新的AOF文件只包含还原当前数据库状态的必须命令,所以新的AOF文件不会浪费任何硬盘空间。
aof_rewrite函数可以很好地完成创建一个新AOF文件的任务。但是,因为这个函数会进行大量的写入操作,所以调用这个函数,线程会长时间阻塞。因为Redis服务器使用单个线程来处理命令请求,所以如果Redis服务器直接调用aof_rewrite函数,那么在重写期间,会导致服务器阻塞,无法处理客户端发过来的命令请求。
所以为了避免这种情况,Redis决定将AOF重写程序放到子进程里执行,这样做可以达到两个目的:
不过,使用子进程也有一个问题需要解决,因为子进程在进行AOF重写期间,服务器进程还需要继续处理命令请求,而新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的AOF文件所保存的数据库状态不一致。
下图展示了一个AOF重写的例子。当子进程开始进行文件重写时,数据库中只有k1一个键,但是当子进程完成AOF文件重写之后,服务器进程的数据库已经重新设置了k2,k3,k4三个键,因此,重写后的AOF文件和服务器当前的数据库状态并不一致,新的AOF文件只保存了k1一个键的数据,而服务器数据库现在却有k1,k2,k3,k4四个键。
为了解决这种数据不一致的问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用。当Redis服务器执行完一个写命令之后,他会同时将这个写明了发送给AOF缓冲区和AOF重写缓冲区。
这也就是说,在子进程执行AOF重写期间,服务器进程需要执行三个工作:
这样一来可以保证:
当子进程完成AOF重写工作之后,它会向父进程发送一个信号,父进程在接到该信号之后,会调用一个信号处理函数,并执行以下工作:
这个信号处理函数执行完毕之后,父进程就可以继续像往常一样接受命令请求了。
在整个AOF后台重写过程中,只有信号处理函数执行时会对服务器进程造成阻塞,在其他时候,AOF重写都不会阻塞父进程,这将AOF重写对服务器性能的影响降到了最低。
举个例子:
当子进程开始重写时,服务器进程的数据库只有k1一个键,当子进程完成AOF文件重写之后,服务器进程的数据库已经多出了k2,k3,k4三个新键。
在子进程向服务器进程发送信号之后,服务器进程会将保存在AOF重写缓冲区里面记录的k2,k3,k4三个键的命令追加到新的AOF文件的末尾,然后用新的AOF文件替换旧AOF文件,完成AOF后台重写工作。
以上就是BGREWRITEAOF命令的实现原理。