跳到主要内容

持久化

2025年02月19日
柏拉文
越努力,越幸运

一、认识


Redis 持久化 是指将 Redis 中的数据保存到磁盘,以便在 Redis 服务器重启或崩溃时能够恢复数据 (Redis 所有数据保持在内存中, 对数据的更新将异步地保存到磁盘上)。Redis 提供了几种持久化方式,通过不同的机制保证数据的持久化和高效性。根据需求的不同,持久化方式的选择也有所不同。Redis 的持久化机制 主要有两种:

一、RDB 数据库快照: RDBRedis 的一种持久化方式,通过在指定时间间隔内生成数据的快照,并将其保存为二进制文件(通常是 dump.rdb 文件)。RDB 适合于数据不需要频繁更新或对持久化性能有较高要求的场景。RDB 特点: 周期性存储, RDB 会在指定的时间间隔内生成快照,将数据库的当前状态保存到磁盘; 全量存储, RDB 存储的是整个数据库的快照,不支持增量保存; 低延迟读取, 由于 RDB 是全量存储的二进制文件,恢复数据时比 AOF 更快速; 适用于备份, RDB 适合做周期性的备份,或者在 Redis 重启后恢复数据。RDB 缺陷: 数据丢失风险, 由于 RDB 是周期性存储的,如果 Redis 崩溃或发生故障,可能会丢失上次保存后的所有数据(比如上次保存和故障发生之间的数据)。因此, RDB, 适合定期备份,内存消耗低,恢复快,但可能丢失最后一次保存后的数据。

  1. 配置: RDB 存储可以通过配置文件设置条件,指定何时生成快照:

    save 900 1      # 900秒(15分钟)内至少有1个键发生变化时,进行快照保存
    save 300 10 # 300秒(5分钟)内至少有10个键发生变化时,进行快照保存
    save 60 10000 # 60秒内至少有10000个键发生变化时,进行快照保存

    dir ./ # 指定了 RDB 文件的存放目录,此处设置为当前工作目录。在生产环境中通常建议使用绝对路径以避免路径问题。
    dbfilename bump.rdb # 设置 RDB 文件的文件名为 bump.rdb,这意味着生成的快照文件不会采用默认的 dump.rdb 名称,便于根据业务或版本需求进行区分管理。

    rdbchecksum yes # 开启校验功能,生成的 RDB 文件末尾会包含校验码。在恢复数据时,Redis 会使用这个校验码检查文件的完整性,确保数据没有被损坏。
    rdbcompression yes # 启用数据压缩,在生成 RDB 文件时会对数据进行压缩,从而节省磁盘空间。需要注意的是,压缩可能会增加 CPU 的负担,因此在高性能需求的场景下需要权衡使用。
    stop-writes-on-bgsave-error yes # 当 Redis 在后台执行 BGSAVE(异步保存 RDB 文件)过程中遇到错误时,会停止接收写命令。这是为了避免在持久化失败后继续写入数据,可能导致数据的不一致性或丢失,从而提高系统的安全性。

    我们在实际项目开发中, 一般不使用自动生成 dump.rdb 的配置,因为不受控制。我们的最佳配置如下:

    dir /bigdiskpath                 # 指定一个很大存储空间的目录
    dbfilename dump-${port}.rdb # 基于端口来区分 dump.rdb 文件
    rdbcompression yes # 启用数据压缩,在生成 RDB 文件时会对数据进行压缩,从而节省磁盘空间
    stop-writes-on-bgsave-error yes # 当 Redis 在后台执行 BGSAVE(异步保存 RDB 文件)过程中遇到错误时,会停止接收写命令
  2. 触发: RedisRDB(快照) 持久化方式主要用于定期将内存中的数据保存到磁盘上的一个二进制文件中,其触发方式主要包括以下几种:

    • 自动触发: 基于 save 配置规则, 在 redis.conf 中,可以配置多个 save 规则,如:

      save 900 1     # 如果 900 秒内至少有 1 个 key 被修改,则触发一次 RDB 快照。
      save 300 10 # 如果 300 秒内至少有 10 个 key 被修改,则触发快照。
      save 60 10000 # 如果 60 秒内至少有 10000 个 key 被修改,则触发快照。

      只要满足任一规则,Redis 就会自动调用 BGSAVE 命令,在后台 fork 出子进程生成 RDB 文件,而不会阻塞主进程。

    • 手动触发:

      • SAVE: 使用 SAVE 命令也可以生成 RDB 快照,但这是一个同步操作,会阻塞 Redis 进程直到保存完成,因此在生产环境中较少使用。

      • BGSAVE: 手动执行 BGSAVE 命令,在后台 fork 出子进程生成 RDB 文件,而不会阻塞主进程。适合在需要即时备份数据时使用,同时不会影响正常业务。

    • 主从复制初次同步: 当一个从节点刚连接到主节点时,从节点会请求一个完整的数据快照以进行初次同步。此时,主节点会生成一次 RDB 快照并将其传送给从节点。

  3. 文件策略: 默认情况下,RedisRDB 文件命名为 dump.rdb。文件存放位置由 redis.conf 中的 dir 参数指定,确保所有快照文件都位于预设的目录中,便于管理和备份。

    • 原子性与临时文件机制: 1. 临时文件写入, 在执行 RDB 快照时,Redis 会先在子进程中将数据写入一个临时文件(通常带有临时标识,如 dump.rdb.tmp)。2. 原子替换, 完成写入后,临时文件会通过原子性操作(rename)替换原有的 RDB 文件。这种策略确保了在快照过程中,即使发生异常,原有的 RDB 文件仍然保持有效,避免了部分写入或损坏的风险; 文件替换是瞬间完成的,避免在读取文件时出现不一致的状态。

    • 文件覆盖与备份策略: 覆盖更新, 每次成功生成 RDB 快照后,旧的快照文件会被新生成的文件所覆盖。这意味着 Redis 始终只保留最近一次成功生成的全量快照。备份建议, 由于覆盖策略可能导致历史快照丢失,建议在生产环境中定期备份 RDB 文件,以防止误操作或系统故障导致的数据恢复问题。

    • 压缩配置: 可以通过 rdbcompression 参数决定是否对 RDB 文件进行压缩。开启压缩可以显著减少磁盘空间的占用,但在快照生成时可能会增加 CPU 开销。

