Redis作为基于内存的NoSQL数据库,在保障高性能、高并发的同时也支持数据持久化,将内存中的数据写入磁盘中,防止突发场景(如断电)导致的数据丢失。Redis提供了RDB和AOF两种持久化方案。

RDB

RDB全称Redis DataBase,在指定时间间隔内将内存中的数据集快照进行持久化。是Redis默认启用的持久化方案,持久化过程会生成一个压缩过的二进制文件,默认名称为dump.rdb,当服务器重启时,加载持久化文件恢复数据到内存中。
未命名文件.svg
Redis没有提供特定的加载方式,当服务器在启动时候只要发现存在RDB文件就会自动载入,载入过程服务器处于阻塞状态,直到RDB文件加载完成,服务器开始接收客户端请求。

  1. 2022 11:26:14.942 * DB loaded from append only file: 0.000 seconds
  2. 2022 11:26:14.942 * Ready to accept connections

RDB文件创建

RDB有两种触发方式,通过客户端发起SAVE或者BGSAVE手动触发,或者通过配置文件配置,Redis服务器在启动时加载配置信息,由事件来自动触发RDB文件生成调度。

手动触发

在Redis中可以使用SAVE或者BGSAVE命令来生成RDB文件,两者区别在于:

  • SAVE:会阻塞主进程,直到RDB文件创建完成,在此期间服务器不再处理任何命令请求。
  • BGSAVE:不阻塞主进程,后台保存的同时服务器可以持续处理客户端请求。

    在执行BGSAVE过程中,服务端不会再接收SAVE或者BGSAVE命令,如果有BGREWRITEAOF命令发起,会被延迟到BGSAVE之后执行,BGREWRITEAOF在执行过程中,发起BGSAVE命令,会被拒绝执行。

自动触发

  1. 参数配置

Redis在配置文件redis.conf中提供了save参数,只要满足其中的任一条件,就会触发BGSAVE操作。

  1. # Unless specified otherwise, by default Redis will save the DB:
  2. # * After 3600 seconds (an hour) if at least 1 key changed
  3. # * After 300 seconds (5 minutes) if at least 100 keys changed
  4. # * After 60 seconds if at least 10000 keys changed
  5. #
  6. # You can set these explicitly by uncommenting the three following lines.
  7. #
  8. # save 3600 1 表示在3600秒内,至少有一个key被修改,BGSAVE就会执行
  9. # save 300 100
  10. # save 60 10000

修改redis.conf中的配置为save 10 ,在10秒内只要有一个key被修改就触发BGSAVE。查看Redis的日志输出:

  1. 18:C 20 Mar 2022 11:16:05.072 * DB saved on disk
  2. 18:C 20 Mar 2022 11:16:05.072 * RDB: 0 MB of memory used by copy-on-write
  3. 1:M 20 Mar 2022 11:16:05.160 * Background saving terminated with success
  4. 1:M 20 Mar 2022 11:16:32.518 * 1 changes in 10 seconds. Saving...
  5. 1:M 20 Mar 2022 11:16:32.519 * Background saving started by pid 20
  6. 20:C 20 Mar 2022 11:16:32.523 * DB saved on disk
  7. 20:C 20 Mar 2022 11:16:32.523 * RDB: 0 MB of memory used by copy-on-write
  8. 1:M 20 Mar 2022 11:16:32.619 * Background saving terminated with success
  9. 1:M 20 Mar 2022 11:16:43.043 * 1 changes in 10 seconds. Saving...
  10. 1:M 20 Mar 2022 11:16:43.043 * Background saving started by pid 21

在日志中可以看到,BGSAVE执行一次就会创建一个新的进程,并且用到了copy-on-write,这部分接下来会详细描述其执行过程。

  1. 参数加载过程

Redis Server启动时,会读取配置文件中的save属性值,将传入的参数赋值给redisServer结构体的saveParams属性。

  1. struct redisServer {
  2. // ...
  3. struct saveparam *saveparams;//记录了save保存条件的数组
  4. // ...
  5. };
  6. struct saveparam {
  7. time_t seconds;//秒数
  8. int changes;//修改数
  9. };

