IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> redis 持久化原理 -> 正文阅读

[大数据]redis 持久化原理


前言

本文参考源码版本为 redis6.2

我们先来看看演化这个事儿。

软件都是迭代中越做越复杂。redis 也是这样,刚开始做了一个简单功能,通过一定的数据结构组织数据,让你能够相当快的拿到这些数据;

慢慢地,场景越来越多,想要的功能支持越来越强烈;好,redis 慢慢给你实现。

当然,这是功能层面的迭代演进,性能上呢?

首先,我要处理客户端的请求,按照服务端 socket 编程几个步子写,相当容易;

写好之后,你说不行,QPS 至少 10W+;于是,继续改造,写了一套 Reactor 模型来应对网络请求,10W+ 的 QPS 目标达到了。

用了一段时间你又说,这服务一断电,所有数据就没了,我的心血… 能不能靠谱点,想个办法恢复!

于是,又接着写了一个拷贝进程,满足一定条件,就把整个内存快照同步至磁盘,出现断电或者重启,直接从磁盘加载,数据就恢复了,完美!这就是 RDB。

你用了段时间,服务故障崩溃了,重启时 RDB 给你进行了数据恢复,你依然很生气,怎么丢了最近很多数据?

我又想了个办法,干脆把所有的命令都记录下来,下次恢复数据直接重放不就行了?balbala… 很快就写了出来,并且支持三种落盘策略(always, everysec, non)以提升效率,这就是 AOF。

交付的时候,我特意嘱咐道,RDB 和 AOF 要结合使用,数据恢复时,会先加载 RDB 全量数据,然后再重放 AOF 增量就能恢复了,这种情况下能尽可能将损失降到最低。生产上,我们也经常采用这种方式。

一天,你因为线上访问量剧增,redis 服务崩了,准备重启,但花了好长时间才完全恢复,你怒火冲冲质问道,怎么要重启这么久??? 因为 AOF 文件太大,重放了很久。

很好奇,前有 RDB 全备,AOF 增量备份能有多少数据。排查后发现,主要是针对一批相同的 keys 做了大量修改操作;突然间意识到,其实存最新的 value 即可。

于是又写了一个叫 AOF 重写的操作,本质是从拿到这些 key 的最新 value,生成 AOF 指令,来达到压缩 AOF 文件的目的。

终于,开始稳定了。

当然,redis 发展肯定没有这么波折,以上流程只是为了形象化展示 redis 发展历程。

一、持久化?

持久化是什么?就 redis 而言,本质就是将内存中不稳定的数据,通过一些刷盘策略写入磁盘,从而达到断电等故障能恢复数据的效果。

说到持久化,我们一般关心以下几个问题:

  • 落盘策略(频率、时机、格式等)
  • 恢复策略(自动、手动等)

等等,其实我们可能更关心的是:

  • 是否会丢数据?
  • 是否造成节点对外不可用?
  • 性能会不会受影响?
  • 能否手动执行?
  • 生产环境我们该如何使用?

带着问题,我们一起往下寻找答案~

二、RDB

RDB 全称 redis database,是以时间为轴线的全量内存快照,存在磁盘上。快照,就是那个时间点内存数据库的全景图,就像拍照一样,把那个瞬间的所有状态“咔”的一声记录下来。

实际过程中,我们可以定期执行 RDB 操作,要进行数据恢复的时候,找到最近的一个快照点进行恢复,可以将数据丢失风险尽可能的降低。

1. 落盘策略:

很慢?

这个主要取决于你的数据量,比如,你的配置是 16Gb redis 内存,目前已占用 80%,相当于 10多个Gb的数据了,执行一次是需要花一定的时间。

redis 提供了两种方式,一种是 save,另一种是 bgsave。两者底层处理原理都一样,最大的区别点在于 SAVE 由主线程执行,会阻塞客户端命令;BGSAVE 由 fork 的子进程处理,不会阻塞执行。

1)SAVE:

在命令行输入:

127.0.0.1:6379> save
OK

全程会阻塞,直到 RDB 文件创建完毕。

2)BGSAVE:

在命令行输入:

127.0.0.1:6379> bgsave
Background saving started

这个过程会派生出子进程来创建 RDB 文件,父进程继续处理命令请求。

手动执行?

可以在命令行通过 SAVE 或者 BGSAVE 命令手动执行,生成 RDB 文件。

自动执行?

相比于手动执行,通常情况下,我们更希望通过一些配置,达到某些策略条件之后,自动执行 RDB 持久化。

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

save 900 1
save 300 10
save 60 10000

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

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

占用额外内存?

