关于java:源码级别了解-Redis-持久化

2次阅读

共计 7910 个字符,预计需要花费 20 分钟才能阅读完成。

文章首发于公众号“蘑菇睡不着”,欢送来访~

前言

  大家都晓得 Redis 是一个内存数据库,数据都存储在内存中,这也是 Redis 十分快的起因之一。尽管速度提上来了,然而如果数据始终放在内存中,是非常容易失落的。比方 服务器敞开或宕机了,内存中的数据就木有了。为了解决这一问题,Redis 提供了 长久化 机制。别离是 RDB 以及 AOF 长久化。

RDB

什么是 RDB 长久化?

RDB 长久化能够在指定的工夫距离内生成数据集的工夫点快照(point-in-time snapshot)。

RDB 的长处?

  • RDB 是一种示意某个即时点的 Redis 数据的紧凑文件。RDB 文件实用于备份。例如,你可能想要每小时归档最近 24 小时的 RDB 文件,每天保留近 30 天的 RDB 快照。这容许你很容易的复原不同版本的数据集以容灾。
  • RDB 非常适合于劫难复原,作为一个紧凑的繁多文件,能够被传输到近程的数据中心。
  • RDB 最大化了 Redis 的性能。因为 Redis 父过程长久化时惟一须要做的是启动 (fork) 一个子过程,由子过程实现所有残余的工作。父过程实例不须要执行像磁盘 IO 这样的操作。
  • RDB 在重启保留了大数据集的实例比 AOF 快。

    RDB 的毛病?

  • 当你须要在 Redis 进行工作 (例如停电) 时最小化数据失落,RDB 可能不太好。你能够配置不同的保留点 (save point) 来保留 RDB 文件(例如,至多 5 分钟和对数据集 100 次写之后,然而你能够有多个保留点)。然而,你通常每隔 5 分钟或更久创立一个 RDB 快照,所以一旦 Redis 因为任何起因没有正确敞开而进行工作,你就得做好最近几分钟数据失落的筹备了。
  • RDB 须要常常调用 fork()子过程来长久化到磁盘。如果数据集很大的话,fork()比拟耗时,后果就是,当数据集十分大并且 CPU 性能不够弱小的话,Redis 会进行服务客户端几毫秒甚至一秒。AOF 也须要 fork(),然而你能够调整多久频率重写日志而不会有损 (trade-off) 持久性(durability)。

RDB 文件的创立与载入

  有个两个 Redis 命令能够用于生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE
  SAVE 命令会阻塞 Redis 服务器过程,直到 RDB 文件创建结束为止,在服务器过程阻塞期间,服务器不能解决任何命令申请。

> SAVE     // 始终等到 RDB 文件创建结束
OK

和 SAVE 命令间接阻塞服务器过程不同的是,BGSAVE 命令会派生出一个子过程,而后由子过程负责创立 RDB 文件,服务器过程 (父过程) 持续解决命令过程。
执行 fork 的时候操作系统(类 Unix 操作系统)会应用写时复制(copy-on-write)策略,即 fork 函数产生的一刻父子过程共享同一内存数据,当父过程要更改其中某片数据时(如执行一个写命令),操作系统会将该片数据复制一份以保障子过程的数据不受影响,所以新的 RDB 文件存储的是执行 fork 一刻的内存数据。

> BGSAVE  // 派生子过程,并由子过程创立 RDB 文件
Background saving started

生成 RDB 文件由两种形式:一种是手动,就是上边介绍的用命令的形式;另一种是主动的形式。
接下来具体介绍一下主动生成 RDB 文件的流程。
Redis 容许用户通过设置服务器配置的 save 选项,让服务器每隔一段时间主动执行一次 BGSAVE 命令。
用户能够通过在 redis.conf 配置文件中的 SNAPSHOTTING 下 save 选项设置多个保留条件,但只有其中任意一个条件被满足,服务器就会执行 BGSAEVE 命令。
如,以下配置:
save 900 1
save 300 10
save 60 10000
上边三个配置的含意是:

  • 服务器在 900 秒内,对数据库进行了至多 1 次批改。
  • 服务器在 300 秒内,对数据库进行了至多 10 次批改。
  • 服务器在 60 秒内,对数据库进行了至多 10000 次批改。

