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 | |
其伪代码如下:
1 | |
2 BGSAVE命令
BGSAVE命令会派生出一个子进程, 然后由子进程负责创建RDB文件, 服务器进程(父进程) 继续处理命令请求:
1 | |
其伪代码如下:
1 | |
在 BGSAVE 执行期间,服务器对 SAVE、BGSAVE、BGREWRITEAOF 三个命令的处理有特殊规则:
- 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 | |
注意:
因为AOF文件的更新频率通常比RDB文件的更新频率高, 所以:
如果服务器开启了AOF持久化功能, 那么服务器会优先使用AOF文件来还原数据库状态。
只有在AOF持久化功能处于关闭状态时, 服务器才会使用RDB文件来还原数据库状态
载入RDB文件的实际工作由rdb.c/rdbLoad函数完成,这个函数和rdbSave函数之间的关系可以用下图表示。

2.2 自动间隔性保存
BGSAVE命令可以在不阻塞服务器进程的情况下执行,所以Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。
用户可以通过save选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行BGSAVE命令。
举例,如果我们向服务器提供以下配置:
1 | |
那么只要满足以下三个条件中的任意一个,BGSAVE命令就会被执行:
- 服务器在900秒之内,对数据库进行了至少1次修改。
- 服务器在300秒之内,对数据库进行了至少10次修改。
- 服务器在60秒之内,对数据库进行了至少10000次修改。
服务器自动执行BGSAVE命令时打印出来的日志:
1 | |
2.1.1 服务器状态中的保存条件
服务器状态 redisServer 结构中的 saveparams 属性是一个数组,保存了所有由 save 选项设置的保存条件。每个条件由 saveparam 结构表示:
1 | |
每个saveparam结构都保存了一个save选项设置的保存条件:
1 | |
默认情况下,服务器状态中的saveparams数组将会是下图所示的样子。

2.1.2 dirty计数器和lastsave属性
服务器还维护了两个重要的属性:
- dirty 计数器:记录了上次成功执行
SAVE或BGSAVE以来,服务器对数据库进行了多少次修改(写入、删除、更新等)。每次成功执行一个写命令,dirty会增加相应的次数。 - lastsave 属性:是一个 UNIX 时间戳,记录了上次成功执行
SAVE或BGSAVE的时间。
1 | |
2.1.3 检查保存条件是否满足
Redis 的服务器周期性函数 serverCron 默认每隔 100 毫秒执行一次。它的一项工作就是遍历 saveparams 数组,检查保存条件是否满足。
伪代码如下:
1 | |
一旦某个条件满足,服务器就会执行 BGSAVE 命令,创建 RDB 文件,并重置 dirty 计数器和更新 lastsave 时间。
2.3 RDB 文件结构详解
RDB 文件是一个经过压缩的二进制文件,由多个部分组成。理解其结构有助于深入理解 Redis 的数据持久化方式,也有助于进行故障排查和数据恢复。一个完整的 RDB 文件包含以下部分:
1 | |
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 | |
- 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_STRING、REDIS_RDB_TYPE_LIST、REDIS_RDB_TYPE_SET、REDIS_RDB_TYPE_ZSET、REDIS_RDB_TYPE_HASH、REDIS_RDB_TYPE_LIST_ZIPLIST、REDIS_RDB_TYPE_SET_INTSET、REDIS_RDB_TYPE_ZSET_ZIPLIST、REDIS_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_INT 或 REDIS_ENCODING_RAW。
- INT 编码:如果保存的是整数,则使用图 10-20 所示的结构:
ENCODING+integer。ENCODING可以是REDIS_RDB_ENC_INT8、REDIS_RDB_ENC_INT16或REDIS_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,每个 element 按 member + 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 文件
执行 FLUSHALL 和 SAVE 后,生成的 RDB 文件内容:
1 | |
解析:
REDIS0006:文件头和版本号。377:EOF 常量(八进制 377 即十进制 255)。- 后续 8 字节:校验和。
2.4.2 包含一个字符串键的 RDB 文件
执行 SET msg "hello" 后:
1 | |
解析:
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 | |
解析:
002:TYPE 为 REDIS_RDB_TYPE_SET(2)。004:键长度为 4。LANG:键名。003:集合大小为 3。- 三个元素依次为:
004 RUBY、004 JAVA、001 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 | |
AOF持久化保存数据库状态的方法则是将服务器执行的SET、SADD、RPUSH三个命令保存到AOF文件中。
被写入AOF文件的所有命令都是以Redis的命令请求协议格式保存的,因为Redis的命令请求协议是纯文本格式,所以我们可以直接打开一个AOF文件,观察里面的内容。
对于之前执行的三个写命令来说,服务器将产生包含以下内容的AOF文件:
1 | |
在这个AOF文件里面,除了用于指定数据库的SELECT命令是服务器自动添加的之外,其他都是我们之前通过客户端发送的命令。
3.1.2 AOF文件的载入
服务器在启动时,可以通过载入和执行AOF文件中保存的命令来还原服务器关闭之前的数据库状态,以下就是服务器载入AOF文件并还原数据库状态时打印的日志:
1 | |
服务器启动时,如果开启了 AOF 持久化,会优先使用 AOF 文件还原数据。还原过程如下:
- 创建一个不带网络连接的伪客户端(fake client),因为 Redis 命令必须在客户端上下文中执行。
- 从 AOF 文件中分析并读取出一条写命令。
- 使用伪客户端执行该命令。
- 重复步骤 2-3 直到 AOF 文件中的所有命令都被处理完毕。
伪客户端执行命令的效果与普通客户端完全一样,因此可以正确还原数据库状态。

