掘金 后端 ( ) • 2024-04-23 10:52

前言

当 Redis 仅用于缓存时,当缓存失效后,可以从关系型数据库中重新加载数据,不存在丢数据的问题。但 Redis 也可以作为操作数据库,存储业务数据,因此需要一定的机制来保证在极端情况下数据尽量不丢失,比如意外断电等灾难场景

生产环境下,为了保证服务可用性,会将 Redis 以高可用的方式部署,如主从复制、哨兵模式、集群模式。但意外总是那么不经意,极端情况下,仍然会存在数据丢失的风险。因此持久化是必不可少的能力

开启持久化功能后,当 Redis 服务器重启时,内存中的数据已经丢失,可以从备份的磁盘文件中恢复

Redis 支持 RDB 和 AOF 两种持久化方式,并衍生出以下 4 种持久化策略

  • RDB(Redis Database)
  • AOF(Append Only File)
  • 不开启持久化
  • RBD 与 AOF 结合

AOF 持久化更可靠,但需要消耗更多的资源,RDB 消耗的资源较少,但持久化没有 AOF 可靠。实际选型时可以按具体情况权衡利弊。Redis 官方更推荐使用 AOF,或者 RDB + AOF 的持久化策略

一、RDB

实现方式

RDB 持久化以一定的时间间隔,对内存中的数据打一份实时快照,并存到一个磁盘文件中,默认配置下,该文件名为 dump.rdb

启用方式

开启 RDB 持久化的配置非常简单,语法如下

save <seconds> <changes> [<seconds> <changes> ...]

注意:一个 <seconds> <changes> 对就是一个保存点,可以配置多个保存点

这是 Redis 的默认持久化策略,Redis7.0 及以上,如果不显示配置 save,Redis 的默认配置如下

save 3600 1 300 100 60 10000

默认配置下,有多个保存点

  • 3600s(1 小时)至少有 1 次修改(Redis7.0 以下是 900s,即 15 分钟至少有 1 次修改)
  • 300s(5 分钟) 至少有 100 次修改
  • 60s(1 分钟) 至少有 10000 次修改

此外,在 Redis 正常停机之前,会尝试执行一次全量的快照持久化,尽量保证所有的数据都被写入到 RDB 文件中

优缺点

优点

  1. RDB 是一个紧凑的单文件,是存储时间点全量内存数据的实时表示,是数据备份的首选方式
  2. RDB 非常适合作为灾难恢复方案,因为只有一个相对小的单文件,有利于远程传输
  3. RDB 持久化方式性能好,Redis 父进程会 fork 一个子进程完成持久化处理,父进程自身没有磁盘 I/O 操作
  4. 数据量极大时,Redis 通过持久化文件启动时,RDB 比 AOF 更快
  5. 在主从复制场景下,RDB 支持部分重新同步。在从节点重启或者故障转移后,从节点重新连接到主节点时,可以只请求主节点发送部分数据,而不是全部数据。这样可以减少重新同步所需的带宽和时间

缺点

  1. 相对于 AOF,RDB 持久化方式丢数据的风险更大。假设配置的最小保存点间隔为 1min,Redis 每 1 分钟保存一次快照,那么在 Redis 意外停止时,最多可能丢失近 1 分钟的数据
  2. 由于 RDB 需要 fork 子进程来完成持久化处理,如果数据量很大,这个处理过程很耗时。严重情况下,甚至会有高达 1s 的业务延迟。AOF 虽然也会 fork 子进程,但是频率比较低

二、AOF

Redis1.1 引入了 AOF(Append Only File),从字面上理解,就是持续往一个文件追加操作日志

实现方式

Redis 按照 Redis 协议的格式记录每一次写操作日志。在服务重启时,重放这些日志,重建历史数据

fsync

待持久化的 AOF 记录会先存到输出缓冲区,Redis 会调用 fsync() 方法,请求操作系统将缓冲区的数据刷盘

