redis——持久化

因为redis是内存数据库,他把数据都存在内存里,所以要想办法实现持久化功能。

RDB

RDB持久化可以手动执行,也可以配置定期执行,可以把某个时间的数据状态保存到RDB文件中,反之,我们可以用RDB文件还原数据库状态。

    生成

有两个命令可以生成RDB文件:

  • SAVE 命令由服务器进程直接执行保存操作,所以该命令会阻塞服务器,服务器不能接受其他指令。
  • BGSAVE 命令由子进程执行保存操作,所以该命令不会阻塞服务器,服务器可以接受其他指令。。

禁止BGSAVE和SAVE同时执行,也就是说执行其中一个就会拒绝另一个,这是为了避免父进程和子进程同时执行两个rdbsave,防止产生竞争条件。

    载入

    RDB载入工作是服务器启动时自动执行的。

    自动保存

用户可以通过save选项设置多个保存条件,服务器状态中会保存所有用 save 选项设置的保存条件,当任意一个保存条件被满足时,服务器会自动执行 BGSAVE 命令。

比如

save 900 1

save 300 10

满足:服务器在900秒之内被修改至少一次或者300秒内修改至少十次。就会执行BGSAVE。

 

当服务器启动时,用户可以通过指定配置文件或者传入启动参数来设置save选项,服务器会把条件放到一个结构体里,结构体有一个数组,保存了所有条件。

 

serverCron函数默认100毫秒检查一次,他会遍历数组依次检查,符合条件就会执行BGSAVE。

    RDB文件结构

一个完整 RDB 文件所包含的各个部分:

REDIS,长度5字节, 保存着 "REDIS" 五个字符。 通过这五个字符, 可以在载入文件时, 快速检查载入文件是否 RDB 文件。

db_version ,长度 4 字节, 它的值是一个字符串表示的整数, 这个整数记录了 RDB 文件的版本号

databases 部分包含着零个或任意多个数据库, 以及各个数据库中的键值对数据

EOF 常量的长度为 1 字节, 这个常量标志着 RDB 文件正文内容的结束

check_sum 是一个 8 字节长的无符号整数, 保存着一个校验和,以此来检查 RDB 文件是否出错或损坏

我并不想深入探究databases的组成。就是知道

  • RDB 文件是一个经过压缩的二进制文件,由多个部分组成。
  • 对于不同类型的键值对, RDB 文件会使用不同的方式来保存它们。

即可。

 

AOF

AOF持久化是通过保存服务器执行的命令来记录状态的。还原的时候再执行一遍即可。

功能的实现可以分为命令追加、文件写入、文件同步三个步骤。

 

当 AOF 持久化功能处于打开状态时, 服务器在执行完一个写命令之后, 会以协议格式将被执行的写命令追加到服务器状态的 aof_buf 缓冲区的末尾:

struct redisServer {
    // ...
    // AOF 缓冲区
    sds aof_buf;
    // ...
};

Redis 服务器进程就是一个事件循环

循环中的文件事件负责接收客户端的命令请求, 以及向客户端发送命令回复,

而时间事件则负责执行像 serverCron 函数这样需要定时运行的函数。

因为服务器在处理文件事件时可能会执行写命令, 使得一些内容被追加到 aof_buf 缓冲区里面, 所以在服务器每次结束一个事件循环之前, 它都会调用 flushAppendOnlyFile 函数, 考虑是否需要将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件里面, 这个过程可以用伪代码表示:

def eventLoop():

    while True:

        # 处理文件事件,接收命令请求以及发送命令回复
        # 处理命令请求时可能会有新内容被追加到 aof_buf 缓冲区中
        processFileEvents()

        # 处理时间事件
        processTimeEvents()

        # 考虑是否要将 aof_buf 中的内容写入和保存到 AOF 文件里面
        flushAppendOnlyFile()

flushAppendOnlyFile 函数的行为由服务器配置的 appendfsync 选项的值来决定

值为 always 时, 服务器在每个事件循环都要将 aof_buf 缓冲区中的所有内容写入到 AOF 文件并且同步 AOF 文件, 所以 always 的效率最慢的一个, 但从安全性来说, always 是最安全的, 因为即使出现故障停机, AOF 持久化也只会丢失一个事件循环中所产生的命令数据。

值为 everysec 时, 服务器在每个事件循环都要将 aof_buf 缓冲区中的所有内容写入到 AOF 文件, 每隔超过一秒就要在子线程中对 AOF 文件进行一次同步: 从效率上来讲, everysec 模式足够快, 并且就算出现故障停机, 数据库也只丢失一秒钟的命令数据。

值为 no 时, 服务器在每个事件循环都要将 aof_buf 缓冲区中的所有内容写入到 AOF 文件, 至于何时对 AOF 文件进行同步, 则由操作系统控制。

因为处于 no 模式下的 flushAppendOnlyFile 调用无须执行同步操作, 所以该模式下的 AOF 文件写入速度总是最快的, 不过因为这种模式会在系统缓存中积累一段时间的写入数据, 所以该模式的单次同步时长通常是三种模式中时间最长的: 从平摊操作的角度来看,no 模式和 everysec 模式的效率类似, 当出现故障停机时, 使用 no 模式的服务器将丢失上次同步 AOF 文件之后的所有写命令数据。

    重写

