Redis的RDB和AOF持久化的实现原理

116 阅读15分钟

文章目录


1. 概述

Redis中数据在持久化之前都是位于内存中,当Redis进行持久化时,它首先会发起系统调用,将内存中的数据传输到操作系统的内核缓冲区中,然后由操作系统负责将内核缓冲区中的数据传输到磁盘缓冲区,最后写入到磁盘上。Redis所提供的持久化有全量模式和增量模式两种,即RDB和AOF。


image-20200913000824089


2. RDB - 快照

2.1 简介

Redis的节点本身是具有状态的,当节点中的key-value发生变化时,Redis节点就发生了一次状态变化。全量模式的持久化方式就是将当时状态完全保存下来,通常保存为rdb文件,便于后续的重放。基本原理示意图如下所示:


image-20200914153521094

基于全量的持久化方式也被称为RBD,它是Redis默认的持久化机制,它是最简单、最快的持久化模式。它生成的快照的存储格式为二进制文件,便于持久化文件的传输,但是它无法保证数据的绝对安全。

2.2 配置

Redis中启动RDB持久化只需要在配置文件中添加相关的配置项,例如:

[root@iZbp15ffbqqbe97j9dcf5dZ conf]# cat redis.conf
requirepass root
# RDB
# 900s之内有key发生改变就执行RDB持久化
save 900 1
save 300 10
save 60 10000
# 开启RDB持久化压缩
rdbcompression yes
# RDB持久化文件名
dbfilename redis.db

然后重启服务,当执行的条件有一个被触发时,在相应的目录下就会看到redis.db的持久化文件。如果后续重启服务,仍然可以从文件中得到对应的数据。

[root@iZbp15ffbqqbe97j9dcf5dZ data]# docker exec -it 96 bash
root@96de9b284f78:/data# ls
root@96de9b284f78:/data# redis-cli
127.0.0.1:6379> set name Forlogen
(error) NOAUTH Authentication required.
127.0.0.1:6379> auth root
OK
127.0.0.1:6379> set name Forlogen
OK
127.0.0.1:6379> shutdown save
[root@iZbp15ffbqqbe97j9dcf5dZ data]# ls
redis.db
[root@iZbp15ffbqqbe97j9dcf5dZ data]# cat redis.db
REDIS0009▒      redis-ver5.0.7▒
▒edis-bits▒@▒ctime▒▒4_used-mem▒h
aof-preamble▒▒▒namForlogen▒▒5▒G▒

2.3 原理

Redis本身是单线程的,当执行持久化操作时,它不仅需要响应用户的请求,还需要进行内存快照,最后持久化过程需要进行IO操作写入磁盘。那如何保证既能响应用户的请求,又可以完成持久化操作呢?操作系统的COW(Copy of Write)机制在进行持久化时会fork一个子进程,即根据父进程复制了一个子进程,父子进程会共享内存中的代码块和数据段。快照持久化的工作最终是由子进程完成的,而父进程则可以正常的响应用户的请求。


image-20200913001758036

子进程在持久化操作中并不会修改内存中的数据结构,只是对其进行遍历读取,然后序列化写到磁盘中。父进程由于不断的响应用户的请求,内存中的数据结构相应的会发生变化。一旦持久化操作完成,子进程操作的都是父进程fork时的状态副本,父进程在fork之后请求导致状态发生的变化并不会反映到子进程的副本中。

通常上面的介绍可以知道,RDB持久化的工作依赖于fork出的子进程完成,同时使用COW机制来保证持久化过程中子进程操作的文件的不变性,以及父进程可以正常的响应客户端的请求。仔细来看,RDB持久化可以分为写入和恢复两个过程,写入即生成快照的过程。其中写入的方式又有SAVEBGSAVE两种,区别在于:

  • SAVE:写入期间其他所有的命令都不会并发的执行,即便写入的时间很长,数据的状态始终是一致的。但是,这种方式会阻塞Redis服务器进程,使得Redis无法正常的响应客户端的请求
  • BGSAVE:即上述通过fork子进程来完成写入操作的方式,它使得Redis在持久化期间仍然正常的响应客户端请求。但由于fork子进程涉及到父进程内存复制的开销,当所需的内存不能满足时,仍可能阻塞Redis服务