二、AOF 追加文件日志: AOFRedis 的另一种持久化方式,它通过记录所有写操作来实现持久化。每次对 Redis 的写操作都会追加到 AOF 文件中,从而可以通过重放 AOF 文件来恢复数据。AOF 持久化保证了较高的数据持久性,但它对磁盘和性能的要求较高。AOF 特点: 增量存储, AOF 会记录每次写操作的命令,因此数据恢复时可以通过执行这些命令来重建数据; 高度可靠, 由于每次写入操作都被记录,AOF 可以保证数据的持久性。即使 Redis 崩溃,AOF 文件也可以恢复所有已执行的操作; 慢写操作, AOF 文件会不断追加写操作,因此其写入性能相对较差,尤其在高并发写操作时。AOF 缺陷: 性能开销, AOF 操作会记录每个写操作,频繁的写入会增加 I/O 开销,导致性能下降; AOF 文件增大, AOF 文件随着写入操作的增加会变得非常大,尤其在没有优化的情况下。因此, AOF, 适合对数据可靠性要求高的应用,能够保证不丢失写操作,但可能会导致性能下降。

  1. 配置: AOF 持久化可以通过以下方式进行配置

    appendonly yes         # 启用 AOF 持久化
    appendfsync everysec # 每秒同步一次 AOF 文件
    appendfsync always # 每次写入操作都同步(更可靠,但性能差)
    appendfsync no # 不主动同步(性能最佳,但风险最大)
  2. 策略: Redis AOF 持久化机制提供了三种不同的 appendfsync 策略,用于平衡数据安全性和写入性能:

    • always 策略: 每执行一条写命令后,Redis 都会立即调用 fsync 将数据写入磁盘。数据几乎不会丢失,安全性最高。性能开销大,频繁的系统调用可能严重影响写入性能,不适合高并发场景。

    • everysec 策略: Redis 每秒钟执行一次 fsync 操作,将这一秒内的所有写命令统一写入磁盘。在大多数场景下,性能和数据安全性取得了较好的平衡;即使发生故障,也最多丢失最近 1 秒的数据。在故障发生时,可能会丢失最后一秒的数据。

    • no 策略: 不主动调用 fsync,完全依赖操作系统的磁盘缓存刷新机制,由操作系统决定何时将数据从缓存写入磁盘。性能最高,因为减少了大量的系统调用。数据安全性最低,操作系统调度不当时可能会导致较大范围的数据丢失。

  3. 重写: 由于 AOF 会不断增加写操作命令,文件可能会变得非常大。Redis 提供了一个 AOF 重写(bgrewriteaof)机制,定期重新生成一个精简版本的 AOF 文件,去掉冗余的命令, 来减少硬盘占用量, 加速恢复速度。Redis 会在后台线程中执行 bgrewriteaof 操作,压缩 AOF 文件,删除不必要的命令,使 AOF 文件更小。AOF 触发方式 如下:

    • 自动触发: AOF 文件的大小超过某个阈值时,Redis 会自动触发 AOF 重写。比如 AOF 信息 aof_current_size > auto-aof-rewrite-min-size && aof_current_size - aof_base_size/aof_base_size > auto-aof-rewrite-percentage 同时满足时, Redis 会自动触发 AOF 重写

      auto-aof-rewrite-min-size       # AOF 文件重写需要的尺寸
      auto-aof-rewrite-percentage # AOF 文件增长率

      我们实际项目的 AOF 配置如下:

      dir /bigdiskpath                          # 指定一个很大存储空间的目录 
      appendonly yes # 启用 AOF 持久化
      appendfsync everysec # 使用 everysec 同步策略
      appendfilename "appendonly-${port}.aof" # AOF 文件命名
      no-appendfsync-on-rewrite yes # AOF 重写时,是否需要做正常的 append 操作, yes 为不做 append 操作
      auto-aof-rewrite-min-size 100 # AOF 文件重写需要的尺寸
      auto-aof-rewrite-percentage 64mb # AOF 文件增长率
    • 手动触发: 手动执行 bgrewriteaof 命令进行重写。

    Redis 使用 fork 出一个子进程来执行 AOF 重写任务,利用操作系统的写时复制(copy-on-write)机制,使得子进程可以基于当前内存数据生成新的 AOF 文件,而不会阻塞主进程对客户端请求的响应。子进程遍历内存中的所有数据,将恢复当前数据状态所需的最简命令(例如 SET key value)依次写入一个临时的 AOF 文件。这样生成的新文件只包含能够重构出当前数据状态的必要命令,相比原来的 AOF 文件更加精简。在子进程重写期间,主进程仍然处理客户端的写请求,这些新写入的命令会被保存在一个增量缓冲区中。重写完成后,这些在重写过程中产生的命令会追加到新 AOF 文件的末尾,确保没有任何数据丢失。当新 AOF 文件生成完毕,并且增量缓冲区中的数据也已经追加进去后,Redis 使用原子性操作(如 rename)将旧的 AOF 文件替换为新的文件。这种原子替换保证了在任何时刻,Redis 读取的都是一个完整且一致的 AOF 文件。

  4. AOF 追加阻塞: RedisAOFAppend Only File)持久化机制通过将每次写命令追加到文件中,来实现数据的持久化。但在这一过程中,会存在 追加阻塞 的问题。Redisappendfsync 参数提供了三种策略: always, 每次写操作后都立即调用 fsync,将数据同步到磁盘。这种模式保证了数据安全性,但每次写操作都需等待磁盘同步完成,容易导致阻塞; everysec, 每秒执行一次 fsync 操作,是一种折中的选择。这样可以在保证数据相对安全的前提下,降低每次写入的阻塞时间; no, 不主动调用 fsync,而是让操作系统决定何时刷新磁盘缓冲区。这种方式性能最好,但数据的实时持久化安全性较低。由于 fsync 是一个同步调用,在执行该操作期间,Redis 主线程会被阻塞,无法处理其他命令请求。尤其在 appendfsync always 模式下,每个写操作都会等待磁盘同步完成,从而直接导致请求延迟。在高并发场景中,如果磁盘的响应较慢(例如使用性能较低的机械硬盘),频繁的 fsync 操作会使得大量命令处于等待状态,从而显著影响 Redis 的整体吞吐量和响应速度。为了防止 AOF 文件过大,Redis 会定期进行 AOF 重写操作。虽然重写过程采用后台 fork 机制,但在合并最后缓冲区内容时仍可能出现短暂阻塞,因此可以考虑在系统低负载时进行重写。

    • 阻塞定位:

      • Redis 命令 info all / info persistence 命令: 可以查看 AOF 状态、当前 AOF 文件大小、AOF fsync 延迟、最后一次 fsync 操作的状态(如 aof_last_write_status)等信息,从中判断是否有频繁的 fsync 阻塞现象。

      • Redis 命令 latency latest / latency history: 可以获取最近的延迟记录,看看是否有和 fsyncAOF 相关的延迟条目。Redis 的延迟监控能够帮助你快速定位是否存在由于 fsync 导致的命令阻塞问题。

      • 使用系统工具(如 iostatvmstatdstat)监控磁盘 I/O 性能,确认磁盘响应是否存在瓶颈。若磁盘延迟较高,fsync 操作的等待时间自然会拉长,从而导致 AOF 阻塞。

    • 阻塞优化:

      • 不要和高硬盘负载服务部署在一起: 比如存储服务、消息队列

      • 使用 SSD 或者高性能存储设备可以降低 fsync 操作的延迟,从根本上减少阻塞时间

      • 单机多实例持久化目录, 可以考虑分盘、或者资源限制、甚至可以使用 CGroup 类似技术来限制硬盘资源的分配

      • 通常推荐使用 appendfsync everysec 策略,这样可以在保证大部分数据安全的同时,避免每次写操作都被阻塞

      • 为了防止 AOF 文件过大,Redis 会定期进行 AOF 重写操作。虽然重写过程采用后台 fork 机制,但在合并最后缓冲区内容时仍可能出现短暂阻塞,因此可以考虑在系统低负载时进行重写。