AOF持久化是保存了一堆命令来恢复数据库,随着时间流逝,存的会越来越多,如果不加以控制,文件过大可能影响服务器甚至计算机。而且文件过大,恢复时需要时间也太长。

所以redis提供了重写功能,写出的新文件不会包含任何浪费时间的冗余命令。

接下来,我们就介绍重写的原理。

其实重写不会对现有的AOF文件进行读取分析等操作,而是通过当前服务器的状态来实现。

 # 假设服务器对键list执行了以下命令s;
127.0.0.1:6379> RPUSH list "A" "B"
(integer) 2
127.0.0.1:6379> RPUSH list "C"
(integer) 3
127.0.0.1:6379> RPUSH list "D" "E"
(integer) 5
127.0.0.1:6379> LPOP list
"A"
127.0.0.1:6379> LPOP list
"B"
127.0.0.1:6379> RPUSH list "F" "G"
(integer) 5
127.0.0.1:6379> LRANGE list 0 -1
1) "C"
2) "D"
3) "E"
4) "F"
5) "G"
127.0.0.1:6379> 

当前列表键list在数据库中的值就为["C", "D", "E", "F", "G"]。要使用尽量少的命令来记录list键的状态,最简单的方式不是去读取和分析现有AOF文件的内容,,而是直接读取list键在数据库中的当前值,然后用一条RPUSH list "C" "D" "E" "F" "G"代替前面的6条命令。

  • 伪代码表示如下
def AOF_REWRITE(tmp_tile_name):

  f = create(tmp_tile_name)

  # 遍历所有数据库
  for db in redisServer.db:

    # 如果数据库为空,那么跳过这个数据库
    if db.is_empty(): continue

    # 写入 SELECT 命令,用于切换数据库
    f.write_command("SELECT " + db.number)

    # 遍历所有键
    for key in db:

      # 如果键带有过期时间,并且已经过期,那么跳过这个键
      if key.have_expire_time() and key.is_expired(): continue

      if key.type == String:

        # 用 SET key value 命令来保存字符串键

        value = get_value_from_string(key)

        f.write_command("SET " + key + value)

      elif key.type == List:

        # 用 RPUSH key item1 item2 ... itemN 命令来保存列表键

        item1, item2, ..., itemN = get_item_from_list(key)

        f.write_command("RPUSH " + key + item1 + item2 + ... + itemN)

      elif key.type == Set:

        # 用 SADD key member1 member2 ... memberN 命令来保存集合键

        member1, member2, ..., memberN = get_member_from_set(key)

        f.write_command("SADD " + key + member1 + member2 + ... + memberN)

      elif key.type == Hash:

        # 用 HMSET key field1 value1 field2 value2 ... fieldN valueN 命令来保存哈希键

        field1, value1, field2, value2, ..., fieldN, valueN =\
        get_field_and_value_from_hash(key)

        f.write_command("HMSET " + key + field1 + value1 + field2 + value2 +\
                        ... + fieldN + valueN)

      elif key.type == SortedSet:

        # 用 ZADD key score1 member1 score2 member2 ... scoreN memberN
        # 命令来保存有序集键

        score1, member1, score2, member2, ..., scoreN, memberN = \
        get_score_and_member_from_sorted_set(key)

        f.write_command("ZADD " + key + score1 + member1 + score2 + member2 +\
                        ... + scoreN + memberN)

      else:

        raise_type_error()

      # 如果键带有过期时间,那么用 EXPIREAT key time 命令来保存键的过期时间
      if key.have_expire_time():
        f.write_command("EXPIREAT " + key + key.expire_time_in_unix_timestamp())

    # 关闭文件
    f.close()

    AOF后台重写


aof_rewrite函数可以创建新的AOF文件,但是这个函数会进行大量的写入操作,所以调用这个函数的线程被长时间的阻塞,因为服务器使用单线程来处理命令请求;所以如果直接是服务器进程调用AOF_REWRITE函数的话,那么重写AOF期间,服务器将无法处理客户端发送来的命令请求;


Redis不希望AOF重写会造成服务器无法处理请求,所以将AOF重写程序放到子进程(后台)里执行。这样处理的好处是: 
1)子进程进行AOF重写期间,主进程可以继续处理命令请求;
2)子进程带有主进程的数据副本,使用子进程而不是线程,可以避免在锁的情况下,保证数据的安全性。

还有一个问题,可能重写的时候又有新的命令过来,造成信息不对等,所以redis设置了一个缓冲区,重写期间把命令放到重写缓冲区。
 

  总结


AOF重写的目的是为了解决AOF文件体积膨胀的问题,使用更小的体积来保存数据库状态,整个重写过程基本上不影响Redis主进程处理命令请求;
AOF重写其实是一个有歧义的名字,实际上重写工作是针对数据库的当前状态来进行的,重写过程中不会读写、也不适用原来的AOF文件;
AOF可以由用户手动触发,也可以由服务器自动触发。

 

©️2020 CSDN 皮肤主题: 书香水墨 设计师:CSDN官方博客 返回首页