当前位置:   article > 正文

Redis持久化(五)-AOF文件的载入与数据还原、AOF文件重写实现原理_aof rewrite过程

aof rewrite过程

AOF文件的载入与数据还原

在此先贴上AOF和RDB优先级处理流程:
在这里插入图片描述

因为AOF文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态

Redis读取AOF文件 并 还原数据库状态的详细步骤如下:

  1. 创建一个不带网络连接的伪客户端(fake client):因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令,伪客户端执行命令的效果和带网络连接的客户端执行命令的效果完全一样
  2. 从AOF文件中分析并读取出一条写命令
  3. 使用伪客户端执行被读出的写命令
  4. 一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处理完毕为止

当完成以上步骤之后,AOF文件所保存的数据库状态就会被完整地还原出来,整个过程如下所示:
在这里插入图片描述
注意以下几点

  • AOF持久化开启且存在AOF文件时,优先加载AOF文件
  • AOF关闭或者AOF文件不存在时,加载RDB文件
  • 加载AOF/RDB文件成功后,Redis启动成功
  • AOF/RDB文件存在错误时,Redis启动失败并打印错误信息

AOF文件损坏

加载损坏的AOF文件时会拒绝启动,并打印如下日志:

# Bad file format reading the append only file: make a backup of your AOF file, 
    then use ./redis-check-aof --fix <filename>
  • 1
  • 2

对于错误格式的AOF文件,先进行备份,然后采用redis-check-aof--fix命令进行修复,修复后使用diff -u对比数据的差异,找出丢失的数据,有些可以人工修改补全

AOF文件可能存在结尾不完整的情况,比如机器突然掉电 导致AOF尾部文件命令写入不全
Redis为我们提供了aof-load-truncated配置来兼容这种情况,默认开启
加载AOF时,当遇到此问题时会忽略并继续启动,同时打印如下警告日志:

# !!! Warning: short read while loading the AOF file !!!
# !!! Truncating the AOF at offset 397856725 !!!
# AOF loaded anyway because aof-load-truncated is enabled
  • 1
  • 2
  • 3

AOF文件重写实现原理

因为AOF持久化是通过保存被执行的写命令来记录数据库状态的,所以随着服务器运行时间的流逝,AOF文件中的内容会越来越多,文件的体积也会越来越大,如果不加以控制的话,体积过大的AOF文件很可能对Redis服务器、甚至整个宿主计算机造成影响,并且AOF文件的体积越大,使用AOF文件来进行数据还原所需的时间就越多

为了解决AOF文件体积膨胀的问题,Redis提供了 AOF文件 重写(rewrite)功能

通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但 新AOF文件 不会包含 任何浪费空间的冗余命令,所以新AOF文件的体积通常会比旧AOF文件的体积要小得多

首先 从数据库中 读取 键现在的值,然后 用一条命令 去记录 键值对,代替 之前记录这个键值对的多条命令,这就是AOF重写功能的实现原理

整个重写过程可以用以下伪代码表示:

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命令获取列表键包含的所有元素
    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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77

因为aof_rewrite函数生成的新AOF文件 只包含 还原当前数据库状态所必须的命令,所以新AOF文件不会浪费任何硬盘空间

注意:在实际中,为了避免 在执行命令时 造成 客户端输入缓冲区溢出,重写程序 在处理 列表、哈希表、集合、有序集合这四种可能会带有多个元素的键时,会先检查键所包含的元素数量,如果元素的数量超过了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那么 重写程序 将使用 多条命令来记录键的值,而不单单使用一条命令

在目前版本中,REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值为64,这也就是说,如果一个集合键包含了超过64个元素,那么重写程序会用多条SADD命令来记录这个集合,并且每条命令设置的元素数量也为64个:

SADD <set-key> <elem1> <elem2> ... <elem64>
SADD <set-key> <elem65> <elem66> ... <elem128>
SADD <set-key> <elem129> <elem130> ... <elem192>
  • 1
  • 2
  • 3

如果一个列表键包含了超过64个项,那么重写程序会用多条RPUSH命令来保存这个列表,并且每条命令设置的项数量也为64个:

RPUSH <list-key> <item1> <item2> ... <item64>
RPUSH <list-key> <item65> <item66> ... <item128>
RPUSH <list-key> <item129> <item130> ... <item192>
  • 1
  • 2
  • 3

重写程序使用类似的方法处理包含多个元素的有序集合键,以及包含多个键值对的哈希表键

重写后的AOF文件为什么可以变小?(面试题)

  1. 进程内已经超时的数据不再写入文件
  2. 旧的AOF文件含有无效命令,如del key1、hdel key2、srem keys、set a111、set a222等。重写使用进程内数据直接生成,这样新的AOF文件只保留最终数据的写入命令
  3. 多条写命令可以合并为一个,如:lpush list a、lpush list b、lpush list c可以转化为:lpush list a b c。为了防止单条命令过大造成客户端缓冲区溢出,对于list、set、hash、zset等类型操作,以64个元素为界拆分为多条

AOF后台重写(BGREWRITEAOF命令实现原理)

aof_rewrite函数可以很好地完成创建一个新AOF文件的任务,但是,因为这个函数会进行大量的写入操作,所以调用这个函数的线程将被长时间阻塞

因为Redis服务器使用单个线程 来处理 命令请求,所以如果由服务器直接调用aof_rewrite函数的话,那么在重写AOF文件期间,服务期将无法处理客户端发来的命令请求

