把学习当成一种习惯
选择往往大于努力,越努力越幸运

前言

  什么是持久化 : Redis读写速度快、性能优越是因为它将所有数据存在了内存中,然而,当Redis进程退出或重启后,所有数据就会丢失,所以我们希望Redis能保存数据到硬盘中,在Redis服务重启之后,原来的数据能够恢复,这个过程就叫持久化.
  MySQL的InnoDB存储引擎通过 redo log、bin log 这两个日志保证了持久化.

Redis的两种持久化方式 --- RDB、AOF

  Redis 提供了两种持久化功能 : RDB、AOF ,但两种功能无论在磁盘上的存储格式、使用性能、安全性以及使用场景等方面上是有区别的.

RDB

  RDB持久化既可以手动执行,也可以根据服务器配置选项定期执行(即可以在某个时间点上达到配置选项的条件而进行持久化RDB文件中).由于RDB文件是保存在磁盘中,所以Redis服务器进程退出后,甚至运行的计算机停机了,只要RDB文件存在,Redis服务器就可以利用RDB文件来还原数据库状态.

接下来分别看看RDB的两种执行方式 : 手动执行、配置选项定期执行.

手动执行

手动执行 : Redis提供了SAVE、BGSAVE两种命令来生成RDB文件

  • SAVE : 该命令会阻塞Redis服务器进程,直到RDB文件生成完毕为止,在Redis服务器进程生成RDB文件期间(阻塞状态),Redis服务器进程不会处理任何命令请求(所有命令都会被拒绝)
  • BGSAVE : 该命令与SAVE不同,BGSAVE会通过调用fork()生成一个子进程,该子进程负责创建生成RDB文件,而服务器进程(父进程)继续处理请求.

配置选项定期执行

  Redis允许通过设置服务器配置save选项,让服务器每隔一段时间自动执行一次BGSAVE命令.我们可以通过save选项设置多个保存条件,只要满意其中一个条件,服务器就会执行一次BGSAVE命令.

通过save命令可以生成保存条件,例如以下 :

  • save 900 1 : 服务器900秒之内,对数据库进行了至少1次修改(写入、删除、更新)
  • save 300 10 : 服务器300秒之内,对数据库进行了至少10次修改(写入、删除、更新)
  • save 60 10000 : 服务器60秒之内,对数据库进行了至少10000次修改(写入、删除、更新)

  Redis服务端维护着一个saveparams属性(数组)来存放这些执行的选项条件,除此之外,还通过 dirty计数器、lastsave属性来配合判断是否执行 BGSAVE命令 :

  • dirty计数器 : 记录距离上次成功执行SAVE命令或者BGSAVE命令之后,服务器对数据库状态进行了多少次修改(写入、删除、更新)
  • lastsave属性 : 该属性是一个UNIX时间戳,记录了服务器上次成功执行SAVE命令或者BGSAVE命令的时间

  Redis服务器会周期性的执行serverCron(),默认每隔100ms执行一次,该函数用于对正在执行的服务器进行维护,其中一项就是检查sava选项所设置的条件是否存在已经满足的,如果存在的话则执行BGSAVE,具体伪代码如下 :


# ... 其他操作

# 遍历saveparams所有保存的条件
for saveparams in server.saveparams:
    
    # 根据lastsave属性计算距离上次执行保存操作多少秒
    save_interval = unixtime_now() - server.lastsave
    
    # 如果数据量状态的修改次数超过条件所设置的次数 并且
    # 距离上次保存的时间超过条件锁设置的时间
    # 则执行BGSAVE操作
    if server.dirty >= saveparam.changes and
       save_interval > saveparam.seconds:
       
        BGSAVE()
        

# ... 其他操作

小结

  • 可通过手动执行 、 save配置选项定期执行 两种方式来保存RDB文件;
  • BGSAVE命令会通过调用fork()来创建子进程来创建新的RDB文件;当 Redis 调用执行 fork() 时,操作系统会使用写时复制策略(CopyOnWrite)来创建一个子进程创建新的RDB文件,父、子进程共享同一内存数据(物理内存),当父进程要修改某个数据时(执行一条写命令),操作系统会将这个共享内存数据另外复制一份给子进程使用,以此来保证子进程的正确运行.因此,新的 RDB 文件存储的是执行 fork() 过程中的内存数据.
  • serverCron()默认每隔100ms执行一次,通过 saveparams数组、dirty计数器、lastsave属性来配合判断是否执行 BGSAVE命令;
  • 当调用操作系统的fock()子进程,即使使用了COW技术,但是如果出现父进程多次的写命令而修改了共享内存,那么此时需要多次复制共享内存数据给子进程以保证子进程正确的运行,此时就会消耗内存,因为内存中不仅保存了当前数据库数据,还会保存 fork() 过程中的内存数据.