你可能会问,BGSAVE 操作会 fork 一个进程处理,而子进程会拥有父进程完整的数据集,这个过程相当于占用了双倍的内存空间?

不会。fork 子进程使用了一种叫做写时拷贝的技术(copy-on-write)。

写时拷贝是指子进程的页表项指向与父进程相同的物理内存页,这样只拷贝父进程的页表项就可以了,当然要把这些页面标记成只读。如果父子进程都不修改内存的内容,大家便相安无事,共用一份物理内存页。但是一旦父子进程中有任何一方尝试修改,就会引发缺页异常(page fault)。

此时,内核会尝试为该页面创建一个新的物理页面,并将内容真正地复制到新的物理页面中,让父子进程真正地各自拥有自己的物理内存页,然后将页表中相应的表项标记为可写:

在这里插入图片描述

从上面的描述可以看出,对于没有修改的页面,内核并没有真正地复制物理内存页,仅仅是复制了父进程的页表。这种机制的引入提升了fork的性能,从而使内核可以快速地创建一个新的进程。

2. 恢复策略:

RDB 的载入策略是自动加载,没有额外提供载入命令。redis 默认使用 RDB 持久化策略,如果你想采用这种模式, 就不需要再去手动开启了。

在该策略下,redis 在启动时,会自动检测是否存在 RDB 文件,存在的话就进行载入。

服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。所以,如果你的文件比较大,还是需要花费一定的时间来加载,在这期间,服务对外不可用。

三、AOF

1. 落盘(同步)策略:

首先:

写入:指通过 write 系统调用,将数据从 aof_buf 写入 内核缓冲区。

落盘(同步):指的是将数据从内核缓冲区同步至磁盘中。因为,由于操作系统自身的优化策略,我们通过 write 写入的数据,都是直接进入内核缓冲区,然后会根据内核将数据同步至磁盘。

aof_buf 缓冲区:

redis 服务端提供的缓冲区,请求命令会写入 aof_buf 缓冲区,然后由 aof_buf 缓冲区写入内核缓冲区或者同步至磁盘。

落盘时机:

在主事件循环,通过 beforeSleep 方法触发 flushAppendOnlyFile 调用,这便是入口。那,一条命令,经过哪些过程最终追加到 AOF 文件呢?

  • redis 接收请求命令 cmd,并执行。
  • cmd 执行成功后,通过 feedAppendOnlyFile 方法将命令写入 aof_buf 缓冲区。
  • 下一轮主循环中,通过 flushAppendOnlyFile 尝试写入或者同步。

来看看同步相关代码块:

try_fsync:
    // 如果设置了 aof_no_fsync_on_rewrite = yes 并且 有子进程在活动(rdb, aof rewrite, module)
    if (server.aof_no_fsync_on_rewrite && hasActiveChildProcess())
        return;

    // 如果是 AOF_FSYNC_ALWAYS 策略,直接 fsync 落盘
    if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
    
        ...
        
        redis_fsync(server.aof_fd); /* Let's try to get this data on the disk */
         ...
         
    } 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_fsync_offset = server.aof_current_size;
        }
        server.aof_last_fsync = server.unixtime;
    }
  • 首先,如果设置了 aof_no_fsync_on_rewrite = yes 并且 有子进程在活动(rdb, aof rewrite),将暂时不进行刷盘,避免大量磁盘操作(Linux 这种情况下,最多可能丢失30s 数据)。
  • 其次,如果设置了 AOF_FSYNC_ALWAYS 策略,将直接进行 fsync 操作。
  • 最后,如果设置的是 AOF_FSYNC_EVERYSEC 策略,将提交给后台线程来完成 fsync 操作。

2. 恢复策略:

恢复一般是指,经历故障或其他行为,通过重启 redis 服务并加载数据文件恢复内存状态的过程。

redis 默认通过 RDB 操作进行持久化,如果需要采用 AOF 持久化可以配置参数:

appendonly yes

服务在重启的过程中会自动检测 AOF 文件是否存在,如果存在的话,就开始通过指令重放来恢复内存数据库状态。

指令重放?redis 通过伪客户端的方式,将命令一条条取出来,走正常的命令处理逻辑,直到处理处理完成。

你可能会问,如果一个 key 出现了多次,也要处理多次吗?

> set key1 value1
> set key1 value11
> set key1 value 111

没错,这种也要执行多次,虽然内存中肯定只会保留最新的一次操作状态。是不是很臃肿?

接下来,我们通过 AOF 重写来看看怎么解决这个问题。

3. 重写

上文提到,AOF 实实在在的记录了每一条命令,尤其是出现大量重复 key 的操作,会使得 AOF 看起来冗余且臃肿;因此,AOF 重写就相当于对 AOF 文件进行压缩,同一个 key 只保留最新的操作记录。

值得注意的是,AOF 重写并不需要从原 AOF 文件中进行压缩,而是直接扫描整个库,把每一个 key / value 转变成写命令,然后追加至临时文件;待重写操作完成后,将临时文件原子性重命名即可。

重写时机?

redis 提供了灵活的重写机制,可以自动触发,也可以手动触发。

首先,需要确定是否开启了 AOF 策略,即:

appendonly yes

你可以在命令行输入 BGREWRITEAOF 手动触发重写:

127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started

另外,redis 也提供了自动触发重写条件:

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

每次重写之后都会记录文件大小,作为下一次重写的触发条件:当前文件大小超过上次重写的百分比后,触发重写操作。

同时,为避免 AOF 文件过小而触发重写操作,提供了 auto-aof-rewrite-min-size 参数控制重写条件的下限。

子进程?

为避免阻塞主线程,AOF 重写仍然采用 fork 子进程的方式

重写流程:

1)触发重写条件(自动 or 手动)
2)redis 调用 aof.c#rewriteAppendOnlyFileBackground 方法,并 fork 子进程进行处理

  • 2a)子进程重写 AOF 到临时文件
  • 2b)父进程继续接收新命令并累加到 server.aof_rewrite_buf 缓冲区

3)直到 2a 完成后
4)父进程尝试将 server.aof_rewrite_buf 数据写入临时文件,并用临时文件替换原 AOF 文件。

四、RDB-AOF混合持久化

我们先看看 redis 重启自动加载持久化文件的片段:

void loadDataFromDisk(void) {

    ...
    
    if (server.aof_state == AOF_ON) {
        // 加载 aof
        if (loadAppendOnlyFile(server.aof_filename) == C_OK)
        ...
    } else {
        // 加载 rdb 
        if (rdbLoad(server.rdb_filename,&rsi,RDBFLAGS_NONE) == C_OK) {
        
        ...

}        

可以看到,要么是加载 AOF 文件,要么加载 RDB 文件;RDB - AOF 混合文件又是如何来的?

随着 redis 的普及使用,大家发现,不管是 RDB 还是 AOF 都有各自缺点,而两者结合使用可以大大改善这种情况。

于是,redis 4.0 推出了 RDB-AOF 这种混合模式,可以通过下面配置开启:

appendonly yes
aof-use-rdb-preamble yes

可以看到,使用混合模式的前提是,需要先开启 AOF;而 aof-use-rdb-preamble 参数则控制是否使用混合模式。

使用这种模式,当数据重写后,AOF 文件前半部分是 RDB 数据(采用 RDB 数据格式),AOF 文件后半部分继续追加 AOF 数据(AOF 数据格式)。

因此,我们回到加载部分来看,当采用 RDB-AOF 混合模式时,数据加载也是直接走 AOF 文件加载,由于 AOF 文件存储了 RDB 和 AOF 数据,也就达到了 全量加载RDB数据 和 增量加载AOF数据 的策略。


五、总结

redis 提供了两种持久化的方式,分别是 RDBAOF

  • RDB 是内存快照,以二进制的形式存储,加载时按照约定的格式解析即可。
  • AOF 则记录真实的执行命令,加载 AOF 文件时,将命令逐条进行重放。

RDB 快照的主要优点:

  • 数据文件相对 AOF 文件更小
  • 数据加载时相对 加载 AOF 文件更快

AOF 文件主要优点:

  • 相对于 RDB 数据丢失可能性更小
  • 提供多种策略及时将数据刷盘

实际很多情况下,我们可能需要 性能优势 + 数据持久性,因此 redis 也提供了 RDB-AOF 的混用模式,在 AOF 文件中前半部分采用 RDB 数据格式,后部分增量数据采用 AOF 格式存储,这也叫做 全量RDB持久化 + 增量AOF持久化模式

由于 AOF 记录的是命令,就有可能出现同一个 key 记录多次的情况而导致 AOF 过度膨胀,因此,引入 AOF 重写机制来压缩 AOF 文件;当然,重写也并不需要真正读取原 AOF 文件,而是直接读取内存数据库,生成对应的指令即可。

生成 RDB 文件 和 AOF 文件重写都是通过 fork 子进程的方式来处理。主要利用了子进程拥有父进程的内存页表项,以及其他从父进程继承的数据项;从而利用子进程的写时复制特性来提升处理效率。




参考文献:
  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-07-03 10:53:18  更:2022-07-03 10:54:17 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 13:40:34-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码