• 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
    
    • 1
    • 2

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

    2)BGSAVE:

    在命令行输入:

    127.0.0.1:6379> bgsave
    Background saving started
    
    • 1
    • 2

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

    手动执行?

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

    自动执行?

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

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

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

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

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

    占用额外内存?

    你可能会问,BGSAVE 操作会 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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 首先,如果设置了 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
    
    • 1

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

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

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

    > set key1 value1
    > set key1 value11
    > set key1 value 111
    
    • 1
    • 2
    • 3

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

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

    3. 重写

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

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

    重写时机?

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

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

    appendonly yes
    
    • 1

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

    127.0.0.1:6379> BGREWRITEAOF
    Background append only file rewriting started
    
    • 1
    • 2

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

    auto-aof-rewrite-percentage 100
    auto-aof-rewrite-min-size 64mb
    
    • 1
    • 2

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

    同时,为避免 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) {
            
            ...
    
    }        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

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

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

    appendonly yes
    aof-use-rdb-preamble yes
    
    • 1
    • 2

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

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

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





    参考文献:
  • 相关阅读:
    java计算机毕业设计消防网站源码+系统+数据库+lw文档+mybatis+运行部署
    C18-PEG- ALD批发_C18-PEG-CHO_C18-PEG-醛基
    Spring | @Order 与 Ordered 控制加载顺序
    Springboot 之 Filter 实现 Gzip 压缩超大 json 对象
    C专家编程 第6章 运行的诗章:运行时数据结构 6.10 MS-DOS中的堆栈段
    吴恩达deeplearning.ai:使用多个决策树&随机森林
    LAL v0.32.0发布,更好的支持纯视频流
    梦幻西游手游:工坊进阶考试题目攻略—考古、乐艺篇
    NVMe协议详解(一)
    基于PHP+MySQL的茶叶销售购物网店
  • 原文地址:https://blog.csdn.net/ldw201510803006/article/details/125031948