Redis持久化AOF原理+伪代码实现
Redis
分别提供了 RDB
和 AOF
两种持久化机制,本章首先介绍 AOF
功能的运作机制, 了解命令是如何被保存到 AOF
文件里的, 观察不同的 AOF
保存模式对数据的安全性、以及 Redis
性能的影响。之后会介绍从 AOF
文件中恢复数据库状态的方法,以及该方法背后的实现机制。其中还会查看有些伪代码方便理解,本文来源 redis设计与实现,关于 redis
持久化知识比较重要,所以直接看的书,避免走弯路,以这篇文章记录一下。
基本介绍
AOF
持久化是通过保存 redis
服务器所执行的写命令来记录数据库状态的
set key1 value1 sadd fruits "apple" "banner" rpush numbers 128 125
RDB
的持久化方式是将 key1、fruits、numbers
三个键的键值对保存到 RDB
文件中,而 AOF
持久化方式是将服务器执行的 set、sadd、rpush
三个命令保存到 AOF
文件中,被写入 AOF
文件的所有命令都是以 Redis
的命令请求协议格式保存的。
持久化实现
AOF
持久化功能的实现可以分为命令追加(append)、文件写入、文件同步三个步骤(sync)
命令指追加 append
当 AOF
持久化功能处于打开状态时,服务器在执行完一个写命令后,会以协议格式将被执行的写命令追加到服务器状态的 aof_buf
缓冲区的末尾
写入与同步
Redis
的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像 serverCron
函数这样需要定时运行的函数。
因为服务器在处理文件事件时可能会执行命令,使得一些内容被追加到 aof_buf
缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用 flushAppendOnlyFile
函数,考虑是否需要将 aof_buf
缓冲区中的内容写入和保存到 AOF
文件中,以下为伪代码
#事件轮询函数 def evenloop(): while True: # 处理文件事件,接收命令请求以及发送命令回复 # 处理命令请求时可能会有新的内容被追加到 aof_buf 缓存区中 processFileEvents() # 处理时间事件 processTimeEvents() # 是否将 aof_buf 缓冲区中的内容写入并同步到 appendonly.aof 文件中。 flushAppendOnlyFile()
flushAppendOnlyFile
函数的行为由服务器配置的 appendfsync
选项的值来决定,各个不同值产生的行为如下
选项 | 行为 |
always | 将 aof_buf 缓冲区中的所有内容写入并同步到 AOF 文件中(最安全,但性能差) |
everysec | 将 aof_buf 缓冲区中的所有内容写入并同步到 AOF 文件中,如果上次同步 AOF 文件的时间距离现在超过 1 秒钟,那么会再次对 AOF 文件进行同步。 (安全,性能较好) |
no | 将 aof_buf 缓冲区中的所有内容写入并同步到 AOF 文件中,但不对 AOF 文件进行同步,何时进行同步一般有操作系统来决定。(一般为 30 秒,不安全,性能最好) |
如果用户没有主动为 appendfsync
选项设置值,那么 appendfsync
选项的默认值为 everysec
,关于 appendfsync
选项的更多信息,可以查看 Redis
项目附带的示例配置文件 redis.conf
为了提高文件的写入效率,在现代操作系统中,当用户调用
write
函数,将一些数据写入到文件的时候,操作系统通常会将写入数据暂时保存在一个内存缓冲区里面,等到缓冲区的空间被填满、或者超过了指定的时限之后,才真正地将缓冲区中的数据写入到磁盘里面。这种做法虽然提高了效率,但也为写入数据带来了安全问题,困为如果计算机发生停机,那么保存在内存缓冲区里面的写入敷据将会丢失。
为此,系统提供了
fsync
和fdatasynce
两个同步函数,它们可以强制让操作系统立即将缓冲区中的敷据写入到硬盘里面,从而确保写入敷据的安全性。
AOF的持久化和效率
服务器配置 appendfsync
选项的值直接决定 AOF
持久化功能的效率和安全性。
- 当
appendfsync
的值为always时,服务器在每个事件循环都要将aof_buf
缓冲区中的所有内容写入到AOF
文件,并且同步AOF
文件,所以always 的效率是appendfsync
选项三个值当中罩慢的一个,但从安全性来说,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
文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态。 Redis
读取 AOF
文件并还原敬据库状态的详细步骤如下:
- 创建一个不带网络连接的伪客户端(fakeclient): 因为
Redis
的命令只能在客户端上下文中执行,而载人AOF
文件时所使用的命令直接来源于AOF
文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行AOF
文件保存的写命令,伪客户端执行命令的效果和带网络连接的客户端执行命令的效果完全一样。 - 从
AOF
文件中分析并读取出一条写命令。 - 使用伪客户端执行被读出的写命令。
- 一直执行步骤2和步骤3,直到
AOF
文件中的所有写命令都被处理完毕为止。
当完成以上步骤之后, AOF
文件所保存的数据库状态就会被完整地还原出来
AOF的重写
因为 AOF
持久化是通过保存被执行的写命令来记录数据库状态的,所以随着服务器运行时间的流逝, AOF
文件中的内容会越来越多,文件的体积也会越来键大,如果不加以控制的话,体积过大的 AOF
文件很可能对Redi服务器、甚至整个宿主计算机造成影响,并且 AOF
文件的体积越大,使用 AOF
文件来进行数据还原所需的时间就越多。
- 举个例子
redis> RPUSH list "A" "B" // ["A","B"] (integer) 2 redis> RPUSH list "C" // ["A","B", "C"] (integer) 3 redis> RPUSH list "D" "E" // ["A","B", "C", "D", "E"] (integer) 5 redis> LPOP list // ["B", "C", "D", "E"] "A" redis> LPOP list // ["C", "D", "E"] "B" redis> RPUSH list "F" "G" // ["C", "D", "E", "F" "G"] (integer) 5
那么光是为了记录这个 list
键的状态, AOF
文件就需要保存六条命令。对于实际的应用程度来说,写命令执行的次数和频率会比上面的简单示例要高得多,所以造成的问题也会严重得多。
为了解决 AOF
文件体积膨胀的间题, Redis
提供了 AOF
**文件重写(rewrite)**功能。通过该功能, Redis
服务器可以创建一个新的 AOF
文件来替代现有的 AOF
文件,新旧两个 AOF
文件所保存的敬据库状态相同,但新 AOF
文件不会包含任何浪费空间的冗余命令,所以新 AOF
文件的体积通常会比旧 AOF
文件的体积要小得多。
在接下来的内容中,我们将介绍 AOF
文件重写的实现原理,以及 BGREWRITEAOF
命令的实现原理。
AOF文件重写的实现
虽然 Redis
将生成新 AOF
文件替换用 AOF
文件的功能命名为 AOF文件重写
,但实际上, AOF
文件重写并不需要对现有的 AOF
文件进行任何读取、分析或者写人操作,这个功能是通过读取服务器当前的数据库状态来实现的。
考虑这样一个情况,如果服务器对 1ist
键执行了以下命令:
redis> RPUSH list "A" "B" // ["A","B"] (integer) 2 redis> RPUSH list "C" // ["A","B", "C"] (integer) 3 redis> RPUSH list "D" "E" // ["A","B", "C", "D", "E"] (integer) 5 redis> LPOP list // ["B", "C", "D", "E"] "A" redis> LPOP list // ["C", "D", "E"] "B" redis> RPUSH list "F" "G" // ["C", "D", "E", "F" "G"] (integer) 5
那么服务器为了保存当前 list
键的状态,必须在AOF文件中写人六条命令。如果服务器想要用尽量少的命令来记录 1ist
键的状态,那么最简单高效的办法不是去读取和分析现有 AOF
文件的内容,而是直接从数据库中读取键 list
的值,然后用一条 RPUSH list "C""D""E""E""G"
命令来代替保存在 AOF
文件中的六条命令,这样就可以将保存 1ist
键所需的命令从六条减少为一条了。
整个过程的伪代码可以如下表示:
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.writecommand("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命令获取所有元素 item1,item2, ... , itemN = LRANGE(key, 0, 1) #使用RPUSH命令重写列表 f.write_command(RPUSH, key, item1,item2.....) 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 = SMERBERS(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, member1, score1, member2, score2, ..., memberN, scoreN) def rewrite_expire_time(key): #获取毫秒精度的键过期时间 timestamp = get_expire_time_in_unixstamp(key) #使用 PEXPIREAT 命令重写过期时间 f.write_command(PEXPIREAT, key, timestamp)
因为 aof_rewrite
函数生成的新AOF文件只包含还原当前数据库状态所必须的命令,所以新AOF文件不会浪费任何硬盘空间。
注意:在实际中,为了避免在执行命令时造成客户端输入缓冲区溢出,重写程序在处理列表、哈希表、集合、有序集合这四种可能会带有多个元素的键时,会先检查键所包含的元素数量,如果元素的数量超过了 redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD 常量的值,那么重写程序将使用多条命令来记录键的值,而不单单使用一条命令。在 3.0 版本中, REDIS_AOF_REWRITE_ITEMS_PER_CMD 常量的值为64,这也就是说,如果一个集合键包含了超过64个元素,那么重写程序会用多条 SADD 命令来记录这个集合,并且每条命令设置的元素数量也为64个
AOF后台重写
上面介绍的AOF重写程序 aof_rewrite
函教可以很好地完成建一个新 AOF
文件的任务,但是,因为这个函数会进行大量的写入操作,所以调用这个函数的线程将被长时间阻塞。因为 Redis
服务器使用单个线程来处理命令请求,所以如果由服务器直接调用 aof_rewrite
函数的话,那么在重写 AOF
文件期间,服务期将无法处理客户端发来的命令请求。
很明显,作为一种辅佐性的维护手段, Redis
不希望 AOF
重写造成服务器无法处理请求,所以Redis决定将 AOF
重写程序放到子进程里执行,这样做可以同时达到两个目的
- 子进程进行
AOF
重写期间,服务器进程(父进程)可以继绥处理命令请求。 - 子进程带有服务器进程的教据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性
不过,使用子进程也有一个问题需要解决,因为子进程在进行 AOF
重写期间,服务器进程还需要继续处理命令请求,而新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的 AOF
文件所保存的数据库状态不一致。
举个例子
时间 | 服务器进程 | 子进程 |
t1 | 执行命令 SET k1 v1 | |
t2 | 执行命令 SET k1 v2 | |
t3 | 执行命令 SET k1 v3 | |
t4 | 创建子进程,执行AOF文件重写 | 开始AOF文件重写 |
t5 | 执行命令 SET k2 100 | 执行重写操作 |
t6 | 执行命令 SET k3 101 | 执行重写操作 |
t7 | 执行命令 SET k4 102 | 完成AOF重写 |
上面展示了一个 AOF
文件重写例子,当子进程开始进行文件重写时,数据库中只有 k1
一个键,但是当子进程完成 AOF
文件重写之后,服务器进程的数据库中已经新设置了k2、k3、k4三个键,因此,重写后的 AOF
文件和服务器当前的数据库状态并不一致,新的 AOF
文件只保存了 k1
一个键的数据据,而服务器教据库现在却有 k1
、 k2
、 k3
、 k4
四个键。
为了解决这种数据不一致问题, Redis
服务器设置了一个 AOF
重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当 Redis
服务器执行完一个写命令之后,它会同时将这个写命令发送给 AOF
缓冲区和 AOF
重写缓冲区。
这也就是说,在子进程执行 AOF
重写期间,服务器进程需要执行以下三个工作:
- 执行客户端发来的命令
- 将执行后的写命令追加到
AOF
缓冲区 - 将执行后的写命令追加到
AOF
重写缓冲区
这样一来可以保证
AOF
缓冲区的内容会定期被写入和同步到AOF
文件,对现有AOF
文件的处理工作会如常进行。- 从创建子进程开始,服务器执行的所有写命令都会被记录到
AOF
重写缓冲区里面。
当子进程完成 AOF
重写工作之后,它会向父进程发送一个信号,父进程在接到该馆号之后,会调用一个信号处理函数,并执行以下工作:
- 将
AOF
重写缓冲区中的所有内容写人到新AOF
文件中,这时新AOF
文件所保存的数据库状态将和服务器当前的数据库状态一致。 - 对新的
AOF
文件进行改名,原子地(atomic)覆盖现有的AOF
文件,完成新旧两个AOF
文件的替换。
这个信号处理函数执行完毕之后,父进程就可以继续像往常一样接受命令请求了。在整个 AOF
后台重写过程中,只有信号处理函教执行时会对服务器进程(父进程)造成阻塞,在其他时候, AOF
后台重写都不会阻塞父进程,这将 AOF
重写对服务器性能造成的影响降到了最低。
完整的重写过程如下:
时间 | 服务器进程 | 子进程 |
t1 | 执行命令 SET k1 v1 | |
t2 | 执行命令 SET k1 v2 | |
t3 | 执行命令 SET k1 v3 | |
t4 | 创建子进程,执行AOF文件重写 | 开始AOF文件重写 |
t5 | 执行命令 SET k2 100 | 执行重写操作 |
t6 | 执行命令 SET k3 101 | 执行重写操作 |
t7 | 执行命令 SET k4 102 | 完成AOF重写,向父进程发送信号 |
t8 | 接受到子进程发来的信号,将命令 SET k2 100、SET k3 101、SET k4 102 追加到新AOF文件的末尾 | |
t9 | 用新的AOF文件覆盖旧的AOF文件 |
以上就是 AOF
后台重写,也即是 BGREWRITEAOF
命令的实现原理
你已经知道了
AOF
文件通过保存所有修改数据库的写命令请求来记录服务器的数据库状态。AOF
文件中的所有命令都以Redis
命令请求协议的格式保存。appendfsync
选项的不同值对AOF
持久化功能的安全性以及Redis
服务器的性有很大的影响- 服务器只要载人并重新执行保存在
AOF
文件中的命令,就可以还原数据库本来的状态。 AOF
重写可以产生一个新的AOF
文件,这个新的AOF
文件和原有的AOF
文件所保存的数据库状态一样,但体积更小。AOF
重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF
文件进行任何读人、分析或者写人操作。- 在执行
BGRERIRTEAOF
命令时,Redis
服务器会维护一个AOF
重写缓冲区,该缓冲区会在子进程创建新AOF
文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF
文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF
文件的末尾,使得新旧两个AOF
文件所保存的数据库状态一致。最后,服务器用新的AOF
文件替换旧的AOF
文件,以此来完成AOF
文件重写操作。