百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

Redis 持久化

mhr18 2024-11-18 14:32 19 浏览 0 评论

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 数据结构的类型:

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

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

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


如何保证写操作正常执行

利用 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 设计与实现

相关推荐

【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...

Pure Storage推出统一数据管理云平台及新闪存阵列

PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...

对Java学习的10条建议(对java课程的建议)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!

官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...

JDK21有没有什么稳定、简单又强势的特性?

佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...

「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了

在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...

Java面试题及答案最全总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

数据库日常运维工作内容(数据库日常运维 工作内容)

#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...

分布式之系统底层原理(上)(底层分布式技术)

作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...

oracle 死锁了怎么办?kill 进程 直接上干货

1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

取消回复欢迎 发表评论: