type
status
date
slug
summary
tags
category
password

1、概述

Redis是一个内存数据库,所有的数据将保存在内存中,这与传统的 MySQL、Oracle、SqlServer 等关系型数据库直接把数据保存到硬盘相比,Redis的读写效率非常高。但是保存在内存中也有一个很大的缺陷,一旦断电或者宕机,内存数据库中的内容将会全部丢失。为了弥补这一缺陷,Redis 提供了把内存数据持久化到硬盘文件,以及通过备份文件来恢复数据的功能,即 Redis 的持久化机制。有两种方式:
持久化方式
描述
优点
缺点
适合场景
AOF
记录所有写操作命令,以追加方式写入日志文件,重启时重新执行这些命令来恢复数据
数据安全性高,使用 everysec 策略最多丢失一秒数据,使用 always 策略则几乎不会丢失数据。
文件体积大,数据恢复速度慢
文件小,恢复速度快,适合用于定时快照备份和主从全量数据同步的场景。
RDB
按照指定时间间隔生成 Redis 数据库快照(dump.rdb 文件),这些快照保存了某个时间点的全量 Redis 数据。可以使用数据库快照来恢复数据或者实现主从复制。
二进制文件体积小,恢复速度快,便于快速备份和灾难恢复
数据安全性低,如果 Redis 意外宕机,从上一次快照到宕机时的数据都会丢失。
文件大,但是大部分情况保留了原始的命令操作,适合用于对数据完整性和安全性更高的场景中。

2、AOF

AOF 持久化方式会记录每一次写操作命令,并将这些命令追加到一个日志文件的末尾。当 Redis 重启时,会重新执行 AOF 文件中的所有命令来重建数据。
注意只会记录写操作命令,读操作命令是不会被记录的。

2.1 开启AOF

第一种方式,通过配置文件开启(推荐),修改 Redis 配置文件 redis.conf
第二种方式,可以通过 Redis 客户端临时开启 AOF(重启后会失效):
💡
AOF 文件默认保存在 Redis 的工作目录(由 dir 配置指定),文件名为 appendonly.aof

2.2 AOF的工作原理

AOF 的操作步骤为:
  1. 命令追加(append):当 AOF 持久化功能打开时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器的 AOF 缓冲区的末尾。(先写内存,再写日志。这样可以保证只有正确执行的命令才会记录到日志上。)
  1. 文件的写入和同步(sync):根据配置的 appendfsync 同步刷盘策略决定何时将 AOF 缓冲区的内容保存到磁盘的 AOF 文件中(原理是调用 Linux 的 fsyncfdatasync 函数)。
AOF 提供三种同步策略(通过 appendfsync 配置):
配置项
写回时机
优点
缺点
Always
每次执行完一个命令之后, write 和 sync 都会被执行。也就是说每个写命令都会同步写入磁盘
可靠性高,数据基本不丢失
性能较差
Everysec
默认策略,write 和 sync 每隔一秒钟就会执行一次。也就是说每秒会同步一次磁盘,该操作由一个线程专门负责。
安全性和性能的折中方案
宕机丢失1s的数据
No
write 都会被执行, 但 sync 会被略过。也就是说将 aof_buf 缓存区的内容写入 AOF 文件,但什么时候同步到磁盘由操作系统来决定
性能好
宕机丢失数据多

2.2.1 AOF缓冲区

AOF 缓冲区是 Redis 服务器中的一个内存缓冲区。当 Redis 执行完一个写命令后,这个命令会按照 AOF 协议格式被追加到这个缓冲区中。
AOF 缓冲区主要目的是解决性能问题:如果每执行一个写命令就立即将其写入磁盘的 AOF 文件,那么磁盘 I/O 操作会非常频繁,严重拖累 Redis 的性能(Redis 的核心优势就是基于内存的高速读写)。AOF 缓冲区作为一个中间层,先将命令在内存中积累起来,然后根据配置的 appendfsync 策略,统一、批量地写入磁盘,大大减少了磁盘 I/O 次数。

2.3 AOF重写机制

