前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis持久化AOF原理+伪代码实现

Redis持久化AOF原理+伪代码实现

作者头像
憧憬博客
发布2020-11-27 14:36:59
4640
发布2020-11-27 14:36:59
举报
文章被收录于专栏:憧憬博客分享憧憬博客分享

Redis持久化AOF原理+伪代码实现

Redis 分别提供了 RDBAOF 两种持久化机制,本章首先介绍 AOF 功能的运作机制, 了解命令是如何被保存到 AOF 文件里的, 观察不同的 AOF 保存模式对数据的安全性、以及 Redis 性能的影响。之后会介绍从 AOF 文件中恢复数据库状态的方法,以及该方法背后的实现机制。其中还会查看有些伪代码方便理解,本文来源 redis设计与实现,关于 redis 持久化知识比较重要,所以直接看的书,避免走弯路,以这篇文章记录一下。

基本介绍

AOF 持久化是通过保存 redis 服务器所执行的写命令来记录数据库状态的

代码语言:javascript
复制
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 文件中,以下为伪代码

代码语言:javascript
复制
#事件轮询函数
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 函数,将一些数据写入到文件的时候,操作系统通常会将写入数据暂时保存在一个内存缓冲区里面,等到缓冲区的空间被填满、或者超过了指定的时限之后,才真正地将缓冲区中的数据写入到磁盘里面。 这种做法虽然提高了效率,但也为写入数据带来了安全问题,困为如果计算机发生停机,那么保存在内存缓冲区里面的写入敷据将会丢失。 为此,系统提供了 fsyncfdatasynce 两个同步函数,它们可以强制让操作系统立即将缓冲区中的敷据写入到硬盘里面,从而确保写入敷据的安全性。

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 文件来进行数据还原所需的时间就越多。

  • 举个例子
代码语言:javascript
复制
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 键执行了以下命令:

代码语言:javascript
复制
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 键所需的命令从六条减少为一条了。

整个过程的伪代码可以如下表示:

代码语言:javascript
复制
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 一个键的数据据,而服务器教据库现在却有 k1k2k3k4 四个键。

为了解决这种数据不一致问题, Redis 服务器设置了一个 AOF 重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当 Redis 服务器执行完一个写命令之后,它会同时将这个写命令发送给 AOF 缓冲区和 AOF 重写缓冲区。 这也就是说,在子进程执行 AOF 重写期间,服务器进程需要执行以下三个工作:

  • 执行客户端发来的命令
  • 将执行后的写命令追加到 AOF 缓冲区
  • 将执行后的写命令追加到 AOF 重写缓冲区
20201117161705
20201117161705

这样一来可以保证

  • AOF 缓冲区的内容会定期被写入和同步到 AOF 文件,对现有 AOF 文件的处理工作会如常进行。
  • 从创建子进程开始,服务器执行的所有写命令都会被记录到 AOF 重写缓冲区里面。

当子进程完成 AOF 重写工作之后,它会向父进程发送一个信号,父进程在接到该馆号之后,会调用一个信号处理函数,并执行以下工作:

  1. AOF 重写缓冲区中的所有内容写人到新 AOF 文件中,这时新 AOF 文件所保存的数据库状态将和服务器当前的数据库状态一致。
  2. 对新的 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 文件重写操作。
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Redis持久化AOF原理+伪代码实现
    • 基本介绍
      • 持久化实现
        • 命令指追加 append
        • 写入与同步
        • AOF的持久化和效率
        • AOF文件的载入与数据还原
        • AOF的重写
        • AOF文件重写的实现
        • AOF后台重写
      • 你已经知道了
      相关产品与服务
      云数据库 Redis
      腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档