三、AOFRDB 混合使用: Redis 允许同时启用 RDBAOF 持久化机制,以便结合两者的优点。RDB 用于周期性生成数据的快照,而 AOF 记录每次写操作,提供更高的持久化保证。组合优点: 备份与数据恢复的双重保障, RDB 提供较快的数据恢复速度,而 AOF 提供较高的数据持久性,二者结合能提供更强的可靠性; 适应不同需求, 可以根据业务需求调整 RDBAOF 的配置,使得数据恢复和性能能够达到一个平衡点。

  1. 配置:

    save 900 1           # 启用 RDB 快照
    appendonly yes # 启用 AOF 持久化
    appendfsync everysec # 每秒同步一次 AOF 文件
  2. 优先级: 如果 Redis 重新启动后, 同时有 dump.rdb 文件和 .aof 文件。 AOF 文件的加载优先级高于 dump.rdb 文件。在默认配置下(即启用了 AOF 持久化),如果 Redis 重启时同时存在 dump.rdb 文件和 .aof 文件,Redis 会优先加载 AOF 文件。这是因为 AOF 文件记录了所有写操作,相较于 dump.rdb 快照,AOF 文件通常包含了最新的数据变更,从而能够更准确地恢复出最近的数据状态。如果 AOF 加载成功,则 dump.rdb 文件不会被使用。

