Redis持久化机制详解

为什么需要持久化

Redis通常被作为缓存使用,但是Redis一旦宕机,内存中的数据全部丢失,可能会导致数据库崩溃。如果是从数据库中恢复这些数据就会存在频繁访问数据库和读取速度慢的问题。所以redis实现数据的持久化,是至关重要的。

Redis 为了实现⽆畏宕机快速恢复,设计了两⼤杀⼿锏,分别是 AOF(Append Only FIle)⽇志和 RDB 快照。

RDB(内存快照)

什么是RDB

RDB(内存快照)是Redis DataBase的缩写,所谓内存快照,就是指内存中的数据在某一个时刻的状态记录。这就类似于照片,当你给朋友拍照时,一张照片就能把朋友一瞬间的形象完全记下来。

对Redis来说,它实现类似照片记录效果的方式,就是把某一时刻的状态以文件的形式写到磁盘上,也就是快照。即使宕机,快照文件也不会丢失,数据的可靠性也就得到了保证。在做数据恢复时,我们可以直接把RDB文件读入内存,很快地完成恢复。RDB保存的是dump.rdb文件。

生成RDB策略

Redis提供了两个命令来生成RDB文件,分别是save和bgsave。

  • save:在主线程中执行,会导致阻塞(线上禁止使用);
  • bgsave:创建一个子进程,专门用于写入RDB文件,避免了主线程的阻塞,这也是Redis RDB文件生成的默认配置。
在对内存数据做「快照」的时候,内存数据还能修改么

避免阻塞和正常处理写操作并不是一回事。此时,主线程的确没有阻塞,可以正常接收请求,为了保证快照完整性,它只能处理读操作,因为不能修改正在执行快照的数据。

为了快照而暂停写操作,肯定是不能接受的。所以这个时候,Redis就会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。

简单来说,bgsave子进程是由主线程fork生成的,可以共享主线程的所有内存数据。bgsave子进程运行后,开始读取主线程的内存数据,并把它们写入RDB文件。

如果主线程对这些数据也都是读操作(例如图中的键值对A),那么,主线程和bgsave子进程相互不影响。但是,如果主线程要修改一块数据(例如图中的键值对C),那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave子进程会把这个副本数据写入RDB文件,而在这个过程中,主线程仍然可以直接修改原来的数据。

Redis 会使⽤ bgsave 对当前内存中的所有数据做快照,这个操作是⼦进程在后台完成的,这就允许主线程同时可以修改数据。

可以每秒执行RDB文件吗

过于频繁的执⾏全量数据快照,有两个严重性能开销:

  1. 频繁⽣成 RDB ⽂件写⼊磁盘,磁盘压⼒过⼤。会出现上⼀个 RDB 还未执⾏完,下⼀个⼜开始⽣成,陷⼊死循环。
  2. fork 出 bgsave ⼦进程会阻塞主线程,主线程的内存越⼤,阻塞时间越⻓。
RDB的优缺点

快照的恢复速度快,但是⽣成 RDB ⽂件频率不好把握,频率过低宕机丢失的数据就会⽐较多;太快,⼜会消耗额外开销。RDB 采⽤⼆进制 + 数据压缩的⽅式写磁盘,⽂件体积⼩,数据恢复速度快。

