Redis实现-持久化

Redis实现-持久化

一、引言

​ Redis 作为一款高性能的内存数据库,其数据主要存储在内存中,从而提供了极快的读写速度。然而,内存是易失性存储,一旦服务器进程退出或宕机,内存中的数据就会全部丢失。

​ 为了解决这一问题,Redis 提供了持久化功能,将内存中的数据保存到磁盘中,以便在重启后恢复数据。Redis 支持两种持久化方式:RDB持久化和 AOF持久化。本文将深入剖析这两种持久化机制的底层原理、实现细节、优缺点以及在实际生产环境中的选择策略。

二、RDB(Redis Database)持久化

​ RDB 持久化通过创建数据库的快照(snapshot)来保存数据。快照是一个经过压缩的二进制文件,默认名为 dump.rdb,它包含了某个时间点上 Redis 服务器中的所有数据。当服务器重启时,可以通过加载 RDB 文件来还原数据库状态。

2.1 RDB文件的创建与载入

2.1.1 RDB文件的创建

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

1 SAVE命令

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

1
2
redis> SAVE //等待直到RDB文件创建完毕
OK

其伪代码如下:

1
2
3
def SAVE():
#创建RDB文件
rdbSave()

2 BGSAVE命令

BGSAVE命令会派生出一个子进程, 然后由子进程负责创建RDB文件, 服务器进程(父进程) 继续处理命令请求:

1
2
redis> BGSAVE //派生子进程, 并由子进程创建RDB文件
Background saving started

其伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# BGSAVE实现命令:
def BGSAVE():
#创建子进程
pid = fork()
if pid == 0:
#子进程负责创建RDB文件
rdbSave()
#完成之后向父进程发送信号
signal_parent()
elif pid > 0:
#父进程继续处理命令请求, 并通过轮询等待子进程的信号
handle_request_and_wait_signal()
else:
#处理出错情况
handle_fork_error()

BGSAVE 执行期间,服务器对 SAVEBGSAVEBGREWRITEAOF 三个命令的处理有特殊规则:

  • SAVE:客户端发送的 SAVE 命令会被服务器拒绝,避免父子进程同时执行 rdbSave 导致竞争条件。
  • BGSAVE:同时执行两个 BGSAVE 也会产生竞争,因此也会被拒绝。
  • BGREWRITEAOF:如果 BGSAVE 正在执行,BGREWRITEAOF 会被延迟到 BGSAVE 完成后执行;反之,如果 BGREWRITEAOF 正在执行,BGSAVE 会被拒绝。这是为了避免两个子进程同时进行大量磁盘写入,影响性能。

2.1. RDB文件的载入

​ RDB文件的载入工作是在服务器启动时自动执行的, 所以Redis并没有专门用于载入RDB文件的命令, **只要Redis服务器在启动时检测到RDB文件存在, 它就会自动载入RDB文件。 **服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。

​ 以下是Redis服务器启动时打印的日志记录, 其中第二条日志DB loaded from disk:…就是服务器在成功载入RDB文件之后打印的:

1
2
3
4
$ redis-server
[7379] 30 Aug 21:07:01.270 * Server started, Redis version 2.9.11
[7379] 30 Aug 21:07:01.289 * DB loaded from disk: 0.018 seconds
[7379] 30 Aug 21:07:01.289 * The server is now ready to accept connections on port 6379

注意:

因为AOF文件的更新频率通常比RDB文件的更新频率高, 所以:

  • 如果服务器开启了AOF持久化功能, 那么服务器会优先使用AOF文件来还原数据库状态。

  • 只有在AOF持久化功能处于关闭状态时, 服务器才会使用RDB文件来还原数据库状态

​ 载入RDB文件的实际工作由rdb.c/rdbLoad函数完成,这个函数和rdbSave函数之间的关系可以用下图表示。

载入RDB流程

2.2 自动间隔性保存

BGSAVE命令可以在不阻塞服务器进程的情况下执行,所以Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。

用户可以通过save选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行BGSAVE命令。