RDB相关配置 --- 了解

  • stop-writes-on-bgsave-error yes : 当执行 BGSAVE 命令出现错误时,Redis 是否终止执行写命令.参数的值默认被设置为 yes,表示当硬盘出现问题时,服务器可以及时发现,及时避免大量数据丢失;当设置为 no 时,就算执行 BGSAVE 命令发生错误,服务器也会继续执行写命令;当对 Redis 服务器的系统设置了监控时,建议将该参数值设置为 no.
  • rdbcompression yes : 是否开启 RDB 压缩文件,默认为 yes 表示开启,不开启则设置为 no.
  • rdbchecksum yes : 是否开启 RDB 文件的校验,在服务器进行 RDB 文件的写入与读取时会用到它.默认设置为 yes.如果将它设置为 no,则在服务器对 RDB 文件进行写入与读取时,可以提升性能,但是无法确定 RDB 文件是否已经被损坏.

RDB文件结构

RDB文件结构如下图 :

  • REDIS : 这个部分是一个长度为5字节,保存着 "REDIS" 五个字符,通过这五个字符可以快速检验载入的文件是否RDB文件.
  • db_version : 占用4字节,是一个字符串表示的整数,表示RDB文件的版本号,例如 "0006"
  • databases : 该部分包含着各个数据库中的键值对数据;
    • 如果服务器的数据库状态为空,该部分也为空,即长度占用0字节.
    • 如果服务器的数据库状态至少含有一个数据库,那么会存放各个数据库对应所保存的键值对的数量、类型、内容等.
  • EOF : 占用1字节,表示RDB正文内容的结束,即当读取哦到该值时,表示所有数据库的所有键值对都载入到内存了.
  • check_num : 占用8个字节的无符号整数,保存着一个检验和;这个检验和通过前面四个部分的内容进行计算出来的.当服务器在载入RDB文件时,会通将载入数据所计算出的检验和与check_num进行对比,以此来检查RDB文件是否出错或者损坏的情况.

  核心看看databases字段,如下图保存了0号、1号两个非空数据库,并且每个非空数据库对应着 SELECTDB、db_number、key_value_pairs 三个字段 :

  • SELECTDB : 占用1字节,当程序遇到这个值的时候,那么它将直到接下来将会读取到一个数据库号码;
  • db_number : 保存了数据库号码,当程序读取到这个值的时候,服务器会调用SELECT命令,并且根据该值保存的数据库号码来进行数据库切换.
  • key_value_pairs : 保存了数据库中所有的键值对数据,包括带有过期时间的键值对.

key_value_pairs,结构如下图 :

  • 不带过期键值对 :
    • TYPE : 占用1字节,记录了value的存储类型,比如字符串、链表、哈希表等
    • key : 总是一个字符串对象
  • 带过期键值对 : 在不带过期键值对的基础上新增了 EXPIRETIME_MS 、 ms :
    • EXPIRETIME_MS : 占用1字节,表示接下来将会读取到一个以毫秒为单位的过期时间.
    • ms : 一个8字节长的带符号整数,记录了以毫秒为单位的UNIX时间戳,即该键值对的过期时间.

AOF

AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的,如下图 :

AOF持久化功能的实现主要分为命令追加、文件写入、文件同步三个步骤 :

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

# 写操作
SET KEY VALUE

# 协议格式
*3\rn$3\r\nSET\rn$3\r\nKEY\r\n$5\r\nVALUE\r\n


  • 文件写入、同步 : 文件的写入是指将aof_buf缓冲区的内容写入到磁盘中;而同步操作依赖于appendfsync选项值来决定.
    在上面我们有提到serverCron()会根据sava配置项来执行BGSAVE命令,该函数不止只包含该作用,现在我们又看到它包含把客户端发送的写命令以协议格式写入到aof_buf中,然后根据appendfsync选项值来执行同步操作.

appendfsync选项值有三种选项 :

  • always : 将aof_buf缓冲区所有内容写入并同步到AOF
  • everysec : 将aof_buf缓冲区所有内容写入到AOF,如果上次同步AOF文件的时间距离现在超过一秒钟,那么再次对AOF文件进行同步操作,这里的同步操作是由一个线程专门负责.(默认选项)
  • no : 将aof_buf缓冲区所有内容写入到AOF,当并不同步操作,具体由操作系统决定

  文件写入磁盘,这里涉及到操作系统的一个知识点 : 在现代操作系统中,对于文件写入操作,为了提高写入效率,通常会将写入的数据暂时保存在一个内存缓冲区中,等到缓冲区的空间被填满、或者超过了指定的时限之后,才真正地将缓冲区中的数据写入到磁盘中;这种做法虽然带来了效率,但是带来了安全性问题,即如果计算机发生停机,那么保存在内存缓冲区的写入数据将会丢失.
  为此,操作系统提供了fsync和fdatasync两个同步函数,它们可以强制让操作系统立即将缓冲区中的数据写入到磁盘中,从而保证了数据的安全性.

【PS : 对于文件I/O同步操作,与MySQL的InnoDB存储引擎的rego log 写入到磁盘都是类似的】

appendfsync三种选项值的安全性、效率 :

  • always : 安全性最高、效率最低,即使出现宕机,AOF持久化也只会丢失一个时间循环所产生的命令数据.
  • everysec : 每个写入命令都会把aof_buf缓存区的内容写入到AOF,但是每个一秒才会同步到磁盘中.效率最高,但是出现宕机情况可能会丢失一秒钟的命令数据.
  • no : 每个写入命令都会把aof_buf缓存区的内容写入到AOF,但是同步操作由操作系统控制.当出现宕机情况,可能会丢失上次同步AOF文件之后的所有写命令数据.

AOF的重写

  随着服务器运行时间的流逝,AOF文件中的内容会越来越多,文件体积越来越大,如果不加以控制的话,体积过大的AOF文件很可能对Redis服务器、甚至整个宿主计算机造成影响,因为AOF文件体积越大,使用AOF文件来进行数据还原所需的时间就越多.
  AOF重写也是调用fork()创建子进程来重写,父进程依然可以继续接收命令.
  AOF在重写过程中,如果父进程有接收到新的写命令,那么父进程会把这些写命令追加到AOF缓冲区、AOF重写缓冲区(子进程重写过程中使用的重写缓冲区);
  当子进程处理完成后,会把AOF重写缓冲区的内容写入到新的AOF文件中,然后原子的覆盖之前所有旧的AOF文件.

小结

  AOF持久化内容是执行的写入命令;只要涉及到写入操作,都会以协议格式拼接到aof_buf缓冲区中,通过serverCron()的周期性调用以及appendfsync选项值把AOF写入并同步到磁盘中.
  AOF的重写是为了防止AOF文件过大而导致服务器重启的时候重新载入数据的执行时间过长 、甚至严重点会影响宿主计算机的性能问题.

总结

  • RDB 优点 :
    • ①备份 : 根据设置的保存条件来统一保存到磁盘中,它保存了某个时间点的数据,非常适用于数据的备份,比如你可以在每个小时保存一下过去24小时内的数据,同时每天保存过去30天的数据.
    • ②文件紧凑 : RDB 文件是一个经过压缩的二进制文件,文件紧凑,体积较小.
    • ③性能 : RDB在保存RDB文件时父进程唯一需要做的就是fork出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他IO操作,所以RDB持久化方式可以最大化redis的性能.
    • ④灾难恢复 : RDB 持久化适用于灾难恢复,而且恢复数据时的速度要快于 AOF 持久化.
  • RDB 缺点 :
    • ①安全性 : 在服务器出现故障时,如果没有触发 RDB 快照执行,那么它可能会丢失大量数据.RDB 快照的持久化方式决定了必然做不到实时持久化,会存在大量数据丢失.
    • ②内存消耗 : 当数据量非常庞大时,在保存 RDB 文件的时候,服务器会启动一个子进程来完成相关的保存操作.这项操作比较耗时,将会占用太多 CPU 时间,从而影响服务器的性能.
  • AOF 优点 :
    • ①更加持久化 : 使用 AOF 持久化会让 Redis 持久化更长,通过设置不同的 fsync 策略来达到更长的持久化.
    • ②兼容性 : AOF 文件是一个日志文件,它的作用是记录服务器执行的所有写命令.当文件因为某条写命令写入失败时,可以使用 redis-check-aof 进行修复,然后继续使用.
    • ③重写 : 当 AOF 文件的体积过大时,在后台可以自动地对 AOF 文件进行重写,因此数据库当前状态的所有命令集合都会被重写到 AOF 文件中.重写完成后,Redis 就会切换到新的 AOF 文件,继续执行写命令的追加操作.
  • AOF 缺点 :
    • ①文件体积过大 : AOF 文件的体积会随着时间的推移逐渐变大,导致在加载时速度会比较慢,进而影响数据库状态的恢复速度,性能快速下降.
    • ②根据所使用的 fsync 策略,使用 AOF 文件恢复数据的速度可能会慢于使用 RDB 文件恢复数据的速度.

RDB 和 AOF 的加载问题 :

  • 如果在 Redis 配置文件中开启了 AOF 持久化(appendonly yes),那么在启动服务器的时候会优先加载 AOF 文件来还原数据库状态.
  • 如果在 Redis 配置文件中关闭了 AOF 持久化(appendonly no),那么在启动服务器的时候会优先加载 RDB 文件来还原数据库状态.

结束语

  • 原创不易
  • 希望看完这篇文章的你有所收获!

相关参考资料

  • Redis设计与实现【书籍】

目录