如果没有手动去配置 save 选项,那么服务器会为 save 选项配置默认参数:
save 900 1
save 300 10
save 60 10000
接着,服务器就会依据 save 选项的配置,去设置服务器状态 redisServer 构造的 saveparams 属性:

struct redisServer{

  // ...
  
  // 记录了保留条件的数组
  struct saveparams *saveparams;
  
  // ...
};

saveparams 属性是一个数组,数组中的每一个元素都是一个 saveparam 构造,每个 saveparam 构造都保留了一个 save 选项设置的保留条件:

struct saveparam {

  // 秒数
  time_t seconds;
  
  // 批改数
  int changes;
};

除了 saveparams 数组之外,服务器状态还维持着一个 dirty 计数器,以及一个 lastsave 属性;

struct redisServer {
    // ...
    
    // 批改计数器
    long long dirty;
    
    // 上一次执行保留工夫
     time_t lastsave;
     
     // ...
}
  • dirty 计数器记录间隔上一次胜利执行 SAVE 或 BGSAVE 命令之后,服务器对数据库状态 (服务器中的所有数据库) 进行了多少次批改(包含写入、删除、更新等操作)。
  • lastsave 属性是一个 UNIX 工夫戳,记录了服务器上一次执行 SAVE 或 BGSAVE 命令的工夫。

查看条件是否满足触发 RDB

Redis 的服务器周期性操作函数 serverCron 默认每隔 100 毫秒执行一次,该函数用于对正在运行的服务器进行保护,它的其中一项工作就是查看 save 选项所设置的保留条件是否曾经满足,如果满足的话就执行 BGSAVE 命令。
Redis serverCron 源码解析如下:

程序会遍历并查看 saveparams 数组中的所有保留条件,只有有任意一个条件被满足,服务器就会执行 BGSAVE 命令。
上面是 rdbSaveBackground 的源码流程:

RDB 文件构造

下图展现了一个残缺 RDB 文件所蕴含的各个局部。

redis 文件的最结尾是 REDIS 局部,这个局部的长度是 5 字节,保留着“REDIS”五个字符。通过这五个字符,程序能够在载入文件时,疾速查看所载入的文件是否时 RDB 文件。

db_version 长度为 4 字节,他的值时一个字符串示意的整数,这个整数记录了 RDB 文件的版本号,比方“0006”就代表 RDB 文件的版本为第六版。

database 局部蕴含着零个或任意多个数据库,以及各个数据库中的键值对数据:

  • 如果服务器的数据库状态为空(所有数据库都是空的),那么这个局部也为空,长度为 0 字节。
  • 如果服务器的数据库状态为非空(有至多一个数据库非空),那么这个局部也为非空,依据数据库所保留键值对的数量、类型和内容不同,这个局部的长度也会有所不同。

EOF 常量的长度为 1 字节,这个常量标记着 RDB 文件注释内容的完结,当读入程序遇到这个值后,他晓得所有数据库的所有键值对曾经载入结束了。

check_sum 是一个 8 字节长的无符号整数,保留着一个校验和,这个校验和时程序通过对 REDIS、db_version、database、EOF 四个局部的内容进行计算得出的。服务器在载入 RDB 文件时,会将载入数据所计算出的校验和与 check_sum 所记录的校验和进行比照,以此来查看 RDB 是否有出错或者损坏的状况。
举个例子:下图是一个 0 号数据库和 3 号数据库的 RDB 文件。第一个就是“REDIS”示意是一个 RDB 文件,之后的“0006”示意这是第六版的 REDIS 文件,而后是两个数据库,之后就是 EOF 完结标识符,最初就是 check_sum。

AOF 长久化

什么是 AOF 长久化

AOF 长久化形式记录每次对服务器写的操作, 当服务器重启的时候会从新执行这些命令来复原原始的数据,AOF 命令以 redis 协定追加保留每次写的操作到文件开端.Redis 还能对 AOF 文件进行后盾重写, 使得 AOF 文件的体积不至于过大.

AOF 的长处?

  • 应用 AOF 会让你的 Redis 更加持久: 你能够应用不同的 fsync 策略:无 fsync, 每秒 fsync, 每次写的时候 fsync. 应用默认的每秒 fsync 策略,Redis 的性能仍然很好(fsync 是由后盾线程进行解决的, 主线程会尽力解决客户端申请), 一旦呈现故障,你最多失落 1 秒的数据.
  • AOF 文件是一个只进行追加的日志文件, 所以不须要写入 seek, 即便因为某些起因 (磁盘空间已满,写的过程中宕机等等) 未执行残缺的写入命令, 你也也可应用 redis-check-aof 工具修复这些问题.
  • Redis 能够在 AOF 文件体积变得过大时,主动地在后盾对 AOF 进行重写:重写后的新 AOF 文件蕴含了复原以后数据集所需的最小命令汇合。整个重写操作是相对平安的,因为 Redis 在创立新 AOF 文件的过程中,会持续将命令追加到现有的 AOF 文件外面,即便重写过程中产生停机,现有的 AOF 文件也不会失落。而一旦新 AOF 文件创建结束,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
  • AOF 文件有序地保留了对数据库执行的所有写入操作,这些写入操作以 Redis 协定的格局保留,因而 AOF 文件的内容非常容易被人读懂,对文件进行剖析(parse)也很轻松。导出(export)AOF 文件也非常简单:举个例子,如果你不小心执行了 FLUSHALL 命令,但只有 AOF 文件未被重写,那么只有进行服务器,移除 AOF 文件开端的 FLUSHALL 命令,并重启 Redis,就能够将数据集复原到 FLUSHALL 执行之前的状态。

AOF 的毛病?

  • 对于雷同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
  • 依据所应用的 fsync 策略,AOF 的速度可能会慢于 RDB。在个别状况下,每秒 fsync 的性能仍然十分高,而敞开 fsync 能够让 AOF 的速度和 RDB 一样快,即便在高负荷之下也是如此。不过在解决微小的写入载入时,RDB 能够提供更有保障的最大延迟时间(latency)。

AOF 长久化的实现

AOF 长久化性能的实现能够分为命令追加(append)、文件写入、文件同步(sync)三个步骤。

命令追加

当 AOF 长久化性能处于关上状态时,服务器在执行完一个写命令之后,会以协定格局将被执行的写命令追加到服务器状态的 aof_buf 缓冲区的开端。

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

如果客户端向服务器发送以下命令:

> set KEY VALUE
OK

那么服务器在执行这个 set 命令之后,会将以下协定内容追加到 aof_buf 缓冲区的开端;

*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n

AOF 文件的写入与同步

  Redis 的服务器过程就是一个事件循环(loop),这个循环中的文件事件负责接管客户端
的命令申请,以及向客户端发送命令回复,而工夫事件则负责执行像 serverCron 函数这样需
要定时运行的函数。
  因为服务器在解决文件事件时可能会执行写命令,使得一些内容被追加到 aof_buf 缓冲区
外面,所以在服务器每次完结一个事件循环之前,它都会调用 flushAppendOnlyFile 函数,考
虑是否须要将 aof_buf 缓冲区中的内容写入和保留到 AOF 文件外面,这个过程能够用以下伪代
码示意:

def eventLoop():
  while True:
  
  #解决文件事件,接管命令申请以及发送命令回复
  #解决命令申请时可能会有新内容被追加到 aof_buf 缓冲区中
  processFileEvents()
  
  #解决工夫事件
  processTimeEvents()
  
  #思考是否要将 aof_buf 中的内容写入和保留到 AOF 文件外面
  flushAppendOnlyFile()

