[转帖]Redis 持久化原理和实现

redis,持久,原理,实现 · 浏览次数 : 0

小编点评

```c# if (server.dirty >= sp->changes && server.unixtime-server.lastsave > sp->seconds && (server.unixtime-server.lastbgsave_try > REDIS_BGSAVE_RETRY_DELAY || server.lastbgsave_status == REDIS_OK)) { // 满足自动保存的标准: // 1. 从上次完成 RDB 到现在的数据库修改次数(dirty)已经达到了 save 配置中 changes 的值 // 2. 距上一次完成 RDB 的时间(lastsave)已经达到了 save 配置中 seconds 的值 // 3. 上一次 RDB 已经成功,或者距上一次尝试 RDB 的时间(lastbgsave_try)已经达到了配置的超时时间(REDIS_BGSAVE_RETRY_DELAY) } ```

正文

https://juejin.cn/post/6877763937513766919

 

Redis 所有的数据和状态存储在内存中,为了避免进程退出而导致数据丢失,需要将数据和状态保存到硬盘上。

为了达到这一目的,通常有两种实现方式:

  1. 将 Redis 当作一个状态机,记录每一次的对 Redis 的操作,也就是状态转移。需要恢复时再从初始状态开始,依次重放记录的操作,这样的方式称作逻辑备份
  2. 将 Redis 完整的状态保存下来,待必要时原样恢复,这样的方式称作物理备份

Redis 也实现了这两种持久化方式,分别时 AOF 和 RDB

AOF

AOF 通过保存 Redis 服务器执行的写命令记录数据库状态。

AOF 配置

Redis 源码中的配置文件示例: redis.conf

# AOF 配置示例
# https://github.com/redis/redis/blob/48e24d54b736b162617112ce27ec724b9290592e/redis.conf#L489

# 重要参数:
appendonly yes # 是否开启 AOF,如果开启了 AOF,后续恢复数据库时会优先使用 AOF,跳过 RDB
appendfsync everysec # 持久化判断规则
appendfilename appendonly.aof # AOF 文件位置
复制代码

命令执行完成后才会写入 AOF 日志

AOF 是写后日志,与写前日志(Write Ahead Log, WAL)相反,写入命令执行完成后才会记录到 AOF 日志。这样设计是因为 AOF 记录的是接收到的命令,并且记录时不会进行语法检查(保证性能),使用写后日志有 2 个优点:

  1. 可以保证日志中记录的命令都是正确的
  2. 命令执行后才记录到日志,不会阻塞当前写操作

风险:

  1. 刚执行完命令,还没写入,此时宕机,这个命令和相应的数据有丢失的风险
  2. 避免了当前命令的阻塞,但是可能阻塞下一个命令