Redis 支持以下 3 种 fsync 模式

fsync 模式 描述 always 每一次写操作都会调用 fsync() 方法,将记录刷到磁盘文件中。速度很慢,但是最安全,几乎不会出现数据丢失 everysec 默认模式。每 1s 调用一次 fsync() 方法,将输出缓冲区中积累的记录刷到磁盘文件中。速度和安全性适中 no Redis 不主动调用 fsync() 方法,由操作系统决定什么时候刷盘。速度更快,但数据安全性最低

启用方式

Redis 默认关闭 AOF 持久化功能。启用方式如下,在 redis.conf 文件中修改

appendonly yes
appendfsync everysec

优缺点

优点

  1. AOF 持久化更可靠。fsync 默认配置 everysec 模式下,仍然有良好的写入性能,fsync 由后台线程处理,主线程则专注于写操作。这种 fsync 模式下,最多会丢失 1s 的写入数据
  2. AOF 文件只支持追加模式,即使由于磁盘已被写满,导致文件以未完成的写入命令结束,文件有损坏的情况,也可以通过 redis-check-aof 工具轻松修复
  3. 当 AOF 文件变得很大时,Redis 会自动在后台进行文件压缩重写,也就是 rewrite,这个过程是安全的。当触发重写条件时,会创建一个新 AOF 文件,然后在后台以重建当前数据最精简的写入命令追加到新文件,处理过程结束后将新文件替换旧文件,新的数据写入日志会追加到重写后的文件中
  4. AOF 文件内容是一条一条的操作日志,通俗易懂,且很容易解析

缺点

  1. 相同数据量下,AOF 文件通常比 RDB 文件大
  2. fsync 模式为 alwayseverysec 时,AOF 在运行效率通常比 RDB 慢

相关配置

AOF 配置 描述 appendonly 是否启用 AOF 持久化,默认值为 no,不启用,如需启用,设为 yes appendfsync 设置 fsync 模式,默认值为 everysec,其他可选值为 always, no appendfilename AOF 文件名/前缀,默认值为 appendonly.aof。Redis7.0 以下,仅表示文件名,Redis7.0+,表示 AOF 相关文件的前缀 appenddirname Redis7.0 新增,AOF 文件夹,用于放置所有 AOF 相关文件。默认值为 appendonlydir no-appendfsync-on-rewrite 重写期间是否允许执行同步操作,默认值为 no。启用的主要目的是提高 AOF 重写的性能 auto-aof-rewrite-percentage 自动重写的触发百分比,默认值为 100。当 AOF 文件大小超过上次重写后的大小的一定百分比时,将触发自动重写。自动重写底层调用 BGREWRITEAOF 命令。如果设置为 0,将彻底关闭自动重写 auto-aof-rewrite-min-size 触发重写的最小文件大小,默认值为 64MB。当 AOF 文件大小超过此值时,将触发自动重写 aof-load-truncated 启动时检测到 AOF 文件被截断时的行为,默认值为 yes,Redis 将尝试加载截断的 AOF 文件,这可能会丢失部分数据;如果设置为 no,Redis 将拒绝启动并给出错误提示 aof-use-rdb-preamble 在 AOF 文件中使用 RDB 文件头部信息,默认值为 yes。如果设置为 yes,Redis 在 AOF 文件开头添加 RDB 文件的内容,以便于在启动时快速加载数据;如果设置为 no,则不添加 RDB 文件头部信息,节省空间但可能导致启动时间较长 aof-timestamp-enabled Redis7.0 新增,默认值为no。如果启用,在 AOF 文件中记录每个写命令的时间戳信息。但 Redis7.0 以下的 Redis 可能解析不了这种格式,有兼容性问题 aof-rewrite-incremental-fsync 重写时是否采用增量同步的方式。默认值为 yes,在重写过程中会以 4MB 的增量同步,可以降低延迟;如果停用,则在 AOF 重写完成后才执行一次完全同步