RDB持久化触发策略
  1. 自动触发策略

    自动触发策略,是指 Redis 在指定的时间内,数据发生了多少次变化时,会自动执行BGSAVE命令。自动触发的条件包含在了 Redis 的配置文件中。

    上图所示, save m n 的含义是在时间 m 秒内,如果 Redis 数据至少发生了 n 次变化,那么就自动执行BGSAVE命令。配置策略说明如下:

    save 3600 1 表示在 3600 秒内,至少更新了 1 条数据,Redis 自动触发 BGSAVE 命令,将数据保存到硬盘。

    save 300 10 表示在 300 秒内,至少更新了 10 条数据,Redis 自动触 BGSAVE 命令,将数据保存到硬盘。

    save 60 10000 表示 60 秒内,至少更新了 10000 条数据,Redis 自动触发 BGSAVE 命令,将数据保存到硬盘。 只要上述三个条件任意满足一个,服务器就会自动执行BGSAVE命令。当然您可以根据实际情况自己调整触发策略。

    演示操作

    配置:save 5 2 -- 5秒内2次修改

    修改dump文件保存路径

    bash 复制代码
    ##创建myredis文件夹
    cd /opt/redis-7.0.0
    mkdir myredis
    ##创建dumpfiles文件夹
    mkdir dumpfiles

    将dir . 改为 /myredis/dumpfiles

    修改dump文件名称

    修改完配置文件尽量都重启下服务。

    连接服务

    bash 复制代码
    ##进入/opt/redis-7.0.0
    cd /opt/redis-7.0.0
    ## 连接redis
    redis-cli
    auth 123456
    bash 复制代码
    ##执行命令(查看myredis/dumpfiles文件夹下是空的)
    set k1 v1
    ##5秒内执行命令(查看myredis/dumpfiles文件夹下有文件)
    set k2 v2

    恢复redis数据
    0. 先备份dump文件

    bash 复制代码
    ##备份
    cd /opt/redis-7.0.0/myredis/dumpfiles/
    mv dump6379.rdb dump6379.rdb.bak
    ​
    1. 清空redis数据
    bash 复制代码
    ##进入/opt/redis-7.0.0
    cd /opt/redis-7.0.0
    ## 连接redis
    redis-cli
    auth 123456
    FLUSHDB

    3.查看dump文件

    执行flushall/flushdb时,也会产生rdb文件,只不过里面是空的,没有意义。


    4. RDB文件载入

    和使用SAVE命令或者BGSAVE命令创建RDB文件不同,RDB文件的载入工作是在服务器启动时自动执行的,所以Redis并没有专门用于载入RDB文件的命令,只要Redis服务器在启动时检测到RDB文件存在,它就会自动载入RDB文件。

    服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。

  2. 手动触发策略

有两个Redis命令可以用于生成RDB文件,一个是SAVE,另一个是BGSAVE。

SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求:

ruby 复制代码
127.0.0.1:6379> save

和SAVE命令直接阻塞服务器进程的做法不同,BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求:

ruby 复制代码
127.0.0.1:6379> bgsave

会触发RDB快照的情况

  1. 配置文件中默认的快照配置
  2. 手动save/bgsave命令
  3. 执行flushall/flushdb命令也会产生dump.rdb文件,但里面是空的
  4. 执行shutdown且没有设置开启AOF持久化
  5. 主从复制时,主节点自动触发

AOF追加日志

AOF是Redis的另外一种持久化方式。AOF就是将Redis服务端执行过的每一条命令都保存到一个文件,这样当Redis重启时只要按顺序回放这些命令就会恢复到原始状态。默认情况下,redis是没有开启AOF(append only file)的功能,需要设置配置:appendonly:yes。

有了RDB为什么还需要AOF

RDB保存的是一个时间点的快照,如果Redis出现了故障,丢失的就是从最后一次RDB执行的时间点到故障发生的时间间隔之内产生的数据。如果Redis数据量很大,那么丢失的数据也会很多。

而AOF保存的是一条条命令,理论上可以做到发生故障时只丢失一条命令。但由于操作系统中执行写文件操作代价很大,Redis提供了配置参数,通过对安全性和性能的折中,我们可以设置不同的策略。

为什么不直接使用AOF

用AOF方法进行故障恢复的时候,需要逐一把操作日志都执行一遍。如果操作日志非常多,Redis就会恢复得很缓慢,影响到正常使用。

AOF的持久化实现

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

命令追加的原理

当AOF持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。

shell 复制代码
## 例如在redis中执行命令
> set K1 value

那么服务器在执行这个SET命令之后,会将以下协议内容追加到aof_buf缓冲区的末尾:

bash 复制代码
*3\r\n$3\r\nSET\r\n$2\r\nK1\r\n$5\r\nvalue\r\n
shell 复制代码
## 例如继续在redis中执行命令
> RPUSH k2 ONE TWO THREE

那么服务器在执行这个SET命令之后,会将以下协议内容追加到aof_buf缓冲区的末尾:

bash 复制代码
*5\r\n$5\r\nRPUSH\r\n$2\r\nK2\r\n$3\r\nONE\r\n$3\r\nTWO\r\n$5\r\nTHREE\r\n

以第一个例子中的参数为例:以*3开始,表示命令共有3个参数。 <math xmlns="http://www.w3.org/1998/Math/MathML"> 3 表示接下来的第 1 个参数( S E T )长度为 3 ,读取为 S E T ,第 1 个参数就解析完毕。以此类推, 3表示接下来的第1个参数(SET)长度为3,读取为SET,第1个参数就解析完毕。 以此类推, </math>3表示接下来的第1个参数(SET)长度为3,读取为SET,第1个参数就解析完毕。以此类推,2表示第2个参数的长度为2,读取为K1,$5表示第3个参数的长度为5,读取为value。至此,该条命令解析完毕。

AOF的写入和同步

为了提⾼⽂件的写⼊效率,当⽤户调⽤ write 函数,将⼀些数据写⼊到⽂件的时候,操作系统通常会将写⼊数据暂时保存在⼀个内存缓冲区⾥⾯, 等到缓冲区的空间被填满、或者超过了指定的时限之后,才真正地将缓冲区中的数据写⼊到磁盘⾥⾯。 这种做法虽然提⾼了效率,但也为写⼊数据带来了安全问题,因为如果计算机发⽣停机,那么保存在内存缓冲区⾥⾯的写⼊数据将会丢失。 为此,系统提供了 fsync 和 fdatasync 两个同步函数,它们可以强制让操作系统⽴即将缓冲区中的数据写⼊到硬盘⾥⾯,从⽽确保写⼊数据的安全性。 Redis 提供的 AOF 配置项 appendfsync 写回策略直接决定 AOF 持久化功能的效率和安全性。

  1. always:同步写回,写指令执⾏完毕⽴⻢将aof_buf缓冲区中的内容刷写到 AOF⽂件。
  2. everysec:每秒写回,写指令执⾏完,⽇志只会写到 AOF ⽂件缓冲区,每隔⼀秒就把缓冲区内容同步到磁盘。
  3. no: 操作系统控制,写执⾏执⾏完毕,把⽇志写到 AOF ⽂件内存缓冲区,由操作系统决定何时刷写到磁盘。

同步写回 :可以做到数据不丢失,但是每个"写"指令都需要写⼊磁盘,性能最差。 每秒写回 :避免了同步写回的性能开销,发⽣宕机可能有⼀秒位写⼊磁盘的数据丢失,在性能和可靠性之间做了折中。 操作系统控制:执⾏写指令后就写⼊ AOF ⽂件缓冲就可以执⾏后续的"写"指令,性能最好,但是有可能丢失很多的数据。

AOF文件的载入与数据还原

AOF文件包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态。

Redis读取AOF文件并还原数据库状态的详细步骤如下:

  1. 创建一个不带网络连接的伪客户端(fake client):因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令,伪客户端执行命令的效果和带网络连接的客户端执行命令的效果完全一样。
  2. 从AOF文件中分析并读取出一条写命令。
  3. 使用伪客户端执行被读出的写命令。
  4. 一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处理完毕为止。

当完成以上步骤之后,AOF文件所保存的数据库状态就会被完整地还原出来。

AOF重写机制

由于AOF持久化是通过保存被执行的写命令来记录数据库状态的,所以随着操作的不断增加,AOF文件中的内容会越来越多,文件的体积也会越来越大。由于体积越来越大如果要恢复数据则需要更多的时间。

可以看到为了记录这个list键的状态(如下图所示),AOF文件就需要保存六条命令。为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写(rewrite)功能。