此外服务器状态还维持了一个dirty计数器和lastsave属性。

  • dirty计数器:记录距离上一次服务器成功执行SAVE或者BGSAVE后进行了多少次写入、删除、修改等操作。
  • lastsave:上次SAVE或者BGSAVE成功的时间。

Redis服务器中的周期函数serverCron默认100ms执行一次,用于服务器信息维护,其中有一项工作就是检查save条件是否满足,如果满足就开始执行BGSAVE命令。

BGSAVE执行过程

Redis中RDB操作几乎都是通过BGSAVE命令来触发,SAVE的使用场景比较少见,因此BGSAVE的执行过程,对于一些问题的排查(如数据量大情况下CPU异常波动)就显得至关重要。BGSAVE执行过程整如图所示:
未命名文件 (1).svg
BGSAVE命令开始执行,父进程会fork一个子进程,由子进程来完成数据持久化操作,父进程继续处理来自客户端的请求,是在fork的过程会阻塞,这个过程耗时长短取决于数据量的多少,具体流程如下:

  • 父进程通过fork来复制一个环境、变量等完全相同的子进程
  • 当复制完成后,子进程通过信号通知父进程,子进程开始将数据集写入一个临时的RDB文件,当新的RDB文件写入完成后会替换掉原来的RDB文件。
  • fork的过程如果出现错误将会交由父进程处理。

在Linux中fork是重量级调用,因为其建立了父进程完整的副本,为了减少相关工作量,使用到了COW(Copy On Write)写时复制技术,整个过程如图所示:
未命名文件 (4).svg
写时复制适合的场景是读多写少的场景,比如黑白名单,热点数据等。这样做的好处是避免了父进程完整的数据复制到子进程,如果采用直接复制的办法,也就意味着执行这个调用至少需要分配2倍的内存空间,对资源大大浪费,而且复制的过程在数据量比较大的时候也会阻塞较长一段时间。