对于BGSAVE操作来说,如果它在执行期间,客户端发送了SAVEBGSAVEBGREWRITEAOF命令,Redis服务器会有不同的操作:

  • 为了防止父子进程同时进程持久化操作产生的竞争,SAVE命令会被拒绝
  • 为了防止多个BGSAVE产生的竞争,其他客户端发送的BGSAVE会被拒绝
  • 处于性能的考虑,BGREWRITEAOF命令会被延迟到BGSAVE命令执行结束后执行

SAVEBGSAVE的执行过程示意图如下所示:


在这里插入图片描述

除了用户手动的调用SAVE或者BGSAVE命令来执行RDB持久化操作外,Redis还提供了自动执行持久化操作的机制。查看Redis的配置文件redis.conf,可以看到相关的配置项

################################ SNAPSHOTTING  ################################
#
# Save the DB on disk:
#
#   save <seconds> <changes>
#
#   Will save the DB if both the given number of seconds and the given
#   number of write operations against the DB occurred.
#
#   In the example below the behaviour will be to save:
#   after 900 sec (15 min) if at least 1 key changed
#   after 300 sec (5 min) if at least 10 keys changed
#   after 60 sec if at least 10000 keys changed
#
#   Note: you can disable saving completely by commenting out all "save" lines.
#
#   It is also possible to remove all the previously configured save
#   points by adding a save directive with a single empty string argument
#   like in the following example:
#
#   save ""

save 900 1
save 300 10
save 60 10000

根据配置文件中的注释可以很清楚的明白save项的含义,只要配置的save项中有一个条件满足,Redis就会自动执行RDB持久化操作。底层实现中,save项的内容会保存在RedisServer结构中,表现为saveparam数组的形式。另外,redisServer中还维持了dirty字段和lastsave字段来帮助Redis服务器进行判断。

struct redisServer{
	// ...
    
    struct saveparam *saveparam;
    
    long long dirty;   // 距离上一次成功执行RDB持久化,所有数据库状态修改的次数
    
    time_t lastsave;   // 服务器上一次执行RDB持久化操作的时间
    // ...
}

struct saveparam{
	time_t seconds; // 秒数
    int changes   // 修改的次数
}

服务器根据lastsave和当前的UNIX时间戳可以得到,距离上一次成功执行RDB持久化操作的时间;根据dirty字段得到对数据库修改的次数。然后遍历saveparam数组,判断是否有满足执行条件的情况存在。

恢复的过程即Redis从磁盘中加载快照文件到内存,恢复到持久化时所处的状态的过程。

RDB文件的载入是由Redis服务器启动时自动执行的,并没有相关的命令直接调用实现。另外,如果AOF持久化功能开启,那么优先使用AOF文件来还原数据库,只有AOF持久化功能关闭时,Redis才使用RDB进行持久化操作。

2.4 优缺点

基于全量的持久化方式具有如下的优点:

  • 只有一个文件 dump.rdb,方便持久化,而且容灾性好
  • 性能最大化,fork 子进程来完成写操作,让主进程继续处理命令
  • 相对于数据集大时,比 AOF 的启动效率更高

缺点是数据安全性低,RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。

2.5 RDB文件

RDB文件整体的表现形式如下所示:


在这里插入图片描述

其中:

  • REDIS:便于程序载入文件时判断是否为RDB文件
  • db_version: RDB文件的版本号
  • databases:包含零个或任意多个数据库,以及各个数据库中的键值对,长度根据数据库的数量,以及数据库中键值对的情况有所不同
  • EOF:表示RDB文件的结束
  • check_sum:检验和,用于和前面四个部分计算得出的结果进行比较,判断RDB文件是否被损坏

databases部分由如下的三部分信息组成:

  • SELECTDB:提示将要读入一个数据库号码
  • db_number:数据库号码,便于SELECT命令进行数据库之间的切换
  • key_value_pairs:保存的具体键值对

