文章目录

  • 前言
  • 1. RDB 文件持久化的优缺点
  • 2. RDB 的触发方式
  • 2.1 save 命令触发
  • 2.2 bgsave 命令触发
  • 2.3 定时任务触发
  • 2.4 主从同步全量复制触发
  • 3. RDB 的处理流程
  • 3.1 RDB 文件传输
  • 3.2 socket 无盘传输
  • 4. RDB 涉及的技术原理
  • 4.1 写时复制 Copy On Write
  • 4.2 diskless 无盘传输的管道读写


前言

Redis 是基于内存的 KV 数据库,使用时所有数据都在内存中,这就是它存取性能高的重要原因之一。但是如我们所知,保存在内存中的数据是有风险的,一旦机器停电或者意外宕机就会彻底丢失

Redis 持久化机制就是为了应对这种情况,其主要手段是将内存中的数据保存到磁盘上,宕机重启后再通过磁盘上的持久化文件将数据恢复。RDB 是 Redis 持久化机制的一种,它会将内存中的所有数据进行快照保存,并且以二进制文件形式存储到硬盘上

1. RDB 文件持久化的优缺点

相对于Redis 的另一种持久化机制 AOF 来说, RDB 持久化的优缺点可以总结如下:

优点

  1. RDB 文件保存的是 Redis 在某个时间点的数据集,二进制形式使其占用磁盘空间很小,非常适于备份和灾难恢复
  2. 生成RDB文件的时候,Redis 主进程可以 fork 一个子进程来处理所有保存工作,主进程可以继续提供服务,不需要进行任何磁盘IO操作
  3. RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快,这是因为 RDB 文件直接加载进内存就可以恢复数据,AOF 文件加载进内存的是一条条 Redis 命令,需要执行后才能恢复数据

缺点

  1. RDB 需要保存内存中整个数据集,通常这个操作是很耗时的,因此一般保存 RDB 文件的操作间隔是比较长的。在这种情况下,一旦发生故障停机,此前没有来得及写入磁盘的数据都将丢失。也就是说,RDB 机制的数据可靠性比 AOF 低,可能丢失的数据更多
  2. 每次保存 RDB 的时候,Redis 可以 fork 出一个子进程来进行实际的持久化工作,但在数据集比较庞大时,fork 操作本身可能就非常耗时,从而导致 fork 完成前主进程无法处理任何请求

虽然经过 fork 操作的父子进程共享内存中的数据,但是父进程需要拷贝内存页表给子进程。如果 Redis 中数据内存占用很大,需要拷贝的内存页表很多,那么相应的拷贝操作就会比较耗时

2. RDB 的触发方式

redis rdb 设置 redis的rdb_缓存

2.1 save 命令触发

Redis 服务端接收到 save 命令时会在主线程中进行保存 RDB 文件的操作,需注意 Redis 对业务逻辑的处理都在主线程中串行执行,因此如果 RDB 需要保存的数据量大耗时长的话,会影响服务端对其他客户端命令的处理响应,不建议使用

Redis 6.0 源码中这个命令由rdb.c#saveCommand() 函数处理

2.2 bgsave 命令触发

Redis 服务端接收到 bgsave 命令时会 fork 一个子进程进行后台保存 RDB 文件的操作,这样主线程依然可以处理其他客户端命令,不影响线上使用

在 Redis 6.0 的源码中该命令由rdb.c#bgsaveCommand() 函数处理

2.3 定时任务触发

Redis 的server.c#serverCron() 定时任务会检查一定时间内数据变动是否超过配置文件中配置的值,如果超过则会触发 rdb.c#rdbSaveBackground()函数使用子进程后台完成 RDB 文件的保存

2.4 主从同步全量复制触发

Redis 6.0 源码阅读笔记(10)-主从复制 Master 节点流程分析 一文中提到过,当从节点连接上主节点的时候,主节点会根据从节点携带的参数判断是否能够进行部分复制,不能进行部分复制则需要后台保存 RDB 进行全量复制。此处保存 RDB 的方式与 bgsave 命令的处理基本一致,都是父进程 fork 子进程,由子进程进行 RDB 数据的保存工作,不过后续对 RDB 数据的处理有两种不同的方式,下文将详细介绍

3. RDB 的处理流程

本节主要介绍主从同步全量复制触发保存 RDB 的流程,事实上其他几种 fork 子进程保存 RDB 的处理也与此类似

3.1 RDB 文件传输

redis rdb 设置 redis的rdb_缓存_02

  • disk有盘传输
    这种方式由主节点子进程将生成的 RDB 数据写入临时文件,完成后将临时文件作为新的 RDB 文件替换掉旧的文件。父进程的定时任务定期检查子进程保存 RDB 的动作是否完成,完成则读取 RDB 文件将其传输给从节点

3.2 socket 无盘传输

redis rdb 设置 redis的rdb_rdb_03

  • diskless 无盘传输
    如果主从节点确认可以直接通过 socket 传输数据,则不需要保存 RDB 文件。这种模式下,子进程每生成一部分 RDB 数据就从管道写端写入,父进程则在管道读端读取 RDB 数据,并通过 socket 直接将其传输到从节点

4. RDB 涉及的技术原理

4.1 写时复制 Copy On Write

Redis 主进程可以 fork 一个子进程来执行 RDB 持久化,二者共享同一份内存空间。fork 操作之后主进程可以继续对外提供服务,那么必然存在对内存的写操作,如果共享的内存数据因此发生改变,那子进程保存的 RDB 就不能称为数据快照。因此,这其中必然有一种机制保证了主进程修改的内存数据对子进程不可见,其实这就采用了 Copy On Write 技术

redis rdb 设置 redis的rdb_redis_04

  • Copy On Write 原理 fork 操作之后,内核会把父进程中所有的内存页都设为只读权限,然后将子进程的地址空间指向父进程。当其中某个进程写内存时,CPU 检测到内存页是只读的,于是触发页异常中断(page-fault),从而进入内核态。内核会把触发异常的页复制一份分配给写内存的进程,于是这个进程可以在复制的页上进行写操作,而不会更改共享的内存数据
  • Copy On Write 缺点 如果在 fork 后父子进程都还需要继续进行写操作,那么会产生大量的页异常中断(page-fault),造成内核态切换频繁,性能损失较大

Redis 在 rehash 阶段写操作是无法避免的,所以在 fork 出子进程之后会调用 server.c#updateDictResizePolicy() 关闭 rehash,尽量减少写操作,最大限度地节约内存

4.2 diskless 无盘传输的管道读写


redis rdb 设置 redis的rdb_redis rdb 设置_05

管道是进程间通信方式的一种,不过管道只能用于具有亲缘关系的进程间的通信,在各个程进行通信时,它们共享文件描述符。管道的一般用法是,进程在使用 fork 函数创建子进程前先创建一个管道,用于在父子进程间通信,然后再创建子进程

管道两端的任务是固定的,一端只能用于读,由描述符fd[0]表示,称为管道读端;另一端只能用于写,由描述符fd[1]来表示,称为管道写端。数据在管道中是单向流动的,如果试图从管道写端读数据,或者向管道读端写数据都将导致出错