AOF 日志是一个文件,随着执行的写操作命令越来越多,文件的大小会越来越大。如果当 AOF 日志文件过大就会带来性能问题,比如重启 Redis 后,需要读 AOF 文件的内容以恢复数据,如果文件过大,整个恢复的过程就会很慢。
假设你对一个计数器 mycounter 执行了 100 次 INCR 命令。
  • 原始的 AOF 文件会忠实地记录这 100 条 INCR mycounter 命令。
  • 最终在数据库里,mycounter 的值是 100。
  • 但事实上,要达到这个状态,我们只需要一条命令SET mycounter 100
为了解决 AOF 文件体积膨胀的问题,Redis 提供了 AOF 重写机制。当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。
AOF 重写的触发条件:
  • 手动触发:执行 BGREWRITEAOF 命令
  • 自动触发:在 redis.conf 配置文件中进行设置,当满足以下两个条件时,Redis 会自动触发 BGREWRITEAOF 。下面的配置意味着:如果 AOF 文件已经大于 64MB,并且其当前大小比上次重写后的大小至少大了一倍(100%),那么就会自动触发重写。
    AOF 重写过程如下:
    1. 主线程 fork 出一个 bgrewriteaof 子进程,这里的 fork() 使用写时拷贝(copy-on-write)的技术,拷贝出一份数据副本,并建立一个 AOF 重写缓冲区
    1. 子进程读取当前数据库数据的所有键值对的最新值,生成新的 AOF 文件。注意:AOF 重写不会去读旧的 AOF 文件,而是读取当前数据库中的所有键值对,然后为每一个键值生成一条命令记录到新的 AOF 文件中。
      1. notion image
    1. 子线程重写期间,如果服务器收到了写命令,主线程会先把写命令写到原有的 AOF 缓冲区(这部分数据会写到旧的 AOF 文件,这样保证即使重写失败也能保证数据的安全),然后再把这些命令写入上面创建的 AOF 重写缓冲区
    1. 子进程重写完成后,通知主线程,主线程收到信号后会把 AOF 重写缓冲区的内容写到新的 AOF 文件(不会直接写到新的 AOF 文件里面,而是先写 AOF 缓冲区,再写回新的 AOF 文件)
    1. 最后主线程把新的 AOF 文件进行改名,覆盖原有的 AOF 文件,之后所有的写命令都会开始追加到这个新文件上。
    整个过程只有最后两步是阻塞主线程的,这将 AOF 重写对性能造成的影响降到了最低。
    notion image
    这里涉及到两个知识点:
    • AOF 重写缓冲区
    • 写时复制(Copy-On-Write)技术

    2.3.1 AOF重写缓冲区

    AOF 重写缓冲区也是一个内存缓冲区。它是在 Redis 启动 AOF 重写(BGREWRITEAOF 子进程时才被创建的。
    在子进程创建并生成新 AOF 文件的过程中,主进程还在持续接收和处理新的写命令。这会导致子进程的内存快照(fork 时刻的数据库状态)和当前数据库的实际状态不一致。所以 AOF 重写缓冲区就是为解决这个问题而生的。
    • 在重写 AOF 期间,当 Redis 执行完一个写命令之后,除了会将写命令写入到 AOF 缓冲区(保证原来的 AOF 文件正常记录), 还会将命令同时写入 AOF 重写缓冲区
    • 在子进程重写完 AOF 文件后,会向主进程发送一个信号。主进程收到信号后,会将 AOF 重写缓冲区 中的所有内容追加到子进程生成的新 AOF 文件的末尾。这样,新 AOF 文件就包含了重写开始时刻的快照数据 + 重写过程中产生的增量数据,保证了数据的完整性。
    AOF 缓冲区和 AOF 重写缓冲区对比
    特性
    AOF 缓冲区
    AOF 重写缓冲区
    目的
    平衡性能与持久化,减少磁盘 I/O 次数。
    保证重写期间的数据一致性,防止新数据丢失。
    创建时机
    Redis 服务器启动时(如果开启了 AOF)。
    AOF 重写开始时(执行 BGREWRITEAOF 时)。
    写入时机
    任何时候执行写命令后。
    仅在 AOF 重写过程中执行写命令后。
    内容
    所有写命令的协议文本。
    重写过程中产生的所有写命令的协议文本。
    清空时机
    内容被同步到主 AOF 文件后。
    内容被追加到新的重写 AOF 文件后。
    生命周期
    长期存在
    临时存在,仅在重写期间存在。

    2.3.2 写时复制(Copy-On-Write)技术

    写时复制技术(COW)的核心思想是:如果有多个调用者同时请求相同资源,他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给该调用者,而其他调用者所见到的资源仍然保持不变。简单来说:只有在真正需要写入的时候,才进行复制。
    Redis 是一个单线程的内存数据库(主要工作线程是单线程的)。这意味着所有客户端的命令都必须在这个独享的线程中顺序执行。
    如果有一个非常耗时的操作在这个主线程中运行,比如需要生成一个巨大的 RDB 快照文件(持久化),或者需要加载一个巨大的 AOF 文件,那么主线程就会被阻塞,无法处理任何新的命令,这对于一个高性能数据库来说是灾难性的。
    为了解决这个问题,Redis 的 RDB 持久化和 AOF 重写都是在后台子进程中执行的。
    • BGSAVE: 在后台子进程中创建当前数据集的 RDB 快照。
    • BGREWRITEAOF: 在后台子进程中重写 AOF 文件以减小体积。
    这里就产生了一个问题:主进程在不断地处理新的读写命令,内存数据在不断变化,而子进程需要为某一时刻的数据生成一个快照。如何保证快照的一致性?
    解决方案就是 COW 机制 + fork()
    1. fork() 子线程:Redis 使用操作系统的 fork() 系统调用来创建后台子进程。fork() 的作用是创建一个与父进程完全相同的子进程。在 fork() 之后,内核并不会立即复制父进程的整个内存空间给子进程。相反,它会将父进程的物理内存页标记为只读(read-only,并让父子进程共享这些内存页。
    1. 触发异常:当父进程(处理客户端请求的主进程)接收到一个写命令(如 SETDEL 等),试图修改某一块共享的内存数据时:,CPU 硬件检测到内存页是只读的,于是触发页异常中断(page-fault)。
    1. 内核介入:操作系统内核的页错误处理程序会介入。
    1. 复制页面:内核会将被修改的内存页(通常是 4KB 大小)复制一份新的副本。
    1. 父进程写入新数据:将父进程的页表项指向这个新复制的页面,并标记为可写。父进程的写操作在这个新页面上继续进行,修改成功。
    1. 子进程读取旧数据:而此时,子进程的页表项仍然指向旧的、未被修改的内存页面。子进程看到的数据就像是 fork() 发生那一刻的静态快照。所以子线程备份的是旧数据。
    notion image
    举个例子:
    假设 Redis 内存中有一个很大的 Hash 表。当子进程通过 fork() 创建时,父子进程共享这个 Hash 表。
    • 如果父进程没有收到任何修改这个 Hash 的命令,子进程就能一直“读”这个共享的 Hash 表,并把它写入 RDB 文件,零复制成本
    • 如果父进程要修改这个 Hash 表中的某个字段,那么仅会复制这个字段所在的具体内存页(4KB),而不是整个 Hash 表,更不是整个数据库。修改完成后,子进程依然看到的是旧的数据。
    优势:
    • 高效节省内存:理论上,子进程不需要消耗与父进程一样大的内存。它只在父进程有写入操作时,才消耗额外的内存(复制的页面)。如果父进程几乎没有写入,那么子进程的内存消耗就非常小。
    • 高性能:fork() 本身通常非常快,因为它不需要复制整个地址空间。这使得创建后台子进程的成本很低,Redis 可以频繁地执行 bgsave 或 bgrewriteaof 而不会对性能造成太大影响。
    • 保证快照一致性:子进程中的数据是 fork() 那一刻的完美快照,保证了 RDB 文件的数据一致性。
    缺点:
    • 写操作可能比传统方式稍慢(需要先复制)
    • 父线程如果产生大量写操作,会产生大量的页中断异常,也会造成性能损失。

    3、RDB

    RDB 通过创建某个时间点的数据快照(RDB 文件)来实现持久化。RDB 文件是压缩的二进制文件,默认名为 dump.rdb,保存着 Redis 某个时间点的全量数据。
    注意:执行快照是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响。如果频率太低,服务器故障时,丢失的数据会更多。通常可能设置至少 5 分钟才保存一次快照,这时如果 Redis 出现宕机等情况,则意味着最多可能丢失 5 分钟数据。

    3.1 开启RDB

    • 自动触发:通过修改 redis.conf 配置文件自动触发。底层通过服务器周期函数 serverCron 函数来触发,该函数每 100 毫秒执行一次。默认配置如下所示:
    • 手动触发:使用 Redis 命令手动触发。Redis 提供了两个命令来生成 RDB 文件,区别就在于是否在「主线程」里执行
      • SAVE 命令:Redis 主进程负责创建 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程
      • BGSAVE 命令:Redis 主进程会 fork 出一个子进程,由子进程来创建 RDB 文件,这样可以避免主线程的阻塞
    验证 RDB 是否工作
    在输出中查找与 RDB 相关的信息,如:
    • rdb_last_save_time:上次成功保存的时间戳
    • rdb_last_bgsave_status:上次后台保存状态

    3.2 RDB的工作过程

    以最常用的 BGSAVE 命令为例,执行过程如下:
    1. fork 子进程:主线程 fork 出一个 BGSAVE 子进程,这里的 fork() 同样使用写时复制(COW)的技术。这意味着父子进程最初共享相同的内存页,只有当父进程(处理写请求)要修改某一块数据时,操作系统才会将该数据页复制一份给子进程使用。。
    1. 子进程负责写入:子进程开始将共享的内存数据写入一个临时的 RDB 文件(例如 temp-<pid>.rdb)。
    1. 替换旧文件:当子进程完成对所有数据的写操作后,会用这个新的临时 RDB 文件原子地替换掉旧的 dump.rdb 文件。
    1. 发送信号并退出:子进程完成后通知父进程,然后自身退出。
    整个过程只有 fork 阶段会阻塞父进程(通常很短),而将数据写入磁盘的繁重工作由子进程完成,确保了主进程的高性能。
    不管是自动触发还是手动触发创建 RDB 文件,最终都会调用 rdbSave 函数来创建 RDB 文件。
    notion image
     
    注意:
    • 为了避免产生竞争条件, BGSAVE 执行时,SAVE 命令不能执行。
    • 为了避免性能问题, BGSAVEBGREWRITEAOF 不能同时执行。
    • RDB 文件的加载工作是在服务器启动时自动执行的,Redis 并没有提供专门用于加载 RDB 文件的命令。
    • 调用 rdbLoad 函数载入 RDB 文件时,不能进行任何和数据库相关的操作,不过订阅与发布方面的命令可以正常执行,因为它们和数据库不相关联。
    • 如果服务器在启动时, 打开了 AOF 功能, 那么程序优先使用 AOF 文件来还原数据。 只有在 AOF 功能未打开的情况下, Redis 才会使用 RDB 文件来还原数据。这是因为在服务器发生故障时,RDB 丢失的数据会比 AOF 持久化的方式更多,因为 RDB 快照是全量快照的方式,因此执行的频率不能太频繁,否则会影响 Redis 性能,而 AOF 日志可以以秒级的方式记录操作命令,所以丢失的数据就相对更少。

    4、混合模式(RDB + AOF)

    还有一种混合模式:同时开启 AOF 和 RDB 功能。这是 Redis 4.0 之后引入的一个强烈推荐的选项。它结合了 RDB 和 AOF 的优点:
    1. 创建 RDB 快照文件以一定的频率执行。
    1. 在两次创建 RDB 文件期间,使用 AOF 日志记录这期间的所有命令操作。
    1. 在下一次创建 RDB 的时候,清空 AOF 日志。
    1. 在恢复数据的时候,会先加载 RDB 文件的内容(速度非常快),然后再重放增量 AOF 命令。大大提升了重启效率。
    notion image
    要开启混合持久化,需要在配置文件中设置:
    Redis系列:事件机制和线程模型Redis系列:键空间和内存策略
    Loading...