COW机制使得内核尽可能延迟页的复制,最理想的情况就是完全不需要复制,不使用任何额外的内存空间。在没有数据修改时共享数据,当数据发生变化修改时才进行复制。

  • 进程通常只使用了内存页的一小部分,在调用fork的过程中,子进程并不复制整个内存空间,而是只复制其内存页,建立虚拟内存空间和物理内存页之间的联系,fork之后父子进程的地址空间指向同样的物理内存页,并且父子进程不能修改彼此的内存页,被标记为只读。
  • 父进程发生写操作时,因为权限已经设置为read-only了,所以会触发页异常中断(page-fault)。在中断处理中,需要被写入的内存页面会复制一份,复制出来的旧数据交给子进程使用,然后主进程继续处理请求。

    RDB文件格式

    RDB文件是一个二进制文件,有着特定的文件结构。Redis中数据对象存储了长度信息,在读写之前就可以知道需要占用的空间大小,并且使用LZF 算法来压缩文件大小,RDB文件被优化的适合快速的读写。优化快速读写意味着磁盘格式应尽可能接近内存中表示形式,下面展示了RDB文件的基本格式: ```java ——————————————# RDB is a binary format. There are no new lines or spaces in the file. 52 45 44 49 53 # Magic String “REDIS” 30 30 30 37 # 4 digit ASCCII RDB Version Number. In this case, version = “0007” = 7

FE 00 # FE = code that indicates database selector. db number = 00 ——————————————# Key-Value pair starts FD $unsigned int # FD indicates “expiry time in seconds”. After that, expiry time is read as a 4 byte unsigned int $value-type # 1 byte flag indicating the type of value - set, map, sorted set etc. $string-encoded-key # The key, encoded as a redis string

$encoded-value # The value. Encoding depends on $value-type

FC $unsigned long # FC indicates “expiry time in ms”. After that, expiry time is read as a 8 byte unsigned long $value-type # 1 byte flag indicating the type of value - set, map, sorted set etc. $string-encoded-key # The key, encoded as a redis string

$encoded-value # The value. Encoding depends on $value-type

$value-type # This key value pair doesn’t have an expiry. $value_type guaranteed != to FD, FC, FE and FF $string-encoded-key

$encoded-value

FE $length-encoding # Previous db ends, next db starts. Database number read using length encoding.

… # Key value pairs for this database, additonal database

FF ## End of RDB file indicator 8 byte checksum ## CRC 64 checksum of the entire file.

  1. 接下来就进步的对RDB文件中的存储格式进一步按照层级分析。
  2. 1. **RDB存储格式**
  3. 首先了解RDB文件的整体存储结构:<br />![未命名文件.svg](https://cdn.nlark.com/yuque/0/2022/svg/2456868/1647769564743-7641f1dc-2356-4d25-bf43-d8993973e69e.svg#clientId=u19cc286c-ac9d-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=ue1fe135b&name=%E6%9C%AA%E5%91%BD%E5%90%8D%E6%96%87%E4%BB%B6.svg&originHeight=173&originWidth=571&originalType=binary&ratio=1&rotation=0&showTitle=false&size=4299&status=done&style=none&taskId=ued2b483b-fa46-41f9-9634-65a545f428f&title=)
  4. - REDISMagic(魔数)占据五个字节,标志一份文件是否为RDB文件,类似于Java字节码文件的cafebabe标识。
  5. - db_version:长度4个字节,值为字符串记录的整数,记录RDB文件的版本号。
  6. - databses:这部分是数据文件存放区域,包含一个或多个数据库。
  7. - EOF:长度为1个字节,标志着RDB文件正文内容的结束,读取到此处标志键值对已经被恢复。
  8. - check_sum:长度为8字节的无符号整数,保存着根据前面几部分计算得出的校验和,用来检查RDB文件是否损坏。
  9. 2. **databases存储格式**
  10. databases部分可以保存任意多个非空数据库,每个数据库中的文件按照固定的数据格式来保存,如下所示:<br />![未命名文件.svg](https://cdn.nlark.com/yuque/0/2022/svg/2456868/1647828546944-774ece95-be49-4823-a3d0-079a8eeeca39.svg#clientId=u4640c251-54f9-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=uc05f1a79&name=%E6%9C%AA%E5%91%BD%E5%90%8D%E6%96%87%E4%BB%B6.svg&originHeight=83&originWidth=392&originalType=binary&ratio=1&rotation=0&showTitle=false&size=2270&status=done&style=none&taskId=u768a38bc-aa4d-417a-8f50-feab542dc4d&title=)
  11. - SELECTDB:长度为1个字节,当读取到这个标志时,意味着接下来读取的是一个数据库编号。
  12. - db_number:数据库的编号,根据号码大小这部分的长度可以动态调整,可以是125字节的长度。
  13. - key_value_pairs:保存了数据库中所有的键值对信息。
  14. 3. **key_value_pairs存储格式**
  15. key_value_pairs的存储根据实际情况有两种类型,过期时间也被包含在键值对中,除此之外,键值对中还包含了数据类型信息,每一个键值对一般包含四部分信息,其中过期时间时可选项。
  16. - 不包含过期时间的键值对
  17. ![未命名文件 (1).svg](https://cdn.nlark.com/yuque/0/2022/svg/2456868/1647829218746-47845373-a4ec-4c37-8dbb-015f3922a902.svg#clientId=u4640c251-54f9-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=ud7b9a82b&name=%E6%9C%AA%E5%91%BD%E5%90%8D%E6%96%87%E4%BB%B6%20%281%29.svg&originHeight=83&originWidth=392&originalType=binary&ratio=1&rotation=0&showTitle=false&size=2250&status=done&style=none&taskId=u9513415c-1474-4c57-a76c-eda094428e1&title=)
  18. - 包含过期时间的键值对
  19. ![未命名文件 (2).svg](https://cdn.nlark.com/yuque/0/2022/svg/2456868/1647829234191-50d4472e-b12c-492b-b597-18d39fd57def.svg#clientId=u4640c251-54f9-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=ud06d581a&name=%E6%9C%AA%E5%91%BD%E5%90%8D%E6%96%87%E4%BB%B6%20%282%29.svg&originHeight=83&originWidth=589&originalType=binary&ratio=1&rotation=0&showTitle=false&size=3590&status=done&style=none&taskId=u0a5e7cb4-3f56-4f5a-9aac-81d4edba67f&title=)<br />区别就在于过期键值对包含了以ms为单位的过期时间信息,其中各部分表示的信息如下:
  20. - TYPE:长度为1字节,记录了value的类型,Redis读取RDB文件的键值对时会根据TYPE的类型来解释键值对信息,Redis中定义了以下集中TYPE的值:
  21. - REDIS_RDB_TYPE_STRING
  22. - REDIS_RDB_TYPE_LIST
  23. - REDIS_RDB_TYPE_SET
  24. - REDIS_RDB_TYPE_ZSET
  25. - REDIS_RDB_TYPE_HASH
  26. - REDIS_RDB_TYPE_LIST_ZIPLIST
  27. - REDIS_RDB_TYPE_SET_INTSET
  28. - REDIS_RDB_TYPE_ZSET_ZIPLIST
  29. - REDIS_RDB_TYPE_HASH_ZIPLIST
  30. - key:是一个字符串对象,编码格式与"String Encoding"类型一致
  31. - valuekey对应的键值对的值,根据其保存数据类型的不同,value的格式也不一样
  32. - EXPIRETIME_MS:长度为1字节的标志位,当读取到此处时,告知接下来读取的数据为以ms为过期时间的值。
  33. - ms:长度为8字节的过期时间值,可以为负数。
  34. 4. **value编码类型**
  35. value部分保存了一个值对象,每个值对象保存的数据都与TYPE记录的类型所对应。因此value的编码格式根据valueTYPE,一个字节标志指示用于保存 Value 的编码。
  36. > - 0 = "String Encoding"
  37. > - 1 = "List Encoding"
  38. > - 2 = "Set Encoding"
  39. > - 3 = "Sorted Set Encoding"
  40. > - 4 = "Hash Encoding"
  41. > - 9 = "Zipmap Encoding"
  42. > - 10 = "Ziplist Encoding"
  43. > - 11 = "Intset Encoding"
  44. > - 12 = "Sorted Set in Ziplist Encoding"
  45. > - 13 = "Hashmap in Ziplist Encoding" (Introduced in rdb version 4)
  46. 下面对Redis中不同对象类型的编码进行简单分析:
  47. - **Length Encoding**
  48. 每个对象中都存储了占用长度,长度编码用于存储流中下一个对象的长度,长度编码是一种可变字节编码,旨在使用尽可能少的字节。长度编码的过程如下:
  49. - 从流中读取一个字节,读取两个最高有效位。
  50. - 如果起始位为 00,则接下来的 6 位表示长度
  51. - 如果起始位为 01,则从流中读取一个额外的字节。组合的 14 位表示长度。
  52. - 如果起始位为 10,则丢弃剩余的 6 位。从流中读取额外的4个字节,这4个字节表示长度(在RDB版本6中以大端格式)
  53. - 如果起始位为 11,则下一个对象将以特殊格式编码。其余 6 位指示格式。此编码通常用于将数字存储为字符串,或存储编码的字符串。请参阅字符串编码
  54. 储存长度时,由于这种编码方式:
  55. - 最多 63 个(包括 63 个)的数字可以存储在 1 个字节中。
  56. - 最多 16383 的数字(包括 16383)可以存储在 2 个字节中。
  57. - 最多 2^32 -1 的数字可以存储在 5 个字节中。
  58. - **String Encoding**
  59. Redis字符串是二进制安全,这意味着你可以存放任意数据,字符串没有任何的特殊字符串作为结尾标记,最好将Redis的字符串看作一个字节数组。Redis有三种String类型:
  60. - Length Prefixed String:长度前缀字符串非常简单。字符串的长度(以字节为单位)首先使用"Length Encoding"进行编码。在此之后,将存储字符串的原始字节。
  61. - An 8, 16 or 32 bit integer(一个8bit16bit或者64bit所表示的整数):首先阅读"Length Encoding"部分,具体是前两位为11时的部分。在这种情况下,读取剩余的6位。
  62. - 0 表示后面跟着一个 8 位整数
  63. - 1 表示后面跟着一个 16 位整数
  64. - 2 表示后面跟着一个 32 位整数
  65. - A LZF compressed string(使用LZF压缩的字符串):首先阅读"Length Encoding"部分,特别是前两位为11的部分。在这种情况下,将读取剩余的 6 位。如果这 6 位的值为 4,则表示后面跟着压缩字符串。
  66. - 使用"长度编码"从流中读取压缩的长度 clen
  67. - 使用"长度编码"从流中读取未压缩的长度
  68. - 从流中读取下一个 clen 字节
  69. - 最后,使用LZF算法解压缩这些字节
  70. - **List Encoding**
  71. RedisList使用一些列的String类型来表示,进行数据存储
  72. - 首先,使用"Length Encoding"从流中读取列表大小的大小
  73. - 接下来,使用"字符串编码"从流中读取大小字符串
  74. - 然后使用这些字符串重新构造列表
  75. - **Set Encoding**
  76. Set集合的编码与List列表完全相同。
  77. - **Hash Encoding**
  78. - 首先,使用"Length Encoding"从流中读取哈希大小的大小。
  79. - 接下来,使用"字符串编码"从流中读取2 * 大小的字符串,备用字符串是键和值。
  80. 示例:读取 2 us washington india delhi,对应如下格式:
  81. ```java
  82. {"us" => "washington", "india" => "delhi"}