其中,key_value_pairs又根据由于过期时间有所不同:

  • 无过期时间:

    • TYPE:value的类型,Redis服务器根据取值来决定如何读取和解释value
    • key:总是字符串对象
    • value:不同类型的对象
  • 有过期时间:

    • EXPIRETIME_MS:提示程序将读入一个以毫秒为单位的过期时间
    • ms:毫秒为单位表示的过期时间

而value部分根据不同的数据类型又有所不同:

  • 不超过32为位的整数:ENCODING表示编码格式,integer表示具体的整数值

  • 字符串:它又可以根据redis.conf中的rdbcompression项的配置决定是否采用压缩模式:

    • 不压缩:len表示字符串的长度,string表示具体的字符串

    • 压缩:REDIS_RDB_ENC_LZF表示使用的LZF压缩算法,compressed_len表示压缩后的长度,orogin_len表示压缩前的长度,compressed_string表示压缩后的字符串

      # Compress string objects using LZF when dump .rdb databases?
      # For default that's set to 'yes' as it's almost always a win.
      # If you want to save some CPU in the saving child set it to 'no' but
      # the dataset will likely be bigger if you have compressible values or keys.
      rdbcompression yes
      
  • 列表:list_length表示列表的长度,item项表示列表中的数据项

  • 集合:set_size表示集合的大小,elem项表示集合中的数据项

  • 哈希表:hash_size表示哈希表的大小,key_value_pair表示具体的键值对

  • 有序集合:sorted_set_size表示集合的大小,member表示集合中的数据项,score表示分数值

另外,对于INTSET来说,RDB持久化操作会将value转换为字符串对象进行保存,载入时转换为原来的整数集合对象。对于ZIPLIST这样由压缩列表保存的value来说,保存RDB文件时会将压缩列表转换为一个字符串对象,载入时再转换回原来的压缩列表对象,并根据TYPE的值设置列表对象的类型。


3. AOF - 重放

3.1 概念

AOF(Append Only File )每次执行修改内存中数据集的写操作时,都会记录该操作,也被称作基于增量的持久化方式。通过重写AOF日志就可以恢复实例的内存数据结构的状态。当Redis收到客户端的修改指令后,会首先进行参数检验、逻辑处理等操作,如果没有问题,则立即将该指令文本存储到AOF日志中,即先执行指令后将日志存盘。

AOF持久化机制默认是关闭的,Redis推荐同时启用RDB和AOF进行混合持久化操作,这样更安全、更能避免数据的丢失。它会将rdb文件和AOF日志存放在一起,这里的AOF日志只是自持久化开始到持久化结束这段时间发生的增量AOF日志,通常较小。Redis重启时,可以先加载rdb文件的内容,然后再重放增量AOF日志就可以代替AOF全量文件的重放,大幅提升Redis重启的效率。

AOF持久化操作的速度相比于RDB来说,速度较慢,而且以文本文件的形式存储,当文本文件较大时,传输较为困难。但是,AOF相对于RDB更为安全。

  • 如果同时开启了AOF和RDB持久化,那么在Redis服务器宕机后,优先选择加载AOF文件作为持久化文件。
  • 如果先开启了RDB,后开启了AOF,AOF执行持久化操作时会将RDB持久化文件中的内容覆盖掉。

3.2 配置

想要开启AOF支持,只需要在配置文件中添加如下内容,重启服务即可

[root@iZbp15ffbqqbe97j9dcf5dZ conf]# cat redis.conf
requirepass root

# RDB
# 900s之内有key发生改变就执行RDB持久化
save 900 1
save 300 10
save 60 10000

# 开启RDB持久化压缩
rdbcompression yes
# RDB持久化文件名
dbfilename redis.db

# 开启AOF持久化
appendonly yes
# AOF持久化文件名
appendfilename "redis.aof"
# AOF持久化执行时机
appendfsync everysec

3.3 原理

AOF的持久化过程如下所示:


image-20200914153536057

AOF追加的就是改变Redis节点状态的操作指令,后续通过重放AOF中所有的指令就可以恢复节点的状态。Redis中AOF包含三种同步策略:

  • always:主循环的每个迭代直接同步触发fsync方法,强制数据落盘
  • every second:每秒异步的触发一次fsync方法,方法的执行者是bio线程池中的某个线程
  • no:不显式调用fsync,由操作系统决定何时落盘

另外,随着Redis的持续运行,不断产生新的数据append到AOF文件中,使得AOF文件越来越大。不仅会占用大量的磁盘空间,而且重放加载效率较低。 为了解决这些问题,Redis提供了混合持久化的方式,随着Redis中命令的执行,AOF中累积的数据大于某个状态快照的程度,这些AOF增量将使用快照代替,以减少磁盘空间的占用。 此时的AOF日志就只是自持久化开始到持久化结束的这段时间内发生增量的AOF日志,通常较小。于是在 Redis 重启的时候,可以先加载快照中的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。


image-20200914155010063

3.4 AOF重写

由上面的分析指导,AOF记录了所有改变数据库状态的操作命令。当相应的命令很多时,AOF文件会变得很大,造成文件传输困难。为了解决这个问题,Redis提供AOF文件重写功能,新的AOF文件和旧的AOF文件保存到数据库状态相同,但是它保存的命令更加精简,所以所占的空间更加的小。而且,新的AOF文件并不会对旧的AOF文件做任何的修改。

如何理解上面的描述呢?假设现在有如下的操作命令:

RPUSH list "C"
RPUSH list "D" "E"
LPOP list
LPOP list
RPUSH list "F" "G"

之前的AOF文件需要保存所有发生的操作命令,但如果从保存数据库最后的状态出发,其实只需要使用一条命令记录最后list的状态即可,例如:RPUSH list "C" "D" "E" "F" "G"。AOF重写依据的就是这样的思想,对应的命令是BGREWRITEAOF。另外,重写过程中,如果某个key的value个数超过了REDIS_AOF_REWRITE_ITEMS_PER_CMD指定的数值(默认64)时,重写程序会使用多条命令记录。

由于Redis采用的是单线程思想,所以执行AOF重写时会阻塞Redis服务器进程,此时服务器无法正常响应客户端的请求。为了避免阻塞的出现,Redis将AOF的重写放入到一个子进程中执行,子进程不会影响服务器进程的功能。另外,子进程持有服务器进程的数据副本,既可以避免锁的使用,又可以保证数据的安全性。

到此为止,AOF重写解决了传统AOF文件过大AOF重写阻塞服务器进程的问题。但是,AOF重放过程中,服务器进程的数据库状态可能还会发生变化,这样AOF文件所保存的数据库状态和实际的数据库状态就存在着不一致的问题,如何解决呢?

问题出现的原因是AOF重写的过程中,数据库状态的变化并没有记录在AOF文件中 。为了避免这部分命令的丢失,可以将其放入到一个缓冲区中,Redis中称为AOF重写缓冲区。当子进程完成重写后,它会调用一个信号处理函数,告诉服务器进程:我已经完成了重写。服务器进程在收到子进程的信息后,就会将AOF重写缓冲区中的命令写到AOF文件中,然后对新的AOF文件进行改名,原子的覆盖现有AOF文件,完成新旧AOF文件的替换 。此时,AOF文件保存的数据库状态和服务器的数据库状态就一致了。

信号处理函数执行期间会阻塞服务器进程,但这是必须的,对于服务器性能也不会造成很大影响。

3.5 优缺点

AOF方式的优点如下:

  • 数据安全,使用always同步策略时,每发生一次状态的改变就记录一次
  • 通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof工具解决数据一致性问题
  • AOF提供的混合持久化方式可以一定程度上提升重放加载的效率

缺点在于:AOF 文件比 RDB 文件大,且恢复速度慢;而且数据集大的时候,比 rdb 启动效率低。


4. 参考

Redis(7)——持久化【一文了解】