举例,如果我们向服务器提供以下配置:

1
2
3
save 900 1 
save 300 10
save 60 10000

那么只要满足以下三个条件中的任意一个,BGSAVE命令就会被执行:

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

服务器自动执行BGSAVE命令时打印出来的日志:

1
2
3
4
5
[5085] 03 Sep 17:09:49.463 * 10000 changes in 60 seconds. Saving...
[5085] 03 Sep 17:09:49.463 * Background saving started by pid 5189
[5189] 03 Sep 17:09:49.522 * DB saved on disk
[5189] 03 Sep 17:09:49.522 * RDB: 0 MB of memory used by copy-on-write
[5085] 03 Sep 17:09:49.563 * Background saving terminated with success

2.1.1 服务器状态中的保存条件

服务器状态 redisServer 结构中的 saveparams 属性是一个数组,保存了所有由 save 选项设置的保存条件。每个条件由 saveparam 结构表示:

1
2
3
4
struct saveparam {
time_t seconds; // 秒数
int changes; // 修改次数
};

每个saveparam结构都保存了一个save选项设置的保存条件:

1
2
3
4
5
6
struct saveparam {
//秒数
time_t seconds;
//修改数
int changes;
};

默认情况下,服务器状态中的saveparams数组将会是下图所示的样子。

默认配置下保存条件

2.1.2 dirty计数器和lastsave属性

服务器还维护了两个重要的属性:

  • dirty 计数器:记录了上次成功执行 SAVEBGSAVE 以来,服务器对数据库进行了多少次修改(写入、删除、更新等)。每次成功执行一个写命令,dirty 会增加相应的次数。
  • lastsave 属性:是一个 UNIX 时间戳,记录了上次成功执行 SAVEBGSAVE 的时间。
1
2
3
4
5
6
7
8
struct redisServer {
// ...
//修改计数器
long long dirty;
//上一次执行保存的时间
time_t lastsave;
// ...
};

2.1.3 检查保存条件是否满足

Redis 的服务器周期性函数 serverCron 默认每隔 100 毫秒执行一次。它的一项工作就是遍历 saveparams 数组,检查保存条件是否满足。

伪代码如下:

1
2
3
4
5
def serverCron():
for saveparam in server.saveparams:
save_interval = now() - server.lastsave
if server.dirty >= saveparam.changes and save_interval > saveparam.seconds:
BGSAVE()

一旦某个条件满足,服务器就会执行 BGSAVE 命令,创建 RDB 文件,并重置 dirty 计数器和更新 lastsave 时间。

2.3 RDB 文件结构详解

RDB 文件是一个经过压缩的二进制文件,由多个部分组成。理解其结构有助于深入理解 Redis 的数据持久化方式,也有助于进行故障排查和数据恢复。一个完整的 RDB 文件包含以下部分:

1
2
3
+-------+-------------+------------+-----+-----------+
| REDIS | db_version | databases | EOF | check_sum |
+-------+-------------+------------+-----+-----------+

2.3.1 REDIS

长度为 5 字节,保存着 "REDIS" 五个字符。载入时通过检查这五个字符可以快速判断文件是否为合法的 RDB 文件。

2.3.2 db_version

长度为 4 字节,是一个字符串表示的整数,记录 RDB 文件的版本号,例如 "0006" 表示版本为 6。Redis 目前使用的是第 6 版 RDB 文件。

2.3.3 databases

这部分保存了零个或多个数据库的数据。如果服务器状态为空,则该部分长度为 0。每个非空数据库在 RDB 文件中由三部分组成:

1
2
3
+----------+------------+-----------------+
| SELECTDB | db_number | key_value_pairs |
+----------+------------+-----------------+
  • SELECTDB:1 字节常量,告知接下来要读入一个数据库号码。
  • db_number:保存数据库号码,长度根据号码大小可能是 1 字节、2 字节或 5 字节。
  • key_value_pairs:保存数据库中的所有键值对数据,可能包含过期时间。

2.3.4 key_value_pairs