3.2 AOF 持久化的实现
AOF 持久化的实现分为三个步骤:命令追加、文件写入、文件同步。
3.2.1 命令追加
服务器在执行完一个写命令后,会将该命令以协议格式追加到服务器状态的 aof_buf 缓冲区的末尾。
aof_buf 是一个 SDS 字符串,定义在 redisServer 结构中:
1 | |
例如,执行 SET KEY VALUE 后,会向 aof_buf 追加 "*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n"。
1 | |
那么服务器在执行这个SET命令之后,会将以下协议内容追加到aof_buf缓冲区的末尾:
1 | |
又例如,如果客户端向服务器发送以下命令:
1 | |
那么服务器在执行这个RPUSH命令之后,会将以下协议内容追加到aof_buf缓冲区的末尾:
1 | |
3.2.2 写入与同步
Redis的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数。
因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到aof_buf缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面,这个过程可以用以下伪代码表示:
1 | |
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 文件,而是通过读取服务器当前的数据库状态来实现。
例如,对于一个列表键,原本可能有多条 RPUSH、LPOP 等命令,但重写时只需一条 RPUSH 命令包含当前列表的所有元素即可。
1 | |
那么服务器为了保存当前list键的状态,必须在AOF文件中写入六条命令。
对AOF文件重写,直接从数据库中读取键list的值,然后用一条RPUSH list”C””D””E””F””G”命令来代替保存在AOF文件中的六条命令,这样就可以将保存list键所需的命令从六条减少为一条了。
整个重写过程可以用以下伪代码表示:
1 | |
3.3.2 AOF后台重写
类似于 BGSAVE,AOF 重写也通过子进程在后台执行,避免阻塞主进程。然而,子进程重写期间,主进程可能继续修改数据库,导致重写后的 AOF 文件与当前数据库状态不一致。为了解决这个问题,Redis 引入了 AOF 重写缓冲区。
AOF 重写缓冲区 在创建子进程后开始使用。每当主进程执行一个写命令,它会同时将命令追加到 AOF 缓冲区(用于写入现有 AOF 文件)和 AOF 重写缓冲区(用于后续写入新 AOF 文件)。当子进程完成重写后,会向父进程发送信号,父进程在信号处理函数中执行以下操作:
- 将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件的末尾,使新文件包含重写期间产生的所有写命令,从而与当前数据库状态一致。
- 对新 AOF 文件进行改名,原子地覆盖现有的 AOF 文件。
整个过程中,只有信号处理函数执行时会阻塞父进程(通常很短),从而将对性能的影响降到最低。
流程图示意:
1 | |
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-percentage和auto-aof-rewrite-min-size控制自动重写的触发条件。
五、总结
Redis 的持久化机制是其作为可靠数据库的重要基石。
RDB 提供了紧凑的快照备份和快速恢复能力,适合用于数据备份和灾难恢复;
- AOF 则通过记录写命令提供了更高的数据安全性,适合对数据完整性要求较高的场景。
在实际产环境中,通常需要根据业务需求权衡数据安全性和性能,选择合适的持久化策略,甚至可以两者结合,以达到最佳效果。