上面简单介绍了几种对象的编码方式,更多编码相关内容,查看https://github.com/sripathikrishnan/redis-rdb-tools/wiki/Redis-RDB-Dump-File-Format

AOF

AOF全称Append Only File,AOF通过特定条件触发服务器写操作追加进行内存数据状态持久化。既然已经有了RDB为什么还要有AOF,上面已经说明RDB保存的是某个时刻的数据快照,在子进程保存过程中父进程新接收的请求增加或者修改的数据不会被处理,会造成一定的数据丢失,AOF 的主要作用是解决数据持久化的实时性和完整性问题。
fork.svg
AOF中存储的内容为固定格式的文本文件,设置一个新的键值对:

  1. 127.0.0.1:6379> set aof testaof
  2. OK

查看AOF文件内容:

  1. root@starsray:/var/lib/redis# cat /var/lib/redis/appendonly.aof
  2. *2$6SELECT$10*3$3set$3aof$7testaof

AOF创建与还原

  1. AOF功能开启

AOF的开启通过redis.conf的配置项来配置开启

  1. # yes 开启 no 关闭
  2. appendonly yes
  3. # AOF文件的名称
  4. appendfilename "appendonly.aof"

AOF持久化的过程主要包含命令追加(append)、文件写入和文件同步(sync)的过程。这里需要明确文件写入和文件同步的概念:

为了提高文件的写入效率,在现代操作系统中,当用户调用write函数,将一些数据写入到文件的时候,os通常会将写入数据暂时保存在一个内存缓冲区里面(例如,unix系统实现在内核中设有缓冲区高速缓存或页高速缓存,当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘),这种方式称为延迟写,等到缓冲区的空间被填满,或者超过了指定的时限,或者内核需要重用缓冲区存放其它磁盘块数据时,才会真正将缓冲区中的所有数据写入到磁盘里面。 这种做法虽然提高了效率,但也为写入数据带来了安全问题,如果计算机停机,则保存在缓冲区中的写入数据将丢失。为了保持一致性,即向文件写入数据立即真正的写入到磁盘上的文件中,而不是先写到内存缓冲区里面,则我们需要采取文件同步。 简单理解就是: 文件写入:只是写入到了内存缓冲区,可能还没有写到文件所拥有的磁盘数据块上
文件同步:将缓冲区中的内容冲洗到磁盘上

  1. AOF触发时机

当AOF开启时,服务器在执行一个写命令之后,同样会将这个命令追加到aof_buf缓冲区,Redis事件循环根据配置项appendfsync策略确定调用flushAppendOnlyFile函数的时机进行数据落盘。其中appendfsync参数有三个可选配置项。

  • always Redis 在每个事件循环都要将 AOF 缓冲区中的所有内容写入到 AOF 文件,并且同步 AOF 文件,所以 always 的效率是 appendfsync 选项三个值当中最差的一个,但从安全性来说,也是最安全的。当发生故障停机时,AOF 持久化也只会丢失一个事件循环中所产生的命令数据。
  • everysec Redis 在每个事件循环都要将 AOF 缓冲区中的所有内容写入到 AOF 文件中,并且每隔一秒就要在子线程中对 AOF 文件进行一次同步。从效率上看,该模式足够快。当发生故障停机时,只会丢失一秒钟的命令数据。
  • no Redis 在每一个事件循环都要将 AOF 缓冲区中的所有内容写入到 AOF 文件。而 AOF 文件的同步由操作系统控制。这种模式下速度最快,但是同步的时间间隔较长,出现故障时可能会丢失较多数据。