每个键值对在 RDB 文件中根据是否带过期时间,有不同的结构:

  • 不带过期时间TYPE + key + value
  • 带过期时间EXPIRETIME_MS + ms + TYPE + key + value

其中:

  • TYPE:1 字节,记录值的类型,可以是以下常量之一:REDIS_RDB_TYPE_STRINGREDIS_RDB_TYPE_LISTREDIS_RDB_TYPE_SETREDIS_RDB_TYPE_ZSETREDIS_RDB_TYPE_HASHREDIS_RDB_TYPE_LIST_ZIPLISTREDIS_RDB_TYPE_SET_INTSETREDIS_RDB_TYPE_ZSET_ZIPLISTREDIS_RDB_TYPE_HASH_ZIPLIST 等。
  • key:字符串对象,保存方式与字符串值类似。
  • value:根据 TYPE 的不同,有不同的编码方式。
  • EXPIRETIME_MS:1 字节常量,表示接下来是毫秒精度的过期时间。
  • ms:8 字节带符号整数,表示过期时间的 UNIX 时间戳(毫秒)。

2.3.5 value 的编码方式

不同类型的值对象在 RDB 文件中有不同的存储结构:

2.3.5.1 字符串对象(REDIS_RDB_TYPE_STRING)

字符串对象的编码可以是 REDIS_ENCODING_INTREDIS_ENCODING_RAW

  • INT 编码:如果保存的是整数,则使用图 10-20 所示的结构:ENCODING + integerENCODING 可以是 REDIS_RDB_ENC_INT8REDIS_RDB_ENC_INT16REDIS_RDB_ENC_INT32,分别对应 8、16、32 位整数。
  • RAW 编码:保存字符串值。如果字符串长度 ≤ 20 字节且未压缩,则直接保存 len + string;如果长度 > 20 字节且开启了压缩,则保存 REDIS_RDB_ENC_LZF + compressed_len + origin_len + compressed_string,使用 LZF 算法压缩。
2.3.5.2 列表对象(REDIS_RDB_TYPE_LIST)

对应 REDIS_ENCODING_LINKEDLIST 编码的列表,存储结构为:list_length + 连续 N 个 item,每个 item 按字符串对象的方式保存。

2.3.5.3 集合对象(REDIS_RDB_TYPE_SET)

对应 REDIS_ENCODING_HT 编码的集合,存储结构为:set_size + 连续 N 个 elem,每个 elem 按字符串对象保存。

2.3.5.4 哈希表对象(REDIS_RDB_TYPE_HASH)

对应 REDIS_ENCODING_HT 编码的哈希表,存储结构为:hash_size + 连续 N 个 key_value_pair,每个键值对按 key + value 顺序保存(均为字符串对象)。

2.3.5.5 有序集合对象(REDIS_RDB_TYPE_ZSET)

对应 REDIS_ENCODING_SKIPLIST 编码的有序集合,存储结构为:sorted_set_size + 连续 N 个 element,每个 elementmember + score 顺序保存,其中 score 会被转换为字符串对象保存。

2.3.5.6 其他编码(INTSET、ZIPLIST)

对于 REDIS_ENCODING_INTSET 编码的集合,以及 REDIS_ENCODING_ZIPLIST 编码的列表、哈希表、有序集合,Redis 会先将这些数据结构转换为字符串对象(即保存其二进制内容),然后再将该字符串对象写入 RDB 文件。载入时,根据 TYPE 识别,再将字符串对象转换回原来的数据结构。

2.3.6 EOF 常量

长度为 1 字节,值为 0xFF,标志着 RDB 文件正文内容的结束。

2.3.7 check_sum

长度为 8 字节,是一个无符号整数,保存着前面所有部分的校验和。载入时会重新计算并与该值比较,以检查文件是否损坏。

2.4 RDB 文件分析实例

我们可以使用 od 命令来分析 RDB 文件的十六进制和 ASCII 内容。以下是一些示例:

2.4.1 空数据库的 RDB 文件

执行 FLUSHALLSAVE 后,生成的 RDB 文件内容:

1
2
3
$ od -c dump.rdb
0000000 R E D I S 0 0 0 6 377 334 263 C 3 60 Z
0000020 334 3 6 2 V

解析:

  • REDIS0006:文件头和版本号。
  • 377:EOF 常量(八进制 377 即十进制 255)。
  • 后续 8 字节:校验和。

2.4.2 包含一个字符串键的 RDB 文件

执行 SET msg "hello" 后:

1
2
3
$ od -c dump.rdb
0000000 R E D I S 0 0 0 6 376 \0 \0 003 M S G
0000020 005 h e l l o 377 207 z = 304 f T L 343

解析:

  • 376:SELECTDB 常量。
  • \0:数据库 0。
  • \0:TYPE 为 REDIS_RDB_TYPE_STRING(0)。
  • 003:键长度为 3。
  • MSG:键名。
  • 005:值长度为 5。
  • hello:值内容。
  • 377:EOF。

2.4.3 包含一个集合键的 RDB 文件

执行 SADD lang C Java Ruby 后:

1
2
3
4
$ od -c dump.rdb
0000000 R E D I S 0 0 0 6 376 \0 002 004 L A N
0000020 G 003 004 R U B Y 004 J A V A 001 C 377 202
0000040 312 r 352 346 305 023

解析:

  • 002:TYPE 为 REDIS_RDB_TYPE_SET(2)。
  • 004:键长度为 4。
  • LANG:键名。
  • 003:集合大小为 3。
  • 三个元素依次为:004 RUBY004 JAVA001 C
  • 377:EOF。

2.5 RDB 的优缺点

2.5.1 优点

  • 紧凑:RDB 文件是经过压缩的二进制文件,体积小,适合备份和灾难恢复。
  • 恢复速度快:载入 RDB 文件时直接解析数据并载入内存,比 AOF 重放命令要快得多。
  • 性能影响小:通过 BGSAVE 派生子进程进行持久化,主进程几乎不受阻塞(仅 fork 瞬间有影响)。

2.5.2 缺点

  • 数据安全性较低:RDB 是定期保存的,如果服务器宕机,可能会丢失最后一次保存之后的数据。
  • fork 开销:当数据量很大时,fork 子进程可能会消耗较多时间和内存(写时复制技术虽然节省内存,但会导致子进程与父进程共享内存页,如果父进程在此期间修改数据,会产生页复制,增加内存占用)。

三、AOF(Append Only File)持久化

​ AOF持久化通过记录服务器执行的所有写命令来实现数据持久化。

​ 每当服务器执行一个写命令,它就会将该命令以 Redis 协议格式追加到 AOF 文件的末尾。当服务器重启时,通过重新执行 AOF 文件中的所有命令,就可以还原数据库状态。

3.1 AOF文件的创建和载入

3.1.1 AOF文件的写入

如果我们对空白的数据库执行以下写命令,那么数据库中将包含三个键值对:

1
2
3
4
5
6
redis> SET msg "hello"
OK
redis> SADD fruits "apple" "banana" "cherry"
(integer) 3
redis> RPUSH numbers 128 256 512
(integer) 3

AOF持久化保存数据库状态的方法则是将服务器执行的SET、SADD、RPUSH三个命令保存到AOF文件中。

被写入AOF文件的所有命令都是以Redis的命令请求协议格式保存的,因为Redis的命令请求协议是纯文本格式,所以我们可以直接打开一个AOF文件,观察里面的内容。

对于之前执行的三个写命令来说,服务器将产生包含以下内容的AOF文件:

1
2
3
4
*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n
*3\r\n$3\r\nSET\r\n$3\r\nmsg\r\n$5\r\nhello\r\n
*5\r\n$4\r\nSADD\r\n$6\r\nfruits\r\n$5\r\napple\r\n$6\r\nbanana\r\n$6\r\ncherry\r\
*5\r\n$5\r\nRPUSH\r\n$7\r\nnumbers\r\n$3\r\n128\r\n$3\r\n256\r\n$3\r\n512\r\n