flushAppendOnlyFile 函数的行为由服务器配置的 appendfsync 选项的值来决定,各个不同
值产生的行为如下表所示。

appendfsync 选项的值 flushAppendOnlyFile 函数的行为
always 将 aof_buf 缓冲区中的所有内容写入并同步到 AOF 文件
everysec 将 aof_buf 缓冲区中的所有内容写入到 AOF 文件,如果上次同步 AOF 文件的工夫间隔当初超过一秒钟,那么再次对 AOF 文件进行同步,并且这个同步操作是由一个线程专门负责执行的
no 将 aof_buf 缓冲区中的所有内容写入到 AOF 文件,但并不对 AOF 文件进行同步,何时同步由操作系统来决定

如果用户没有被动为 appendfsync 选项设置值,那么 appendfsync 选项的默认值为 everysec。
写到这里有的小伙伴可能会对下面说的写入和同步含意弄混,这里说一下:
写入:将 aof_buf 中的数据写入到 AOF 文件中。
同步:调用 fsync 以及 fdatasync 函数,将 AOF 文件中的数据保留到磁盘中。
艰深地讲就是,你要往一个文件写货色,写的过程就是写入,而同步则是将文件保留,数据落到磁盘上。
大家之前看文章的时候是不是大多都说 AOF 最多失落一秒钟的数据,那是因为 redis AOF 默认是 everysec 策略,这个策略每秒执行一次,所以 AOF 长久化最多失落一秒钟的数据。

AOF 文件的载入与数据还原

因为 AOF 文件外面蕴含了重建数据库状态所需的所有写命令,所以服务器只有读入并从新执行一遍 AOF 文件外面保留的写命令,就能够还原服务器敞开之前的数据库状态。Redis 读取 AOF 文件并还原数据库状态的具体步骤如下:

  1. 创立一个不带网络连接的伪客户端(fake client):因为 Redis 的命令只能在客户端上 下文中执行,而载入 AOF 文件时所应用的命令间接来源于 AOF 文件而不是网络连接,所以服 务器应用了一个没有网络连接的伪客户端来执行 AOF 文件保留的写命令,伪客户端执行命令 的成果和带网络连接的客户端执行命令的成果齐全一样。
  2. 从 AOF 文件中剖析并读取出一条写命令。
  3. 应用伪客户端执行被读出的写命令。
  4. 始终执行步骤 2 和步骤 3,直到 AOF 文件中的所有写命令都被处理完毕为止。
    当实现以上步骤之后,AOF 文件所保留的数据库状态就会被残缺地还原进去,整个过程 如下图所示。

AOF 重写

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

> rpush list "A" "B"
OK
> rpush list "C"
OK
> rpush list "D"
OK
> rpush list "E" "F"
OK

  那么光是为了记录这个 list 键的状态,AOF 文件就须要保留四条命令。
  对于理论的利用水平来说,写命令执行的次数和频率会比下面的简略示例要高得多,所 以造成的问题也会重大得多。为了解决 AOF 文件体积收缩的问题,Redis 提供了 AOF 文件重写(rewrite)性能。通过该 性能,Redis 服务器能够创立一个新的 AOF 文件来代替现有的 AOF 文件,新旧两个 AOF 文件所 保留的数据库状态雷同,但新 AOF 文件不会蕴含任何节约空间的冗余命令,所以新 AOF 文件 的体积通常会比旧 AOF 文件的体积要小得多。在接下来的内容中,咱们将介绍 AOF 文件重写的实现原理,以及 BGREWEITEAOF 命令 的实现原理。
  尽管 Redis 将生成新 AOF 文件替换旧 AOF 文件的性能命名为“AOF 文件重写”,但实际上,AOF 文件重写并不需要对现有的 AOF 文件进行任何读取、剖析或者写入操作,这个性能是通 过读取服务器以后的数据库状态来实现的。
  就像下面的状况,服务器齐全能够将这六条命令合并成一条。