appendfsync的默认配置项为everysec,在考虑性能和数据安全性的同时,采取了一个折中的策略,即使数据丢失也最多损失一秒钟的数据内容。

此外针对这三种刷盘策略,需要注意一个参数配置no-appendfsync-on-rewrite,这个里默认的配置为no,官方解释信息为:

When the AOF fsync policy is set to always or everysec, and a background saving process (a background save or AOF log background rewriting) is performing a lot of I/O against the disk, in some Linux configurations Redis may block too long on the fsync() call. Note that there is no fix for this currently, as even performing fsync in a different thread will block our synchronous write(2) call. In order to mitigate this problem it’s possible to use the following option that will prevent fsync() from being called in the main process while a BGSAVE or BGREWRITEAOF is in progress. This means that while another child is saving, the durability of Redis is the same as “appendfsync none”. In practical terms, this means that it is possible to lose up to 30 seconds of log in the worst scenario (with the default Linux settings). If you have latency problems turn this to “yes”. Otherwise leave it as “no” that is the safest pick from the point of view of durability.

分析:当AOF的fsync策略设置为always或者everysec时,后台进程会产生大量的磁盘I/O,在一些Linux配置中Redis可能会因为fsync调用阻塞时间过长,目前没有解决此问题的方法,因为即使在不同的线程中执行 fsync 也会阻塞同步 write(2) 调用。为了缓解这个问题,可以使用以下选项来防止在 BGSAVE 或 BGREWRITEAOF 正在进行时在主进程中调用 fsync()。当子进程正在保存时,Redis 的持久性与“appendfsync none”相同,在最坏的情况下(使用默认的 Linux 设置)可能会丢失多达 30 秒的日志。
结论:如果您有延迟问题,请将其设置为“yes”。 否则,将其保留为“no”,从耐用性的角度来看,这是最安全的选择。

  1. AOF的还原

Redis如果同时开启了RDB和AOF会优先使用AOF来进行数据恢复,在启动过程中可以在启动日志看到Redis对AOF文件的加载。

  1. 11118:M 21 Mar 2022 18:20:27.745 * DB loaded from append only file: 0.000 seconds
  2. 11118:M 21 Mar 2022 18:20:27.745 * Ready to accept connections