在这个AOF文件里面,除了用于指定数据库的SELECT命令是服务器自动添加的之外,其他都是我们之前通过客户端发送的命令。

3.1.2 AOF文件的载入

​ 服务器在启动时,可以通过载入和执行AOF文件中保存的命令来还原服务器关闭之前的数据库状态,以下就是服务器载入AOF文件并还原数据库状态时打印的日志:

1
2
3
[8321] 05 Sep 11:58:50.448 # Server started, Redisversion 2.9.11
[8321] 05 Sep 11:58:50.449 * DB loaded from append only file: 0.000 seconds
[8321] 05 Sep 11:58:50.449 * The server is now ready to accept connections on port 6379

服务器启动时,如果开启了 AOF 持久化,会优先使用 AOF 文件还原数据。还原过程如下:

  1. 创建一个不带网络连接的伪客户端(fake client),因为 Redis 命令必须在客户端上下文中执行。
  2. 从 AOF 文件中分析并读取出一条写命令。
  3. 使用伪客户端执行该命令。
  4. 重复步骤 2-3 直到 AOF 文件中的所有命令都被处理完毕。

伪客户端执行命令的效果与普通客户端完全一样,因此可以正确还原数据库状态。

image-20260216224713133

3.2 AOF 持久化的实现

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

3.2.1 命令追加

服务器在执行完一个写命令后,会将该命令以协议格式追加到服务器状态的 aof_buf 缓冲区的末尾。

aof_buf 是一个 SDS 字符串,定义在 redisServer 结构中:

1
2
3
4
struct redisServer {
sds aof_buf; // AOF缓冲区
// ...
};

例如,执行 SET KEY VALUE 后,会向 aof_buf 追加 "*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n"

1
2
redis> SET KEY VALUE 
OK

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

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

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

1
2
redis> RPUSH NUMBERS ONE TWO THREE 
(integer) 3

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

1
*5\r\n$5\r\nRPUSH\r\n$7\r\nNUMBERS\r\n$3\r\nONE\r\n$3\r\nTWO\r\n$5\r\nTHREE\r\n

3.2.2 写入与同步

​ Redis的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数。

​ 因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到aof_buf缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面,这个过程可以用以下伪代码表示:

1
2
3
4
5
6
7
8
9
def eventLoop():
while True:
#处理文件事件,接收命令请求以及发送命令回复
#处理命令请求时可能会有新内容被追加到 aof_buf缓冲区中
processFileEvents()
#处理时间事件
processTimeEvents()
#考虑是否要将 aof_buf的内容写入和保存到 AOF文件里面
flushAppendOnlyFile()

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

appendfsync 选项 行为
always aof_buf 所有内容写入并同步到 AOF 文件(最安全,但最慢)。
everysec(默认) aof_buf 所有内容写入到 AOF 文件,如果上次同步时间距离现在超过 1 秒,则再同步(由子线程负责同步,性能较好,最多丢 1 秒数据)。
no aof_buf 所有内容写入到 AOF 文件,但不同步,何时同步由操作系统决定(最快,但可能丢失大量数据)。

写入与同步的区别:写入(write)只是将数据从用户缓冲区复制到内核缓冲区,同步(fsync)才将内核缓冲区数据真正写入磁盘。always 选项在每个事件循环都执行同步,保证数据不丢失;everysec 每秒同步一次;no 则完全依赖操作系统同步(通常 30 秒左右)。

3.3 AOF 重写

​ 随着服务器运行,AOF文件中的内容会越来越多,文件的体积也会越来越大,如果不加以控制的话,体积过大的AOF文件很可能对Redis服务器、甚至整个宿主计算机造成影响,并且AOF文件的体积越大,使用AOF文件来进行数据还原所需的时间就越多。

​ 因此:Redis提供了AOF文件重写rewrite)功能。通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件

3.3.1 AOF文件重写的实现

​ AOF 重写并不依赖于现有的 AOF 文件,而是通过读取服务器当前的数据库状态来实现。