三、实战

准备 Redis 环境

使用 Docker 部署 Redis7.2,假设数据和配置都放在 ~/redis 目录下

mkdir ~/redis
mkdir ~/redis/data
mkdir ~/redis/conf
touch ~/redis/conf/redis.conf

拉取镜像

docker pull redis:7.2

docker run --name redis -p 6379:6379 \
    -v ~/redis/data:/data \
    -v ~/redis/conf/redis.conf:/etc/redis/redis.conf \
    -d redis:7.2 redis-server /etc/redis/redis.conf

1. 不开启持久化功能

默认配置下,AOF 持久化功能不开启,在 ~/redis/conf/redis.conf 中填入如下内容,关闭 RDB 持久化,就相当于不开启持久化功能,服务重启后,数据就丢失了

save ""

启动 Redis,在 redis-cli 中简单插入几个字符串

> keys * 
(empty list or set)

> set test_case no_persistence
"OK"
> set data_type str
"OK"

> keys *
1) "data_type" 
2) "test_case"

查看 ~/redis/data 目录,发现没有生成 dump.rdb 文件。重启容器

docker restart redis

继续在 redis-cli 中观察,可以看到刚才的 key 已经不存在了

> keys * 
(empty list or set)

2. 开启 RDB 持久化

~/redis/conf/redis.conf 中填入如下内容,开启 RDB 持久化,3s 内至少有 1 次修改操作时,触发一次快照持久化

save 3 1

使用 docker restart redis 命令重启 redis 容器

然后编写如下 Python 测试脚本

import os
import redis
import time
import logging


def set_string(redis_conn, string_key: str):
    logging.info(f"set key: {string_key}")
    redis_conn.set(string_key, "value")


def set_strings_within_seconds(redis_conn, strings: list, n_seconds: int):
    start = time.time()
    for key_item in strings:
        set_string(redis_conn, key_item)
    elapsed = time.time() - start
    if n_seconds > elapsed:
        time.sleep(n_seconds - elapsed)


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
    r = redis.Redis(host="localhost", port=6379, db=0)
    keys = r.keys()
    logging.info(f"Init keys in memory: {len(keys)}")

    set_strings_within_seconds(r, ["string_1", "string_2"], 3)
    set_strings_within_seconds(r, ["string_3", "string_4"], 3)
    set_string(r, "string_5")
    set_string(r, "string_6")

    keys = r.keys()
    # Kill redis container immediately, mock power outage.
    os.system("docker kill redis")
    logging.info("Oops! Power outage ...")
    logging.info(f"Before killing redis, keys in memory: {len(keys)}")

    # Restart redis container.
    os.system("docker start redis")
    time.sleep(3)
    keys = r.keys()
    logging.info(f"After restarting redis, keys in memory: {len(keys)}")

输出日志如下

2024-04-22 13:02:52,187 - INFO - Init keys in memory: 0
2024-04-22 13:02:52,187 - INFO - set key: string_1
2024-04-22 13:02:52,188 - INFO - set key: string_2
2024-04-22 13:02:55,188 - INFO - set key: string_3
2024-04-22 13:02:55,190 - INFO - set key: string_4
2024-04-22 13:02:58,193 - INFO - set key: string_5
2024-04-22 13:02:58,196 - INFO - set key: string_6
redis
2024-04-22 13:02:58,623 - INFO - Oops! Power outage ...
2024-04-22 13:02:58,623 - INFO - Before killing redis, keys in memory: 6
redis
2024-04-22 13:03:02,031 - INFO - After restarting redis, keys in memory: 4

0-3s 写入了 2 个字符串 string_1string_2,第 3s 触发一次 RDB 持久化

3-6s 写入了 2 个字符串 string_3string_4,第 6s 触发一次 RDB 持久化