AOF文件中包含了数据库重建所包含的所有写命令,在启动过程中只需要执行这些命令就可以恢复数据,启动过程中会创建一个不带网络链接的伪客户端(fake client)来读取并执行AOF文件中的命令,直到所有命令被处理完毕,所有的数据就被恢复到Redis内存中。

  1. AOF配置项
  • aof-load-truncated yes/no

当AOF文件被截断时,即AOF文件的最后命令不完整,如果此时启动Redis,会将AOF数据加载回内存,此时便会出现问题。

  • yes:加载一个截断的AOF,Redis服务器开始发出日志,通知用户该事件;
  • no:服务器将中止并出现错误,拒绝启动。

当我们得知AOF文件报错时,可以用redis-check-aof 来修复出错的 AOF 文件。

  • aof-use-rdb-preamble yes/no

在重写AOF文件时,Redis能够在AOF文件中使用RDB前导,以加快重写和恢复速度。启用此选项后,重写的AOF文件由两个不同的节组成RDB file、AOF tail。加载Redis时,会识别AOF文件以Redis字符串开头,并加载带前缀的RDB文件,然后继续加载AOF尾部。

AOF重写

AOF重写功能是对AOF持久化功能的一个优化方案。AOF记录了所有写命令,持续的记录会导致文件体积越来越大,而且还会出现一些无效命令(所谓无效命令指的是一个key被重复设置,我们不关心中间过程,只关心这个key的最终结果),因此Redis提供了重写机制来压缩文件体积,重写后的AOF由于文件体积减小,也便于Redis的快速重启。

  1. AOF重写原理

Redis提供的AOF重写功能,新文件在替换旧文件时并没有通过读旧文件中的写命令来获取结果,而是通过读取服务器中最新的数据库状态来获取值,这样就需要关心执行过程。例如下面的命令中,如果记录完整的写命令需要记录三条,但是服务器如果想用最少的命令来记录当前结果,最简单的办法就是直接读取数据库来获取当前值。

  1. 127.0.0.1:6379> set key hello
  2. OK
  3. 127.0.0.1:6379> set key world
  4. OK
  5. 127.0.0.1:6379> set key redis
  6. OK
  7. 127.0.0.1:6379> get key
  8. "redis"

AOF重写的过程由aof_rewrite函数执行操作,在数据量较大时,这个过程会进行大量的写操作并且会导致服务器阻塞。借鉴于RDB的实现方式,AOF的重写由子进程来完成,父进程继续处理客户端请求,但是这样同样会出现RDB中数据不完整的问题,在子进程操作的过程中,父进程又处理了新的写操作,就会导致数据不一致。为了解决这个问题,Redis服务器内置了一个AOF重写缓冲区,这个缓冲区在子进程创建完成后开始使用,父进程会同时将写命令发送至AOF缓冲区和AOF重写缓冲区,执行过程如图所示:
fork (1).svg

  • 当客户端发起BGREWRITEAOF或者服务端通过配置自动触发,此时检测BGREWRITEAOF或者BGSAVE是否在执行,如果存在会延迟执行BGREWRITEAOF,在BGREWRITEAOF执行过程中,再发起BGSAVE会被拒绝。
  • 父进程会fork一个子进程来重写AOF文件,当子进程完成重写后会发送信号到父进程,父进程调用信号处理函数继续对文件处理。
  • 父进程将AOF重写缓冲区的所有内容写入到新的AOF文件中,对新的AOF文件重命名原子的覆盖现有AOF文件,完成新旧文件替换。
  • 信号处理函数执行完毕后,一次AOF备份完成,父进程正常处理请求,信号处理函数执行这个过程会造成父进程阻塞,其他时间不阻塞,尽可能低的降低对服务器的延时影响。
  1. AOF触发条件

AOF 重写可以由用户通过调用 BGREWRITEAOF 手动触发。或者通过配置项触发,服务器在 AOF 功能开启的情况下, 会维持以下三个变量:

  • 记录当前 AOF 文件大小的变量 aof_current_size 。
  • 记录最后一次 AOF 重写之后, AOF 文件大小的变量 aof_rewrite_base_size 。
  • 增长百分比变量 aof_rewrite_perc 。