Redis 决定 将AOF重写程序(aof_rewrite) 放到 子进程里执行,这样做可以同时达到两个目的:

  • 子进程 进行 AOF重写 期间,服务器进程(父进程)可以继续处理命令请求
  • 子进程 带有 服务器进程 的 数据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性

不过,使用子进程也有一个问题需要解决,因为子进程在进行AOF重写期间,服务器进程还需要继续处理命令请求,而新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的AOF文件所保存的数据库状态不一致

下面展示了一个AOF文件重写例子,当子进程开始进行文件重写时,数据库中只有k1一个键,但是当子进程完成AOF文件重写之后,服务器进程的数据库中已经新设置了k2、k3、k4三个键,因此,重写后的AOF文件和服务器当前的数据库状态并不一致,新的AOF文件只保存了k1一个键的数据,而服务器数据库现在却有k1、k2、k3、k4四个键
在这里插入图片描述
为了解决这种数据不一致问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区 在服务器 创建子进程之后 开始使用,当Redis服务器 执行完一个 写命令之后,它会 同时 将这个写命令 发送给 AOF缓冲区 和 AOF重写缓冲区
在这里插入图片描述也就是说,在 子进程 执行 AOF重写 期间服务器进程需要执行以下三个工作:

  1. 执行客户端发来的命令
  2. 将执行后的写命令 追加到 AOF缓冲区
  3. 将执行后的写命令 追加到 AOF重写缓冲区

这样一来可以保证:

  • AOF缓冲区的内容 会定期 被写入 和 同步到AOF文件(通过appendfsync选项),对现有AOF文件的处理工作会如常进行
  • 从创建子进程开始,服务器执行的所有写命令 都会被 记录到 AOF重写缓冲区 里面(这句话前后半句是两件事,互不干扰)
  • 当 子进程 完成 AOF重写工作 之后,它会向父进程发送一个信号,父进程在接到该信号之后,会调用一个信号处理函数,并执行以下工作:
    1. AOF重写缓冲区中的所有内容 写入到 新AOF文件中,这时新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致
    2. 对新的AOF文件进行改名,原子地(atomic)覆盖现有的AOF文件,完成新旧两个AOF文件的替
  • 这个信号处理函数执行完毕之后,父进程就可以继续像往常一样接受命令请求了

对于上面的例子,重写过程如下:
在这里插入图片描述

在整个AOF后台重写过程中,只有 信号处理函数 执行时 会对 服务器进程(父进程)造成阻塞
在其他时候,AOF后台重写都不会阻塞父进程,这将AOF重写对服务器性能造成的影响降到了最低

AOF重写降低了文件占用空间,除此之外,另一个目的是:更小的AOF文件可以更快地被Redis加载

BGREWRITEAOF的复杂度:O(N),其中N为Redis服务器所有数据库包含的键值对总数量

阅读参考:

  • https://blog.csdn.net/wenmeishuai/article/details/106096186
  • https://www.cnblogs.com/fanBlog/p/9707422.html

AOF文件重写流程

在这里插入图片描述

  1. 执行AOF重写请求
    如果当前进程正在执行AOF重写,请求不执行并返回如下响应:

    ERR Background append only file rewriting already in progress
    
    • 1

    如果当前进程正在执行bgsave操作,重写命令延迟到bgsave完成之后再执行,返回如下响应:

    Background append only file rewriting scheduled
    
    • 1
  2. 父进程执行fork创建子进程,开销等同于bgsave过程

  3. 主进程fork操作完成后,继续响应其他命令。所有修改命令依然写入AOF缓冲区 并根据 appendfsync策略同步到硬盘,保证原有AOF机制正确性

  4. 由于fork操作运用写时复制技术,子进程只能共享fork操作时的内存数据。由于父进程依然响应命令,Redis使用“AOF重写缓冲区”保存这部分新数据,防止新AOF文件生成期间丢失这部分数据

  5. 子进程根据内存快照,按照命令合并规则写入到新的AOF文件。每次批量写入硬盘数据量由配置aof-rewrite-incremental-fsync控制,默认为32MB,防止单次刷盘数据过多造成硬盘阻塞

  6. 新AOF文件写入完成后,子进程发送信号给父进程,父进程更新统计信息

  7. 父进程把AOF重写缓冲区的数据写入到新的AOF文件

  8. 使用新AOF文件替换老文件,完成AOF重写

触发AOF后台重写的条件(重要!!!)

  • 可以通过调用 BGREWRITEAOF 手动触发AOF重写
  • 服务器在AOF功能开启的情况下,会维持以下三个变量:
    1. 记录当前AOF文件大小的变量aof_current_size
    2. 记录最后一次AOF重写之后,AOF文件大小 的 变量aof_rewrite_base_size
    3. 增长百分比变量aof_rewrite_perc
  • 每次当serverCron(服务器周期性操作函数)函数执行时,它会检查以下条件是否全部满足,如果全部满足的话,就触发自动的AOF重写操作
    1. 没有BGSAVE命令(RDB持久化)/AOF持久化在执行;
    2. 没有BGREWRITEAOF在进行;
    3. 当前AOF文件大小 要大于 aof_rewrite_min_size(默认为1MB),或者在redis.conf配置了auto-aof-rewrite-min-size大小;
    4. 当前AOF文件大小 和 最后一次重写后的大小之间的比率 等于或者等于 指定的增长百分比(在配置文件设置了auto-aof-rewrite-percentage参数,不设置默认为100%)

如果前面三个条件都满足,并且 当前AOF文件大小 比 最后一次AOF重写时的大小 要大于 指定的百分比,那么触发自动AOF重写

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/386069
推荐阅读
相关标签
  

闽ICP备14008679号