Redis 提供了两种持久化方式(RDBAOF), 选择何种持久化方式取决于以下几个因素:

  • 数据丢失容忍度: 如果业务中可以接受一定的丢失,可以选择 RDB;如果要求较高的数据可靠性,则 AOF 更合适。

  • 性能要求: AOF 会有更多的写操作和 I/O 开销,可能会影响性能。如果性能是优先考虑的因素,可以考虑主要使用 RDB

  • 恢复速度: RDB 恢复数据时较快,而 AOF 恢复时需要执行所有操作,速度较慢。

  • 内存占用: AOF 文件会随操作积累,可能占用较多磁盘空间,而 RDB 的文件较小,但会周期性生成快照。

所示, RDB 适合定期备份,内存消耗低,恢复快,但可能丢失最后一次保存后的数据; AOF, 适合对数据可靠性要求高的应用,能够保证不丢失写操作,但可能会导致性能下降; 另外, 结合 RDBAOF 的优点,既能提供数据备份,又能确保高可靠性。选择合适的持久化策略可以根据业务的具体需求进行调整,保证 Redis 的高效和可靠性。

二、扩展


2.1 fork 操作

Redis 中的 fork() 操作主要用于创建子进程来执行后台任务,从而避免阻塞主进程。这一机制在持久化(如 RDB 快照生成、AOF 重写)和复制初始同步等场景中非常关键。 fork() 是同步操作, 且内存越大, 耗时越长。基本原理: 1. 进程克隆:通过 fork()Redis 会复制当前进程的内存、文件描述符等状态,生成一个子进程。子进程一开始与父进程几乎完全相同。2. Copy-On-Write(写时复制)机制:在 fork() 后,父子进程共享同一块物理内存。只有在任一进程尝试修改某个内存页时,内核才会为该页分配新的物理内存副本,确保彼此数据互不影响。注意事项: 内存占用峰值, 在 fork() 后如果父进程持续进行写操作,可能导致大量内存页被复制,从而在短时间内大幅增加内存使用量; 系统资源要求, fork() 操作需要操作系统分配额外资源,特别是在大数据量场景下,可能会对系统性能产生影响,需要合理监控和配置系统参数。改善 fork(): 1. 可以控制 Redis 实例最大可用内存 maxmemory; 2. 合理配置 Linux 内存粉皮儿策略 vm.overcommit_memory = 1; 3. 降低 fork() 频率, 比如放宽 AOF 重写自动触发时机, 不必要的全量复制。Redis fork() 主要用途:

  1. RDB 快照生成(BGSAVE: 当 Redis 需要生成 RDB 快照时,通过 fork() 创建子进程,在子进程中遍历内存数据并写入临时文件,避免阻塞主进程继续响应客户端请求。

  2. AOF 重写(BGREWRITEAOF: 类似地,AOF 重写时利用 fork() 生成子进程,将内存中的数据转换为一系列重构数据状态所需的最简命令,形成新的 AOF 文件。

  3. 复制初次同步: 在主从复制场景中,新从节点连接时,主节点会 fork() 子进程生成数据快照供从节点同步。

2.2 子进程开销

子进程开销如下:

  1. 内存占用与写时复制开销:

    1. 初始开销较低fork() 时,子进程与父进程共享内存,不会立即复制整个内存空间。

    2. 写时复制:一旦父子进程对共享内存进行写操作,内核会为这些内存页创建副本。这在高写负载场景下,会导致大量内存页被复制,从而使内存占用瞬间增加,甚至可能引发内存不足问题。

  2. CPU 与系统调用开销:

    1. fork() 操作本身较快:单次 fork() 调用通常开销较小,但频繁的 fork 操作(比如频繁的 RDBAOF 重写)会给 CPU 带来不小的负担。

    2. 增量复制与同步操作:子进程生成持久化文件时需要遍历整个数据集,并与主进程期间产生的增量数据进行合并,这也会消耗一定的 CPU 资源。

  3. 磁盘 I/O 影响: 子进程在生成快照或重写 AOF 文件时会进行大量磁盘写操作,如果磁盘 I/O 能力不足,可能会拖慢整个过程,进而影响主进程与整体系统的响应能力。

2.3 子进程优化

  1. 减少 fork 期间的写操作: 尽量降低 fork 后父进程对内存大块区域的写操作,可以通过优化业务逻辑、延迟不必要的内存修改等方式,减少写时复制的触发。

  2. 合理规划持久化策略:

    1. 调整持久化频率:根据业务场景调整 RDBsave 规则以及 AOF 重写的触发条件,避免过于频繁的 fork 操作。

    2. 分布持久化任务:对于写操作较多的应用,可以考虑采用异步持久化或使用混合持久化方案,从而降低单次 fork 带来的开销。

  3. 优化内存管理:

    1. 使用高效内存分配器Redis 默认采用 jemalloc,它在处理大内存块和减少碎片方面表现优异,有助于降低 fork 时的内存复制开销。

    2. 监控内存碎片:定期监控 Redis 的内存使用情况,及时进行内存优化,防止内存碎片导致 fork 时内存开销激增。

  4. 调整操作系统参数:

    1. 内存 overcommit 策略:配置 Linux 的内存 overcommit 参数(如 vm.overcommit_memory),合理的设置可以在 fork 时降低因内存预分配不足而导致的问题。

    2. 提升磁盘 I/O 性能:使用 SSD 或其他高性能磁盘,确保持久化文件写入不会成为性能瓶颈。

  5. 不要和高硬盘负载服务部署在一起: 比如存储服务、消息队列

  6. 单机多实例持久化目录: 可以考虑分盘、或者资源限制、甚至可以使用 CGroup 类似技术来限制硬盘资源的分配