AOF 持久化执行步骤

  1. 服务器在执行完命令后,会将命令写入到 struct redisServer 的 sds aof_buf` 缓冲区末尾
  2. Redis 进程每一次事件循环(处理客户端请求的循环)末尾都会调用 void flushAppendOnlyFile 检查时候需要将缓冲区中的命令写入 AOF 文件

AOF 写入条件判断规则

flushAppendOnlyFile 中根据配置文件中的 appendfsync 参数判断是否写入 AOF 文件。将 aof_buf 中的命令写入 AOF 文件分为两个步骤:

  1. 调用 OS 的 write 函数,将 aof_buf 中的命令保存到内存缓冲区
  2. OS 将 内存缓冲区中的写入磁盘

如果只执行了第一步,从 redis 的视角来看,数据已经写入了文件,但实际上并没有写入,如果此时停机,数据仍然会丢失,因此可以使用 OS 提供的 fsync 和 fdatasync 强制将缓冲区中的数据写入磁盘

flushAppendOnlyFile 行为appndfsync 选项
总是将 aof_buf 缓冲区中的内容写入内存缓冲区,并同步到 AOF 文件 always
将 aof_buf 缓冲区中的内容写入内存缓冲区,如果距离上一次同步超过一秒,则同步到 AOF 文件 everysec
只写入到内存缓冲区,由 OS 后续决定何时同步到 AOF 文件 no

AOF 判断过程如下:

void flushAppendOnlyFile(int force) {
    ssize_t nwritten;
    ...
		// 调用 write 写入文件
    nwritten = write(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));
    ...
    // 成功写入后
    server.aof_current_size += nwritten;
    ...
    // 根据 appndfsync 条件判断是否同步到 AOF 文件
    if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
        ...
        // 这里强制执行同步用的是 aof_fsync,是因为 aof_fsync 已经被定义成了 fsync
				// 具体位置在 config.h:https://github.com/redis/redis/blob/48e24d54b736b162617112ce27ec724b9290592e/src/config.h#L89
        aof_fsync(server.aof_fd);
				...
        // 成功后记录下时间,用于下一次同步条件检查
        server.aof_last_fsync = server.unixtime;
    } else if ((server.aof_fsync == AOF_FSYNC_EVERYSEC &&
                server.unixtime > server.aof_last_fsync)) {
        // 在另一个线程中后台执行
        if (!sync_in_progress) aof_background_fsync(server.aof_fd);
        server.aof_last_fsync = server.unixtime;
    }
}
复制代码

AOF 文件载入

  1. Redis 创建一个不带网络连接的伪客户端
  2. 从 AOF 文件中依次读出命令并交给伪客户端执行。这个过程和正常的 Redis 客户端从网络中依次读取命令然后执行效果一致

AOF 重写

由于 AOF 文件是依次记录客户端发来的写入命令,在写入较多的情况下,AOF 文件会快速膨胀,因此需要 AOF 重写精简其中的命令。

AOF 重写的过程中并不会读取原有的 AOF 文件,而是直接根据数据库当前的状态生成一份新的 AOF 文件,类似于 SQL 导出数据时直接生成 INSERT 语句。

对于有多个元素的 key,例如大列表、大集合,简单的将所有元素的写入合并到一条语句中可能会形成一条过大的写入语句,在后续执行命令时导致客户端输入缓冲区溢出。因此 Redis 配置了一个 REDIS_AOF_REWRITE_ITEMS_PER_CMD 常量,当一条命令中的元素超过这个数量时,会被拆分成多条语句

AOF 缓冲

AOF 重写过程中,Redis 服务器仍然要接收客户端的写入请求,为了保证数据安全,使用了子进程执行 AOF 重写,此时如果执行写入命令,子进程并不知道父进程所做的修改,AOF 完成之后会出现 AOF 文件中的数据与实际数据库中的数据不一致的情况。因此在 AOF 重写期间,客户端接收到的命令除了写入 AOF 缓冲区,还要写入 AOF 重写缓冲区

AOF 重写完成后,子进程会向父进程发送一个完成信号。父进程收到后将 AOF 重写区的内容追加到新 AOF 文件中,然后将 AOF 改名,覆盖原来的 AOF 文件

RDB

手动执行持久化

Redis 的 RDB 持久化功能通过 SAVE 和 BGSAVE 两个命令可以生成压缩的二进制 RDB 文件,通过这个文件可以还原生成文件时数据库的状态。

其中 SAVE 阻塞主线程,在 RDB 文件生成完之前不能处理任何请求。而BGSAVE 则会 fork 一个子进程,在子进程中创建 RDB 文件,父进程仍然能够处理客户端的命令。但是 BGSAVE 执行过程中,新的 SAVE 和 BGSAVE 命令会被拒绝,因为会产生竞争条件,BGWRITEAOF 命令会被延迟到 BGSAVE 结束之后。作为对比,BGWRITEAOF 执行过程中,BGSAVE 命令会被拒绝,这里拒绝 BGSAVE 是出于性能考虑,两者实际上不存在竞争冲突

在 Redis 6.0 以前,虽然 Redis 处理处理请求是单线程的,但 Redis Server 还有其他线程在后台工作,例如 AOF 每秒刷盘、异步关闭文件描述符这些操作

SAVE 和 BGSAVE 都会调用 rdb.c/rdbSave 执行真正的持久化过程。

Redis 启动时,会根据 /etc/redis/redis.conf 配置文件中的 dir 和 dbfilename 加载 RDB 文件。如果已经开启了 AOF 持久化,Redis 会优先使用 AOF 来恢复数据库,配置文件例如:

# RDB 配置示例
# https://github.com/redis/redis/blob/48e24d54b736b162617112ce27ec724b9290592e/redis.conf#L125

# 重要参数:
dbfilename dump.rdb
dir /var/lib/redis
复制代码

载入 RDB 文件时实际工作由 rdb.c/rdbLoad 完成,载入期间主线程处于阻塞状态。

自动执行持久化

Redis 启动式根据用户设定的保存条件开启自动保存。在/etc/redis/redis.conf 配置文件中加上 save <seconds> <changes> 表示在 seconds 秒内对数据库进行了 changes 次修改,BGSAVE 命令就会执行。这个配置会被加载到 struct redisServer 的 struct saveparam 参数中。saveparam 是一个链表,当配置多个 save 条件时,这个条件都会被加入链表中。

如何判断是否满足自动保存的条件?

struct redisServer 中 long long dirty 用来保存从上一次 RDB 持久化之后数据库修改的次数,set <key> <value> 会对 dirty 加一,而 sadd <set-name> <value1> <value2> <value3> 会对 dirty 加 3。time_t lastsave 记录了上一次完成 RDB 持久化的时间

Redis 使用 int serverCron 函数执行定时任务,这些任务包括自动保存条件检查、更新时间戳、更新 LRU 时钟等。serverCron 每隔 100 ms 执行一次,其中检查自动保存条件的代码如下:

// https://github.com/redis/redis/blob/48e24d54b736b162617112ce27ec724b9290592e/src/redis.c#L1199

// 开始检查自动保存条件前会先检查是否有正在后台执行的 RDB 和 AOF 进程
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {
	// 已有后台的 RDB 或 AOF 进程
} else {
  // 遍历 saveparams 链表中所有的配置条件
	for (j = 0; j < server.saveparamslen; j++) {
    struct saveparam *sp = server.saveparams+j;

    /* 满足自动保存的标准:
    1. 从上次完成 RDB 到现在的数据库修改次数(dirty)已经达到了 save 配置中 changes 的值
    2. 距上一次完成 RDB 的时间(lastsave)已经达到了 save 配置中 seconds 的值
    3. 上一次 RDB 已经成功,或者距上一次尝试 RDB 的时间(lastbgsave_try)已经达到了配置的超时时间(REDIS_BGSAVE_RETRY_DELAY)
		*/
    if (server.dirty >= sp->changes &&
        server.unixtime-server.lastsave > sp->seconds &&
        (server.unixtime-server.lastbgsave_try >
         REDIS_BGSAVE_RETRY_DELAY ||
         server.lastbgsave_status == REDIS_OK))
    {
        redisLog(REDIS_NOTICE,"%d changes in %d seconds. Saving...",
            sp->changes, (int)sp->seconds);
        rdbSaveBackground(server.rdb_filename);
        break;
    }
  }
}
复制代码

RDB 文件格式(以版本“0006”为例)

RDB 文件主要由五个部分构成:

数据文件中存储了所有的数据。开头的 SELECTDB 常量(值为 376)和紧接着的编号,指示了读取 RDB 文件时,后续加载的数据将会被写入哪个数据库中。

key_values 中保存了所有的键值对,主要包括 key,value 和 value 的类型,对于设置了过期时间的 key,还有 EXPIRETIME_MS 常量(值为 374)和用 unix 时间戳表示的过期时间。其中类型可以是下表中的值,分别对应了 Redis 数据结构的类型:

数据结构类型编码常量
字符串 REDIS_RDB_TYPE_STRING,值为 0
列表 REDIS_RDB_TYPE_LIST,值为 1
集合 REDIS_RDB_TYPE_SET,值为 2
有序集和 REDIS_RDB_TYPE_ZSET,值为 3
哈希 REDIS_RDB_TYPE_HASH,值为 4
使用压缩列表实现的列表 REDIS_RDB_TYPE_LIST_ZIPLIST
使用整数集合实现的集合 REDIS_RDB_TYPE_SET_INTSET
使用压缩列表实现的有序集合 REDIS_RDB_TYPE_ZSET_ZIPLIST
使用压缩列表实现的哈希 REDIS_RDB_TYPE_HASH_ZIPLIST

这些编码常量所对应的值都可以在 rdb.h 中查看

这个类型会影响读取数据时如何解释后面 value 代表的值,而 key 则总是被当作 REDIS_RDB_TYPE_STRING 类型

各类型对应的 value 结构如下:

value 结构备注示例类型
编码,值 表示可以用 8 位整数表示的字符串 REDIS_RDB_ENC_INT8,123 REDIS_RDB_TYPE_STRING
  表示字符串 REDIS_ENCODING_RAW, 5, hello  
元素个数,列表元素 其中会记录每个元素的长度 3, 5, "hello", 5, "world" REDIS_RDB_TYPE_LIST
元素个数,集合元素 其中会记录每个元素的长度 3, 5, "hello", 5, "world" REDIS_RDB_TYPE_SET
键值对个数,键值对 其中会记录每个键值对 key, value 的长度 2, 1, "a", 5, "apple", 1, "b", 6, "banana" REDIS_RDB_TYPE_HASH
元素个数,member 和 score 对 其中会记录 member 的长度,member 在 score 前面 2, 2, "pi", 4, "3.14", 1, "e", 3, "2.7" REDIS_RDB_TYPE_ZSET
转化成字符串对象的整数集合 读取 RDB 时需要将字符串对象转化回整数集合   REDIS_RDB_TYPE_SET_INTSET
转化成字符串对象的压缩列表 读取时需要转化成列表   REDIS_RDB_TYPE_LIST_ZIPLIST
转化成字符串对象的压缩列表 读取时需要转化成哈希   REDIS_RDB_TYPE_HASH_ZIPLIST
转化成字符串对象的压缩列表 读取时需要转化成有序集合   REDIS_RDB_TYPE_ZSET_ZIPLIST

如何保证写操作正常执行

利用 COW 机制,fork 出子进程共享主线程的内存数据。在主线程修改数据时把这块数据复制一份,此时子进程将副本写入 rdb,主线程仍然修改原来的数据

频繁执行全量快照的问题

  1. 全量数据写入磁盘,磁盘压力大。快照太频繁,前一个任务还未执行完,快照任务之间竞争磁盘带宽,恶性循环
  2. fork 操作本身阻塞主线程,主线程内存越大,阻塞时间越长,因为要拷贝内存页表

**解决方法:**全量快照后只做增量快照,但是需要记住修改的数据,下次全量快照时再写入,但这需要在内存中记录修改的数据。因此 Redis 4.0 提出了混合使用 AOF 和全量快照,用 aof-use-rdb-preamble yes 设置。这样,两次全量快照间的修改会记录到 AOF 文件

写多读少的场景下,使用 RDB 备份的风险

  1. 内存资源风险:Redis fork子进程做RDB持久化,如果修改命令很多,COW 机制需要重新分配大量内存副本,如果此时父进程又有大量新 key 写入,很快机器内存就会被吃光,如果机器开启了 Swap 机制,那么 Redis 会有一部分数据被换到磁盘上,当Redis访问这部分在磁盘上的数据时性能很差。如果机器没有开启Swap,会直接触发OOM,父子进程可能会被系统 kill。
  2. CPU资源风险:虽然子进程在做RDB持久化,但生成RDB快照过程会消耗大量的CPU资源。可能会与后台进程产生 CPU 竞争,导致父进程处理请求延迟增大,子进程生成RDB快照的时间也会变长,Redis Server 性能下降。
  3. 如果 Redis 进程绑定了CPU,那么子进程会继承父进程的CPU亲和性属性,子进程必然会与父进程争夺同一个CPU资源,整个Redis Server 的性能爱将,所以如果 Redis 需要开启定时 RDB 和 AOF 重写,进程一定不要绑定CPU。

Ref

  1. Redis-RDB-Dump-File-Format
  2. Redis 设计与实现

与[转帖]Redis 持久化原理和实现相似的内容:

[转帖]Redis 持久化原理和实现

https://juejin.cn/post/6877763937513766919 Redis 所有的数据和状态存储在内存中,为了避免进程退出而导致数据丢失,需要将数据和状态保存到硬盘上。 为了达到这一目的,通常有两种实现方式: 将 Redis 当作一个状态机,记录每一次的对 Redis 的操作,

[转帖]Redis 哨兵模式(Sentinel) 原理

https://juejin.cn/post/6865687858905088008知道的还是太少呢. 为什么需要哨兵模式(Sentinel) 只依靠持久化方案,在服务器下线后无法恢复服务 使用主从复制,在 master 节点下线后,可以手动将 slave 节点切换为 master,但是不能自动完成

[转帖]Redis持久化文件RDB的格式解析

http://www.innereye.cn/2017/01/17/redis/Redis_RDB_File_Format_20170117/ 本文可转载演绎,但需要注明原作者和本文链接。 By BaiFan 发表于 2017-01-17 文章目录 1. Redis RDB文件格式 2. 解析RBD

[转帖]Redis持久化-RDB和AOF

持久化的功能: Redis是内存数据库, 数据都是存储在内存中, 为了避免进程退出导致数据的永久丢失, 需要定期将Redis中的数据以某种形式(数据或命令) 从内存保存到硬盘。 当下次Redis重启时, 利用持久化文件实现数据恢复。 除此之外, 为了进行灾难备份, 可以将持久化文件拷贝到一个远程位置

[转帖]redis 持久化方式 - aof 和 rdb 区别

https://wenfh2020.com/2020/04/01/redis-persistence-diff/ aof 和 rdb 是 redis 持久化的两种方式。我们看看它们的特点和具体应用场景区别。 1. 持久化特点 1.1. aof 1.2. rdb 2. 使用场景区别 3. 持久化详细文

[转帖]深入理解Redis的持久化

https://www.cnblogs.com/ivictor/p/9749465.html RDB RDB是将当前数据生成快照保存到硬盘上。 RDB的工作流程: 1. 执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进程,如RDB/AOF子进程,如果存在bgsave命令直接返回。

[转帖]Redis 运维实战 第05期:RDB 持久化

https://cloud.tencent.com/developer/article/1986826 前面一节,我们聊了 AOF,AOF 有个不足点就是:进行数据恢复时,需要逐一把日志都执行一遍,非常耗时间。 Redis 还有另外一种持久化方法:内存快照。指内存中的数据在某一时刻的状态记录,这个快

[转帖]Redis 运维实战 第04期:AOF 持久化

Redis 运维实战 第04期:AOF 持久化 https://cloud.tencent.com/developer/article/1986824 Redis 有两种持久化方式:AOF 和 RDB。本节就先来聊聊 AOF。 AOF(Append Only File) 日志是写后日志,Redis

[转帖]Redis进阶(发布订阅,PipeLine,持久化,内存淘汰)

目录 1、发布订阅 1.1 什么是发布订阅 1.2 客户端实例演示 1.3 Java API演示 1.4 Redis发布订阅和rabbitmq的区别 2、批量操作 2.1 普通模式与 PipeLine 模式 2.2 适用场景 2.3 源码解析 2.4 Pipelining的局限性 2.5 事务与 L

[转帖]Redis各版本特性汇总

redis4redis5redis6redis6.2重大特性1.模块系统 2.PSYNC2 3.LFU淘汰策略 4.混合RDB-AOF持久化 5.LAZY FREE延迟释放 6.MEMORY内存分析命令 7.支持NAT/DOCKER 8.主动碎片整理 1.新增Stream数据类型 2.新增Redis