> rpush list "A" "B" "C" "D" "E" "F"

  除了下面列举的列表键之外,其余所有类型的键都能够用同样的办法去缩小 AOF 文件中的命令数量。首先从数据库中读取键当初的值,而后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是 AOF 重写性能的实现原理。
  在理论中,为了防止在执行命令时造成客户端输出缓冲区溢出,重写程序在解决列表、哈希表、汇合、有序汇合这四种可能会带有多个元素的键时,会先查看键所蕴含的元素数 量,如果元素的数量超过了 redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD 常量的值,那 么重写程序将应用多条命令来记录键的值,而不单单应用一条命令。在目前版本中,REDIS_AOF_REWRITE_ITEMS_PER_CMD 常量的值为 64,这也就是 说,如果一个汇合键蕴含了超过 64 个元素,那么重写程序会用多条 SADD 命令来记录这个集 合,并且每条命令设置的元素数量也为 64 个。

AOF 后盾重写

  AOF 重写会执行大量的写操作,这样会影响主线程,所以 redis AOF 重写放到了子过程去执行。这样能够达到两个目标:

  • 子过程进行 AOF 重写期间,服务器过程(父过程)能够持续解决命令申请。
  • 子过程带有服务器过程的数据正本,应用子过程而不是线程,能够在防止应用锁的状况 下,保证数据的安全性。

然而有一个问题,当子过程重写数据时,主过程仍然在解决新的数据,这也就会造成数据不统一状况。
为了解决这种数据不统一问题,Redis 服务器设置了一个 AOF 重写缓冲区,这个缓冲区在 服务器创立子过程之后开始应用,当 Redis 服务器执行完一个写命令之后,它会同时将这个写 命令发送给 AOF 缓冲区和 AOF 重写缓冲区,如下图:

这也就是说,在子过程执行 AOF 重写期间,服务器过程须要执行以下三个工作:

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

这样一来能够保障:

  • AOF 缓冲区的内容会定期被写入和同步到 AOF 文件,对现有 AOF 文件的解决工作会如常 进行。
  • 从创立子过程开始,服务器执行的所有写命令都会被记录到 AOF 重写缓冲区外面。

当子过程实现 AOF 重写工作之后,它会向父过程发送一个信号,父过程在接到该信号之 后,会调用一个信号处理函数,并执行以下工作:

  1. 将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中,这时新 AOF 文件所保留的数 据库状态将和服务器以后的数据库状态统一。
  2. 对新的 AOF 文件进行改名,原子地(atomic)笼罩现有的 AOF 文件,实现新旧两个 AOF 文件的替换。

这个信号处理函数执行结束之后,父过程就能够持续像平常一样接受命令申请了。
在整个 AOF 后盾重写过程中,只有信号处理函数执行时会对服务器过程(父过程)造成 阻塞,在其余时候,AOF 后盾重写都不会阻塞父过程,这将 AOF 重写对服务器性能造成的影 响降到了最低。

Redis 混合长久化

Redis 还能够同时应用 AOF 长久化和 RDB 长久化。在这种状况下,当 Redis 重启时,它会优先应用 AOF 文件来还原数据集,因为 AOF 文件保留的数据集通常比 RDB 文件所保留的数据集更残缺。然而 AOF 复原比较慢,Redis 4.0 推出了 混合长久化

混合长久化:将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 自长久化开始到长久化完结 的这段时间产生的增量 AOF 日志,通常这部分 AOF 日志很小。

于是在 Redis 重启的时候,能够先加载 RDB 的内容,而后再重放增量 AOF 日志就能够齐全代替之前的 AOF 全量文件重放,重启效率因而大幅失去晋升。

感觉文章不错的话,小伙伴们麻烦点个赞、关个注、转个发一下呗~你的反对就是我写文章的能源。

更多精彩的文章请关注公众号“蘑菇睡不着”。

你越被动就会越被动,咱们下期见~

正文完
 0