​ 例如,对于一个列表键,原本可能有多条 RPUSHLPOP 等命令,但重写时只需一条 RPUSH 命令包含当前列表的所有元素即可。

1
2
3
4
5
6
7
8
9
10
11
12
redis> RPUSH list "A" "B"                 //  ["A", "B"]
(integer) 2
redis> RPUSH list "C" // ["A", "B", "C"]
(integer) 3
redis> RPUSH list "D" "E" // ["A", "B", "C", "D", "E"]
(integer) 5
redis> LPOP list // ["B", "C", "D", "E"]
"A"
redis> LPOP list // ["C", "D", "E"]
"B"
redis> RPUSH list "F" "G" // ["C", "D", "E", "F", "G"]
(integer) 5

​ 那么服务器为了保存当前list键的状态,必须在AOF文件中写入六条命令。

​ 对AOF文件重写,直接从数据库中读取键list的值,然后用一条RPUSH list”C””D””E””F””G”命令来代替保存在AOF文件中的六条命令,这样就可以将保存list键所需的命令从六条减少为一条了。

整个重写过程可以用以下伪代码表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def aof_rewrite(new_aof_file_name):
#创建新 AOF件
f = create_file(new_aof_file_name)
#遍历数据库
for db in redisServer.db:
#略空数据库
if db.is_empty(): continue
#写入SELECT命令,指定数据库号码
f.write_command("SELECT" + db.id)
#遍历数据库中的所有键
for key in db:
#忽略已过期的键
if key.is_expired(): continue
#根据键的类型对键进行重写
if key.type == String:
rewrite_string(key)
elif key.type == List:
rewrite_list(key)
elif key.type == Hash:
rewrite_hash(key)
elif key.type == Set:
rewrite_set(key)
elif key.type == SortedSet:
rewrite_sorted_set(key)
#如果键带有过期时间,那么过期时间也要被重写
if key.have_expire_time():

#写入完毕,关闭文件
f.close()



def rewrite_string(key):
#用GET命令获取字符串键的值
value = GET(key)
#用SET命令重写字符串键
f.write_command(SET, key, value)

def rewrite_list(key):
#使用LRANGE命令获取列表键包含的所有元素
item1, item2, ..., itemN = LRANGE(key, 0, -1)
#使用RPUSH命令重写列表键
f.write_command(RPUSH, key, item1, item2, ..., itemN)

def rewrite_hash(key):
#使用HGETALL命令获取哈希键包含的所有键值对
field1, value1, field2, value2, ..., fieldN, valueN = HGETALL(key)
#使用HMSET命令重写哈希键
f.write_command(HMSET, key, field1, value1, field2, value2, ..., fieldN, valueN)

def rewrite_set(key);
#使用SMEMBERS命令获取集合键包含的所有元素
elem1, elem2, ..., elemN = SMEMBERS(key)
#用SADD命令重写集合键
f.write_command(SADD, key, elem1, elem2, ..., elemN)


def rewrite_sorted_set(key):
#使用ZRANGE命令获取有序集合键包含的所有元素
member1, score1, member2, score2, ..., memberN, scoreN = ZRANGE(key, 0, -1, "WITHSCORES")
#用ZADD命令重写有序集合键
f.write_command(ZADD, key, score1, member1, score2, member2, ..., scoreN, memberN)

def rewrite_expire_time(key):
#获取毫秒精度的键过期时间戳
timestamp = get_expire_time_in_unixstamp(key)
#使用PEXPIREAT命令重写键的过期时间
f.write_command(PEXPIREAT, key, timestamp)

3.3.2 AOF后台重写

​ 类似于 BGSAVE,AOF 重写也通过子进程在后台执行,避免阻塞主进程。然而,子进程重写期间,主进程可能继续修改数据库,导致重写后的 AOF 文件与当前数据库状态不一致。为了解决这个问题,Redis 引入了 AOF 重写缓冲区

AOF 重写缓冲区 在创建子进程后开始使用。每当主进程执行一个写命令,它会同时将命令追加到 AOF 缓冲区(用于写入现有 AOF 文件)和 AOF 重写缓冲区(用于后续写入新 AOF 文件)。当子进程完成重写后,会向父进程发送信号,父进程在信号处理函数中执行以下操作:

  1. 将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件的末尾,使新文件包含重写期间产生的所有写命令,从而与当前数据库状态一致。
  2. 对新 AOF 文件进行改名,原子地覆盖现有的 AOF 文件。

整个过程中,只有信号处理函数执行时会阻塞父进程(通常很短),从而将对性能的影响降到最低。

流程图示意

1
2
3
4
5
6
7
8
9
10
11
12
13
主进程                        子进程
| |
|--- 创建子进程 ------------> 开始重写
| |
|--- 处理命令,同时写入 | 继续重写
| AOF缓冲区和重写缓冲区 |
| |
| |--- 重写完成
|<--- 收到信号 ---------------|
|--- 信号处理: |
| 将重写缓冲区内容写入新文件|
| 原子替换旧文件 |
|--- 继续处理命令 |

3.4 AOF 的优缺点

3.4.1 优点

  • 数据安全性高:即使使用默认的 everysec 选项,最多丢失 1 秒数据;使用 always 可做到不丢失数据。
  • 易于理解和修复:AOF 文件是纯文本协议格式,可以手动修改,修复错误命令。
  • 自动重写:AOF 文件体积会随着时间增大,但重写机制可以自动压缩。

3.4.2 缺点

  • 体积较大:AOF 文件通常比 RDB 文件大,因为保存的是命令而非数据快照。
  • 恢复速度慢:重放所有命令比直接加载 RDB 快照慢得多。
  • 性能影响always 选项会显著降低写入性能;everysec 在极端情况下也可能造成延迟。

四、RDB 与 AOF 的对比与选择

4.1 功能对比

特性 RDB AOF
数据粒度 某个时间点的全量数据快照 所有写命令的历史记录
文件体积 较小(压缩二进制) 较大(纯文本协议,可能包含冗余命令)
恢复速度
数据安全性 可能丢失最后一次保存后的数据 可配置最多丢失 1 秒或 0 秒数据
对性能影响 BGSAVE 时 fork 子进程,主进程仅 fork 时有影响 写入频率高,同步策略影响写入性能
适用场景 备份、容灾、冷数据恢复 需要高数据安全性的场景,如金融交易

4.2 混合使用

​ Redis 允许同时开启 RDB 和 AOF 持久化。在这种情况下,服务器重启时会优先使用 AOF 文件还原数据,因为 AOF 通常包含更完整的数据。同时,RDB 文件可用于快速备份和灾难恢复。

4.3 配置建议

  • 如果对数据安全性要求不是极高,且希望快速恢复,可以只使用 RDB,并合理设置 save 条件。
  • 如果对数据安全性要求高,建议开启 AOF,并使用 appendfsync everysec 平衡性能与安全。
  • 对于关键业务,可以同时开启两者,并定期备份 RDB 文件。

4.4 最佳实践

  • 定期执行 BGSAVE:即使开启了 AOF,也建议定期执行 BGSAVE 生成 RDB 快照,用于快速恢复和备份。
  • 监控 AOF 文件大小:关注 AOF 重写是否正常进行,避免文件过大占用磁盘。
  • 合理配置重写阈值:通过 auto-aof-rewrite-percentageauto-aof-rewrite-min-size 控制自动重写的触发条件。

五、总结

Redis 的持久化机制是其作为可靠数据库的重要基石。

  1. RDB 提供了紧凑的快照备份和快速恢复能力,适合用于数据备份和灾难恢复;

    1. AOF 则通过记录写命令提供了更高的数据安全性,适合对数据完整性要求较高的场景。

在实际产环境中,通常需要根据业务需求权衡数据安全性和性能,选择合适的持久化策略,甚至可以两者结合,以达到最佳效果。


Redis实现-持久化
https://johnjoyjzw.github.io/2023/03/08/Redis实现-持久化/
Author
JiangZW
Posted on
March 8, 2023
Licensed under