每次当 serverCron 函数执行时, 它都会检查以下条件是否全部满足, 如果是的话, 就会触发自动的 AOF 重写。redis.conf中提供了aof_current_size和aof_rewrite_perc相关的配置项:

  1. auto-aof-rewrite-percentage 100
  2. auto-aof-rewrite-min-size 64mb
  • auto-aof-rewrite-percentage:触发AOF重写的文件增长百分比,默认为100%,也就是当某次写入后AOF膨胀为2倍会触发AOF重写。
  • auto-aof-rewrite-min-size:触发AOF重写的最小文件大小,默认为64M。

    总结

    Redis中提供了RDB和AOF两种持久化方案,各自实现原理和适用场景都不同,下面对两者的特点进行总结:
  1. RDB
  • 优势
    • RDB是一个非常紧凑的单文件时间点的Redis数据表示,因此非常适合备份,可以传输到遥远的数据中心,进行灾难恢复。
    • RDB最大限度地提高了Redis的性能,因为Redis父进程需要做的唯一的工作是为了持久化Redis子进程,它将做所有其余的工作。父进程永远不会执行磁盘I/O或类似的操作。
    • 此外与AOF相比,RDB允许使用大数据集更快地重新启动。
  • 缺点
    • 由于RDB存储的是时间间隔内某个时刻的数据快照,因此有可能会丢失一个时间间隔的据。
    • RDB经常需要fork(),以便使用子进程在磁盘上持久化。如果数据集很大,fork()可能会花费时间,如果数据集很大,CPU性能不是很好,可能会导致Redis停止为客户端服务几毫秒甚至一秒钟。即使fork()使用了COW技术来优化内存占用,但此时如果出现大量key修改依然会需要大量的内存空间消耗。
  1. AOF
  • 优势
    • AOF让Redis的数据可靠性更高,可以使用不同的fsync策略always,everysec,no。在默认的每秒fsync策略下,写性能仍然很好(fsync是使用后台线程执行的,当没有fsync进行时,主线程会努力执行写操作),在everysec策略下最多损失1秒的写操作。
    • AOF日志是一个仅追加的日志,因此不存在查找,也不存在断电时的损坏问题。即使日志由于某种原因(磁盘满了或其他原因)导致AOF文件不完整,redis-check-aof工具也能够轻松地修复它。
    • AOF以易于理解和解析的格式一个接一个地包含所有操作的日志。您甚至可以轻松地导出AOF文件。例如,即使你不小心用FLUSHALL命令刷新了所有的数据,只要在此期间没有重写日志,你仍然可以通过停止服务器,删除最新的命令,重新启动Redis来保存你的数据集。
  • 缺点
    • 对于同一个数据集,AOF文件通常比等效的RDB文件大。
    • AOF可能比RDB慢,这取决于具体的fsync策略。通常,fsync设置为每秒的性能仍然非常高,禁用fsync时,即使在高负载下,它也应该和RDB一样快。即使在巨大的写负载情况下,RDB的延迟明显要比AOF要低。
    • 如果在重写期间有写入数据库的操作(这些操作被缓冲在内存中,并在最后写入新的AOF), AOF会使用大量内存。所有在重写期间到达的写命令都被写入磁盘两次。
  1. 结论

一般的建议是,如果你想要达到PostgreSQL所能提供的数据安全程度,你应该使用这两种持久性方法。如果您非常关心您的数据,但是在发生灾难时仍然可以忍受几分钟的数据丢失,那么您可以只使用RDB。有很多用户单独使用AOF,但是我们不鼓励这样做,因为对于进行数据库备份、更快地重新启动以及在AOF引擎出现bug的情况下,不时地使用RDB快照是一个很好的主意。

官方通知:

注意:由于所有这些原因,我们可能会在将来(长期计划)将AOF和RDB统一为一个持久性模型。