shell 复制代码
redis> RPUSH list "A" "B" //["A","B"]
redis> RPUSH list "c" // ["A","B","C"]
redis> RPUSH list "D" "E" // ["A","B","C","D","E"]
redis> LPOP list "A" // ["B", "C","D","E"]
redis> LPOP list "B" // ["c","D","E"]
redis> RPUSH list "F" "G" // ["C","D","E","F","G" ]
文件重写的原理
  1. redis调用fork函数,产生一个子进程。
  2. 子进程把新的AOF文件写到一个临时文件里。
  3. 父进程持续把新的变动写到内存里的缓冲区(buffer),同时也会把这些新的变动写到旧的AOF文件里,这样即使重写失败也能保证数据的安全。
  4. AOF文件重写并不需要对现有的AOF文件进行任何读取、分析或者写入操作,直接从数据库中读取键list的值,然后用一条终态命令来代替保存在AOF文件中的多条命令
  5. 当子进程完成文件的重写后,父进程会获得一个信号,然后把内存里的缓冲区内容追加到子进程生成的新AOF文件里。
设置日志重写条件

我们可以设置日志重写的条件。下面的实例表示当AOF文件的体积大于64 MB,并且AOF文件的体积比上一次重写之后的体积大了至少一倍(100%)的时候,Redis将执行日志重写操作。

arduino 复制代码
auto aof-rewrite percentage 100
auto-aof-rewrite min size 64MB

Redis会记住自从上一次重写后AOF文件的大小。要禁用自动的日志重写功能,可以把百分比设置为0。

arduino 复制代码
auto-aof-rewrite percentage 0

AOF重写机制为什么能缩小日志文件

重写机制有"多变⼀"功能,将旧⽇志中的多条指令,在重写后就变成了⼀条指令。

如果Redis使用主进程重写AOF,会造成服务器无法处理请求,所以Redis决定将AOF重写程序放到子进程里执行,这样做可以同时达到两个目的:

  • 子进程进行AOF重写期间,服务器进程(父进程)可以继续处理命令请求。
  • 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性。
在重写中有数据写入怎么办

使用子进程也有一个问题需要解决,因为子进程在进行AOF重写期间,服务器进程还需要继续处理命令请求,而新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的AOF文件所保存的数据库状态不一致。

为了解决这种数据不一致问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区。AOF 重写⼀个新⽂件,如果重写失败的话,直接删除这个⽂件就好了,不会对原先的 AOF ⽂件产⽣影响。等重写完成之后,直接替换旧⽂件即可。

在子进程执行AOF重写期间,服务器进程需要执行以下三个工作:

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

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

  1. 将AOF重写缓冲区中的所有内容写入到新AOF文件中,这时新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致。
  2. 对新的AOF文件进行改名,原子的(atomic)覆盖现有的AOF文件,完成新旧两个AOF文件的替换。

AOF后台重写过程中,只有信号处理函数执行时会对服务器进程(父进程)造成阻塞,在其他时候,AOF后台重写都不会阻塞父进程,这将AOF重写对服务器性能造成的影响降到了最低。

AOF案例演示

开启AOF日志
日志路径

redis7的路径是dir+appenddirname:/opt/redis-7.0.0/myredis/dumpfiles/appendonlydir/appendonly.aof

AOF文件方案

MP-AOF实现方案概述 顾名思义,MP-AOF就是将原来的单个AOF文件拆分成多个AOF文件。在MP-AOF中,我们将AOF分为三种类型分别为:

  1. BASE: 表示基础AOF,它一般由子进程通过重写产生,该文件最多只有一个。
  2. INCR: 表示增量AOF,一般会在AOFRW开始执行时被创建,该文件可能存在多个。
  3. HISTORY: 表示历史AOF,它由BASE和INCR AOF变化而来,每次AOFRW成功完成时本次AOFRW之前对应的BASE和INCR AOF都将变为HISTORY,HISTORY类型的AOF会被Redis自动删除。我们引入了一个manifest(清单)文件来跟踪、管理这些AOF。同时,为了便于AOF备份为了管理这些AOF文件和拷贝,我们将所有的AOF文件和manifest文件放入一个单独的文件目录中,目录名由appenddirname配置(Redis 7.0新增配置项)决定。
AOF正常恢复
ruby 复制代码
##进入redis
redis-cli -a 123456
##设置key-value数据
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> set k3 v3
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
​

进入/opt/redis-7.0.0/myredis/dumpfiles/appendonlydir,可以看到如下图所示,有三个文件。

  1. 重启redis,重新加载数据,恢复数据
