RDB持久化通过保存数据库中的键值对来记录数据库状态的不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态
11.1 AOF持久化的实现
分为命令追加、文件写入和文件同步三个步骤11.1.1 命令追加
当AOF持久化功能处于打开状态时,服务器在执行完一个写命令后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾struct redisServer {
sds aof_buf;//AOF缓冲区
};
11.1.2 AOF文件的写入和同步
Redis的服务器进程就是一个事件循环loop,这个循环中的文件事件负责接受客户端的命令请求,以及向客户端发送命令请求,而时间事件负责执行向serverCron函数这样的定时任务服务器每次结束一个事件循环之前,都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区的内容写入和保存到AOF文件里
def eventLoop():
while True :
#处理文件事件,接收命令请求以及发送命令回复
#处理命令请求时可能会有新内容被追加到aof_buf缓冲区中
processFileEvents()
#处理时间事件
processTimeEvents()
#考虑是否要将 aof_buf中的内容写入和保存到 AOF 文件里面
flushAppendOnlyFile()
flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值来决定,各个不同值产生的行为如下所示
- always:将aof_buf缓冲区中的所有内容写入并同步到AOF文件
- everysec:将aof_buf缓冲区的所有内容写入到AOF文件里,如果上次同步AOF文件的事件距离现在超过一秒钟,那么再次对AOF文件进行同步,同步操作由一个专门的线程负责执行
- no:将aof_buf缓冲区中的所有内容**写入到AOF文件里但是不同步 **
默认值为everysec
AOF持久化的效率和安全性
- appendfsync = always:效率最慢但是最安全
- appendfsync = everysec:足够快,最多丢失一秒的数据
- appendfsync = no:写入速度最快,但是最不安全11.2 AOF文件的载入与数据还原
服务器只需要读取并重新执行一遍AOF文件里面保存的写命令就可以还原服务器关闭之前的数据库状态
详细步骤
- 创建一个不带网络连接的伪客户端:因为Redis的命令只能在客户端上下文中执行
- 从AOF文件里分析并读取一条写命令
- 使用伪客户端执行被读取的写命令
4.一直重复步骤2和3,直到所有的写命令被处理完毕
11.3 AOF重写
随着服务器运行时间的增加,AOF文件里的内容会越来越多,文件的体积也会越来越大,会对Redis服务器甚至宿主机造成影响,而且AOF文件的体积越大,使用AOF文件来进行数据还原所需的时间就越多
因此Redis提供了AOF重写功能,Redis服务器创建了一个新的AOF文件来替代现有的AOF文件,两个文件的AOF保存的数据库状态相同但是新的AOF文件不包含任何浪费空间的冗余命令,所以体积会小
11.3.1 AOF文件重写的实现
这个功能实际上是通过读取服务器当前的数据库状态实现的,比如向list里写入了6条数据,如果写到AOF文件里的话,就是6行数据,但是如果读取list的值,就可以用1行数据实现。
基本原理就是从数据库读取现在键的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令
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_expired(): 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命令获取字符串键的值
value = GET(key)
#使用SET命令重写字符串键
f.write_command(SET,key,value)
def rewrite list(key):
#使用LRANGE 命令获取列表键包含的所有元素
iteml,item2,……itemN=LRANGE(key,0,-1)
#使用RPUSH命令重写列表键
f.write_command(RPUSH,key,iteml,item2,...itemN)
def rewrite_hash(key):
#使用HGETALL命令获取哈希键包含的所有键值对
fieldl,valuel,field2,value2,……fieldN,valueN = HGETALL(key)
#使用HMSET命令重写哈希键
f.write_command(HMSET, key,fieldl,valuel,field2,value2,...fieldN,valueN)
def rewrite_set(key):
#使用SMEMBERS 命令获取集合键包含的所有元素
eleml,elem2,...elemN = SMEMBERS(key)
#使用SADD 命令重写集合键
f.write_command(SADD,key,eleml,elem2,..elemN)
def rewrite_sorted_set(key):
#使用ZRANGE命令获取有序集合键包含的所有元素
member1,score1,member2,score2,……,memberN,scoreN = ZRANGE(key,0,-1,"WITHSCORES")
#使用ZADD命令重写有序集合键
f.write_command(ZADD,key,score1,member1,score2,member2,……,scoreN,memberN)
def rewrite_expire_time(key):
#获取毫秒精度的键过期时间戳
timestamp=get_expire_time_in_unixstamp(key)
#使用PEXPIREAT命令重写键的过期时间
f.write_command(PEXPIREAT,key,timestamp)
在实际中,为了避免在执行命令时造成客户端输入缓冲区溢出,重写程序在列表、哈希表、集合、有序集合这四种可能带有多个元素的键时,会先检查键所包含的元素数量,如果元素数量超过了redis.h/REDIS_AOF_REWRITE_ITEMS_PRE_CMD常量的值,程序会用多条命令来记录,而不是单一命令。目前默认值为64
11.3.2 AOF后台重写
Redis如果用主线程处理重写的话,会造成长时间阻塞,所以使用子进程来执行。 子进程在进行AOF重写的时候,服务器进程(父进程)可以继续处理命令请求;子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下保证数据的安全性。 但是要解决的一个问题是,当子进程在进行AOF重写期间,服务器进程还需要继续处理命令请求,而新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前数据库状态和重写后的AOF文件所保存的数据库状态不一致。 为了解决这个问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令或,会同时把这个命令发送给AOF缓冲区和AOF重写缓冲区。 这样就可以保证:AOF缓冲区的内容会定期被写入和同步到AOF文件,对现有AOF文件的处理工作也会如常进行;从创建子进程开始,服务器执行的所有写命令都会被记录到AOF缓冲区里,当子进程完成AOF重写工作后,会向父进程发送一个信号,父进程收到信号后,调用信号处理函数并执行以下工作:- 将AOF重写缓冲区的所有内容写入到新AOF文件里
- 对新的AOF文件进行改名,原子性地覆盖现有地AOF文件,完成替换
在整个过程里,只有信号处理函数执行时会对服务器进程造成阻塞,对服务器性能的影响降到了最低。