有关RDB持久化的可以看这个
除了RDB
持久化功能之外,Redis
还提供了AOF(Append Only File)
持久化功能。与RDB
持久化通过保存数据库中的键值对来记录数据库状态不同,AOF
持久化是通过保存Redis
服务器所执行的写命令来记录数据库状态的。
例如执行下面的几条命令:
redis> SET msg "hello"
OK
redis> SADD fruit "apple" "banana" "cherry"
(integer) 3
redis> RPUS numbers 128 256 512
(integer) 3
RDB
持久化的话,就会将msg、fruit、numbers
三个键的键值对保存到RDB
文件中;而AOF
则会将这三个命令保存到AOF
文件中,还原数据库时,再执行这三条命令进行数据还原
被写入到AOF
文件中的命令是按照Redis
的命令请求协议格式保存的,因为Redis
的命令请求格式是纯文本格式,所以可以直接打开AOF
文件,例如上面的命令,在AOF
文件中会是这样保存的:
的一行的SELECT
命令是服务器自动添加的,其他均是客户端输入的命令
AOF
持久化功能的实现分为三步:
AOF
缓冲区AOF
缓冲区的内容写入和保存到AOF
文件里AOF
缓冲区的内容直接写入到AOF
文件中,但实际上,它会先将内容写到一个内存缓冲区中,等到缓冲区满了之后,再一次性写入到AOF
文件中命令追加
当AOF
持久化功能打开时,服务器执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf
缓冲区的末尾:
struct redisServer{
//...
//AOF缓冲区
sds aof_buf;
//...
};
例如,执行下面这条命令后:
redis> SET KEY VALUE
OK
会将以下的内容追加到aof_buf
缓冲区末尾:
文件的写入与同步
Redis
的服务器进程就是一个事件循环,这个事件循环里面存在文件事件与时间事件。
serverCron
函数(例如RDB
持久化功能中通过save
设置保存条件,就在这个函数中得到运行)这样需要定时运行的函数因为服务器在处理文件事件时可能会执行写命令,从而追加一些内容到aof_buf
缓冲区,所以在每次事件循环结束前,需要调用flushAppendOnlyFile
函数,考虑是否将aof_buf
缓冲区的内容写入和保存到AOF
文件中。这个过程可以用以下伪代码表示:
def eventLoop():
while True:
# 处理文件事件,接受命令请求以及发送命令回复
# 处理命令请求时可能会有新内容被追加到aof_buf缓冲区中
processFileEvents()
# 处理时间事件
processTimeEvents()
# 考虑是否要将aof_buf中的内容写入和保存到AOF文件中
flushAppendOnlyFile()
flushAppendOnlyFile
函数的行为由服务器配置的appendfsync
选项的值设定,这个值决定AOF
文件的同步频率
在上面有提及到文件同步,操作系统会先将数据写入到内容缓冲区,而非直接写到文件,等到内容缓冲区满了之后,再一次性写入到文件中。这样有助于提高写入效率,但也带来一个问题:如果数据写入到内容缓冲区了,但是在将内容写入到文件之前,计算机停机了,那么保存在内存缓冲区中的数据将会丢失。
appendfsync 选项的值 | flushAppendOnlyFile 函数的行为 |
---|---|
always | 将aof_buf 缓冲区的所有内容写入并同步到AOF 文件 |
everysec | 将aof_buf 缓冲区的所有内容写入到AOF 文件,如果距离上次同步的时间距离超过一秒钟,将对AOF 文件再次进行同步,并且有一个专门的线程进行同步 |
no | 将aof_buf 缓冲区的所有内容写入到AOF 文件,但不对AOF 文件进行同步,何时同步由操作系统决定 |
如果没有主动为appendfsync
设定值,那么默认值会被设置为:everysec
不同的值带来的效率与安全性影响如下:
always
:因为在每个事件循环都要进行一次AOF
文件同步,所以效率是三种选项之中最低的;但从安全性来讲,是最安全的,因为即使出现停机故障,也只会丢失一个事件循环中产生的命令数据everysec
:在每个事件中写入数据到AOF
文件,并且每隔一秒就执行一次同步操作,效率足够快,安全性得到保障。因为即使出现故障,也只会丢失这一秒的命令数据no
:每个事件循环中将数据写入到AOF
文件,但至于何时对AOF
文件进行同步则由操作系统决定。有可能在内存中积累一段时间的写入数据,因此这种模式下的单次同步时长通常是最长的;出现故障时,会丢失到上次同步为止的所有数据。当使用AOF
文件重建数据库时,服务器只需要读入并重新执行一次AOF
文件里面保存的命令即可,步骤如下:
AOF
文件中,所以不需要使用网络连接AOF
文件中分析并读取一条写命令AOF
文件中的命令处理完为止因为AOF
文件是通过保存客户端执行的写命令来记录数据库状态,随着服务器的运行,这个文件会越来越庞大,如果不加以控制的话,会对服务器乃至计算机造成影响。并且文件过大,还原数据库时需要的时间就越长。
例如执行以下命令:
redis> RPUSH list "A" "B"
(integer) 2
redis> RPUSH list "C"
(integer) 3
redis> RPUSH list "D" "E"
(integer) 5
redis> LPOP list
"A"
redis> LPOP list
"B"
redis> RPUS list "F" "G"
(integer) 5
光是为了记录这个list
键的状态就需要保存六条命令。
为了解决AOF
文件体积膨胀的问题,Redis
提供了AOF
文件重写(rewrite
)功能。Redis
服务器可以创建一个新的AOF
文件来代替现有的AOF
文件,新旧两个AOF
文件所保存的数据库状态相同,但新的AOF
文件不会保存任何浪费空间的冗余命令,所以新的AOF
文件体积会比旧的小得多。
AOF
文件重写不需要对旧的AOF
文件进行任何读取,而是读取服务器当前的数据库状态来实现的。
以上面的例子为例,执行六条命令后,服务器中就存在了一个列表键,键名list
,值为:[“C”,”D”,”E”,”F”,”G”]
,服务器可以使用一条命令:RPUSH list “C” ”D” ”E” ”F” ”G”
来替代保存在AOF
文件中的六条命令。
所有类型的键都可以用同样的方式来减少AOF
文件中的命令数量:首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是AOF
文件重写的原理
整个重写过程的伪代码如下所示:
def aof_rewrite(new_aof_file_name):
# 创建新AOF文件
f=create_file(new_aof_file_name)
# 遍历数据库
for db in reidsServer.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_strng(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.hava_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命令获取列表键包含的所有元素
item1,item2,...,itemN=LRANGE(key,0,-1)
# 使用RPUSH命令重写列表键
f.write_command(RPUSH,key,item1,item2,...,itemN)
def rewrite_hash(key):
# 使用HGETALL命令获取哈希键包含的所有键值对
field1,value1,field2,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,"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_AOF_REWRITE_ITEMS_PER_CMD
常量的值,那么重写程序会使用多条命令来记录键的值。
比如,假设该常量值为64,并且有一个列表有100个元素,那么重写程序会分为两条命令来存储这100个元素,第一条命令存储前64个,第二条命令存储剩下的元素。
因为重写操作需要进行大量的写入操作,而Redis
服务器使用单个线程来处理命令请求,所以如果由服务器直接调用aof_rewrite
函数的话,那么在重写AOF
文件期间,服务器无法响应客户端发来的请求。基于此,Redis
决定将AOF
文件重写放到子进程里执行。这样做的好处:
AOF
重写期间,服务器进程(父进程)可以继续处理命令请求使用子进程进行AOF
文件重写,需要考虑一个问题:在子进程进行重写期间,如果客户端继续执行一批写命令,新命令会对数据库状态进行修改,而子进程拿到的又是原先的数据副本,从而使得服务器当前的数据库状态与重写后的AOF
文件所保存的数据库状态不一致。
为了解决这个问题,Redis
设置了一个AOF
重写缓冲区,这个缓冲区会在服务器创建子进程之后开始使用,当Redis
服务器执行完一个命令后,它会同时将写命令发送给AOF
缓冲区和AOF
重写缓冲区。
那么在子进程执行AOF
重写期间,服务器进程与子进程做的工作都有:
AOF
缓冲区,保证了AOF
缓冲区的内容会定期被写入和同步到AOF
文件AOF
重写缓冲区,从创建子进程开始,服务器执行的写命令都会被记录到AOF
重写缓冲区中AOF
重写工作后,会向父进程发送一个信号,父进程接收到信号后,会调用一个信号处理函数
AOF
文件中AOF
文件进行改名,原子地(atomic
)覆盖现有的AOF
文件,完成新旧AOF
文件的替换这也就是BGREWRITEAOF
命令的实现原理