AOF文件的写入与同步
Redis服务器进程就是一个时间循环(loop),这个循环中的文件时间负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数。因为服务器在处理文件事件时可能会执行些命令,使得一些内容被追加到aof_buf缓冲区里面,所以
在服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,考虑是否将aof_buf缓冲区中的内容写入和保存到AOF文件里面,这个过程可以用以下伪代码表示
def eventLoop():
while True :
# 处理文件事件,接收命令请求以及发送命令回复
# 处理命令请求时可能会有新内容被追加到aof_buf缓冲区中
processFileEvents()
# 处理时间事件
processTimeEvents()
# 考虑是否要将aof_buf中的内容写入和保存到AOF文件里面
flushAppendOnlyFile()
flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值来决定,各个不同的值产生的行为也不同,如果用户没有主动为appendfsync选项设置值,那么appendfsync选项的默认值为everysec
文件的写入和同步
为了提高文件的写入效率,在现代操作系统中,当用户调用write函数,将一些数据写入到文件的时候,操作系统通产会将写入数据暂时保存在一个内存缓冲区里面,等到缓冲区的空间被填满,或者超过了指定
的时限之后,才真正地将缓冲区中的数据写入到磁盘里面。这种做法虽然提高了效率,但也为写入数据带来了安全问题,因为如果计算机发生停机,那么保存在内存缓冲区里面的写入数据将会丢失。为此,系统提供
了fsync和fdatasync两个同步函数,它们可以强制让操作系统立即将缓冲区中的数据写入到硬盘里面,从而确保写入数据的安全性
AOF持久化的效率和安全性
服务器配置appendfsync选项的值直接决定AOF持久化功能的效率和安全性。
- 1.当appendfsync的值为always时,服务器在每个事件循环都要讲aof_buf缓冲区中的所有内容写入到AOF文件并且同步AOF文件,所以always的效率时appendfsync选项三个当中最慢的一个,但从安全性来说,always也是最安全的,因为即使出现故障停机,AOF持久化也只会丢失一个事件循环中所产生的命令数据
- 2.当appendfsync的值为everysec时,服务器在每个事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件并且每隔一秒就要在子线程中对AOF文件进行一次同步。从效率上来讲,everysec模式足够快,并且就算出现故障停机,数据库也只丢失一秒钟的命令数据
- 3.当appendfsync的值为no时,服务器在每隔事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件,至于何时对AOF文件进行同步,则由操作系统控制。因为处于no模式下的flushAppendOnlyFile调用无须执行同步操作,所以该模式下的AOF文件写入速度总是最快的,不过因为这种模式会在系统缓存中积累一段时间的写入数据,所以该模式的单次同步时长通常是三种模式中时间最长的。从平摊操作的角度来看,no模式和everysec模式的效率类似,当出现故障停机时,使用no模式的服务器将丢失上次同步AOF文件之后的所有写命令数据
例子
- 例如,对于以下AOF文件来说
*2
$6
SELECT
$1
0
*5
$4
SADD
$6
fruits
$6
banana
$6
cherry
$5
apple
*3
$3
SET
$3
msg
$5
hello
*5
$5
RPUSH
$7
numbers
$3
128
$3
256
$3
512
AOF文件的载入与数据还原
因为AOF文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态。Redis读取AOF文件并还原数据库状态的详细步骤如下:
- 1.创建一个不带网络连接的伪客户端(fake client):因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令,伪客户端执行命令的效果和带网络连接的客户端执行命令的效果完全一样。
- 2.从AOF文件中分析并读取出一条写命令
- 3.使用伪客户端执行被读出的写命令
- 4.执行步骤2和步骤3,直到AOF文件中的所有写命令被处理完毕为止
当完成以上步骤之后,AOF文件所保存的数据库状态就会被万丈地还原出来,整个过程如图
服务器首先读入并执行SELECT 0命令,之后是SET msg hello命令,在之后是SADD fruits apple banana cherry命令,最后是RPUSH numbers 128 256 512 命令,当这些命令都执行完毕之后,服务器的数据库就被还原到之前的状态了,
整个重写过程可以用以下伪代码表示:
def aof_rewrite(new_aof_file_name):
# 创建新的AOF文件
f = create_file(new_aof_file_name)
# 遍历数据库
for db in redisServr.db:
# 忽略空数据
if db.is_empth(): 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_expir_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, item3, ....itemN = LRANGE(key, 0, -1)
# 使用RPUSH命令重写列表键
f.write_command(RPUSH, key, item1, item2, item3,...,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, elem3,....,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, "WOTJSCPRES")
# 使用ZADD命令重写有序集合键
f.write_command(ZADD, key, score1, memeber1, score2, member2, ...., scoreN, memberN)
def rewrite_expire_time(key):
# 获取毫秒精度的键过期时间戳
timestamp = get_expire_time_in_unixstamp(key)
# 使用PEXIREAT命令重写键的过期时间
f.write_command(PEXIREAT, key, timestamp
)
因为aof_rewrite函数生成的AOF文件只包含还原当前数据库状态所必须的命令,所以新AOF文件不会浪费任何硬盘空间
AOF文件重写的实现
虽然Redis将生成新AOF文件替换旧AOF文件的功能命名为"AOF文件重写",但实际上,AOF文件重写并不需要对现有的AOF文件进行任何读取、分析或者写入操作,这个功能是通过读取服务器当前的数据库状态来实现的。
例子
- 举个例子,如果对服务器对list键执行了以下命令:
127.0.0.1:6379> RPUSH list "A" "B" // ["A", "B"]
(integer) 2
127.0.0.1:6379> RPUSH list "C" // ["A", "B", "C"]
(integer) 3
127.0.0.1:6379> RPUSH list "D" "E" // ["A", "B", "C", "D", "E"]
(integer) 5
127.0.0.1:6379> LPOP list // ["B", "C", "D", "E"]
"A"
127.0.0.1:6379> LPOP list // ["C", "D", "E"]
"B"
127.0.0.1:6379> RPUSH list "F" "G" // ["C", "D", "E", "F", "G"]
(integer) 5
那么服务器为了保存当前list键的状态,必须在AOF文件中写入六条命令。
如果服务器想要用尽量少的命令来记录list键的状态,那么最简单高效的办法不是去读取和分析现有的AOF文件的内容,而是直接从数据库中读取键list的值,然后用一条RPUSH list “C” “D” “E” “F” "G"命令来代替保存在AOF文件中的六条命令这样旧可以将保存list键所需的命令从六条减少为一条了