第 6s+ 快速写入 2 个字符串 string_5string_6,此时内存中共有 6 个字符串。然后立即使用 docker kill redis 停止了容器,模拟一次断电,此时还没来得及触发 RDB 持久化。重新启动后,内存中只有 4 个字符串,此时已经出现了数据丢失

查看 ~/redis/data 目录,发现已经有 dump.rdb 文件了

3. 开启 AOF 持久化

~/redis/conf/redis.conf 中填入如下内容,关闭 RDB 持久化,开启 AOF 持久化,并且每 1s 触发一次刷盘操作,当 AOF 文件达到 2MB 时,触发 rewrite 操作

save ""
appendonly yes
appendfsync everysec
auto-aof-rewrite-min-size 2mb

删除 ~/redis/data/dump.rdb,然后执行如下命令清空数据

docker kill redis && docker start redis

重启 redis 容器后,可以发现 data 目录下多了 AOF 持久化相关的文件

.
└── appendonlydir
    ├── appendonly.aof.1.base.rdb
    ├── appendonly.aof.1.incr.aof
    └── appendonly.aof.manifest

appendonly.aof.1.incr.aof 就是 AOF 文件,此时内容为空,接下来尝试写入一个字符串

> set string_1 value 
"OK"

再次查看 appendonly.aof.1.incr.aof 文件,发现已经追加了很多内容

*2
$6
SELECT
$1
0
*3
$3
set
$8
string_1
$5
value

可以看到,其中包含 string_1value

接下来再编写一段 Python 脚本,重复设置字符串 string_1,让 AOF 文件逐渐变大,触发 rewrite

import redis

if __name__ == "__main__":
    r = redis.Redis(host="localhost", port=6379, db=0)
    for i in range(1, 50000):
        r.set("string_1", f"value_{i}")
    r.close()

执行结束后,查看 data 目录

.
└── appendonlydir
    ├── appendonly.aof.2.base.rdb
    ├── appendonly.aof.2.incr.aof
    └── appendonly.aof.manifest

发现序号已经变成 2 了,appendonly.aof.2.base.rdb 中保存着 rewrite 后精简的写入命令

REDIS0011? redis-ver7.2.4?
redis-bits?@?ctime???%fused-mem???aof-base??string_1
                                                    value_46893?r???V?? **%**

appendonly.aof.2.incr.aof 前 12 行如下

*2
$6
SELECT
$1
0
*3
$3
SET
$8
string_1
$11
value_46894

通过以上 2 个文件,可以看出重写之前最后写入的 string_1 的值为 value_46893,重写期间持续写入的 string_1 的值为 value_46894

4. 同时开启 RDB 和 AOF 持久化

可以同时使用 RDB 和 AOF 持久化业务数据。RDB 可以提供定期的完整数据库备份;AOF 则可以记录业务数据的修改操作,以确保业务数据的增量持久化和恢复能力。RDB 定期数据备份需要结合 cron 定时任务

~/redis/conf/redis.conf 中填入如下内容。开启 RDB 持久化,每 1 小时进行一次数据备份。同时开启 AOF 持久化,并且每 1s 触发一次 AOF 刷盘操作

save 3600 1 300 100
appendonly yes
appendfsync everysec

删除 ~/redis/data/appendonlydir,然后执行如下命令清空数据

docker kill redis && docker start redis

redis-cli 中写入 3 个字符串

> set string_1 value1 
"OK" 

> set string_2 value2 
"OK" 

> set string_3 value3 
"OK"

然后重启 redis 容器,观察 data 目录,可以发现同时存在 RDB 文件和 AOF 文件

.
├── appendonlydir
│   ├── appendonly.aof.1.base.rdb
│   ├── appendonly.aof.1.incr.aof
│   └── appendonly.aof.manifest
└── dump.rdb

四、参考文档

Data persistence

Redis persistence

Redis configuration

Redis persistence demystified

The Importance of Database Persistence and Backups