bash 复制代码
##备份aof文件夹
cp -r appendonlydir/ appendonlydir.bak
##清空内存数据
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> keys *
(empty array)
##删除rdb文件
cd /opt/redis-7.0.0/myredis/dumpfiles
rm -rf dump6379.rdb
##关闭redis
127.0.0.1:6379> SHUTDOWN
not connected> QUIT
##进入配置文件文件夹
cd /opt/redis-7.0.0/myredis/dumpfiles
rm -rf dump6379.rdb
rm -rf appendonlydir
mv appendonlydir.bak/ appendonlydir
##启动redis
redis-server redis.conf
##检查是否恢复
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
3) "k3"
​
  1. AOF在异常下的恢复
ruby 复制代码
##打乱aof的文件,模拟网络闪断,文件写error,如下图所示
[root@bogon appendonlydir]# vim appendonly.aof.1.incr.aof
##关闭redis
127.0.0.1:6379> SHUTDOWN
shell 复制代码
##启动redis,发现启动失败
redis-server redis.conf
bash 复制代码
##执行异常修复命令
cd /opt/redis-7.0.0/myredis/dumpfiles/appendonlydir
redis-check-aof --fix appendonly.aof.1.incr.aof
bash 复制代码
##验证是否修复数据
cat appendonly.aof.1.incr.aof
ruby 复制代码
##启动redis服务
redis-server redis.conf
##连接redis
redis-cli -a 123456
##查看数据是否恢复
127.0.0.1:6379> keys *
1) "k2"
2) "k3"
3) "k1"

Redis 4.0 混合⽇志模型

重启 Redis 时,我们很少使⽤ rdb 来恢复内存状态,因为会丢失⼤量数据。我们通常使⽤ AOF ⽇志重放,但是重放 AOF ⽇志性能相对 rdb 来说要慢很多,这样在 Redis 实例很⼤的情况下,启动需要花费很⻓的时间。

Redis 4.0 为了解决这个问题,带来了⼀个新的持久化选项------混合持久化。将 rdb ⽂件的内容和增量的 AOF ⽇志⽂件存在⼀起。这⾥的 AOF ⽇志不再是全量的⽇志,⽽是⾃持久化开始到持久化结束的这段时间发⽣的增量 AOF ⽇志,通常这部分 AOF ⽇志很⼩。

于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF ⽇志就可以完全替代之前的 AOF 全量⽂件重放,重启效率因此⼤幅得到提升。

所以 RDB 内存快照以稍微慢⼀点的频率执⾏,在两次 RDB 快照期间使⽤ AOF ⽇志记录期间发⽣的所有"写"操作。这样快照就不⽤频繁的执⾏,同时由于 AOF 只需要记录两次快照之间发⽣的"写"指令,不需要记录所有的操作,避免出现⽂件过⼤的情况。

相关推荐
Deepincode22 分钟前
Redis源码探究系列—跳表(skiplist)源码实现详解
redis
专注API从业者1 小时前
Open Claw 京东商品监控选品实战:一键抓取、实时监控、高效选品
java·服务器·数据库
摇滚侠1 小时前
DBeaver 导入数据库 导入 SQL 文件 MySQL 备份恢复
java·数据库·mysql
keep one's resolveY1 小时前
SpringBoot实现重试机制的四种方案
java·spring boot·后端
天空属于哈夫克32 小时前
企业微信API常见的错误和解决方案
java·数据库·企业微信
摇滚侠3 小时前
VMvare 虚拟机 Oracle19c 安装步骤,远程连接 Oracle19c,百度网盘安装包
java·oracle
梁萌3 小时前
idea报错找不到XX包的解决方法
java·intellij-idea·启动报错·缺少包
Agent产品评测局3 小时前
生产排期与MES/ERP系统打通,实操方法详解 —— 2026企业级智能体自动化选型与实战指南
java·运维·人工智能·ai·chatgpt·自动化
阿丰资源3 小时前
基于Spring Boot的电影城管理系统(直接运行)
java·spring boot·后端
呱牛do it3 小时前
企业级门户网站设计与实现:基于SpringBoot + Vue3的全栈解决方案(Day 8)
java