Loading

[07] Redis 持久化

Redis 提供了 2 个不同形式的持久化方式:RDB(Redis DataBase),AOF(Append Of File)

1. RDB(Redis DataBase)快照

在默认情况下, Redis 将内存数据库快照保存在名字为 dump.rdb 的二进制文件中。你可以对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时,自动保存一次数据集。恢复时是将快照(二进制)文件直接读到内存里。

还可以手动执行命令生成 RDB 快照,进入 Redis 客户端执行命令 save 或 bgsave 可以生成 dump.rdb 文件,每次命令执行都会将所有 Redis 内存快照到一个新的 rdb 文件里,并覆盖原有 rdb 快照文件。

1.1 备份内存快照

思路

停止手头的工作可以保证时点性,但阻塞了客户端。不停止手头的工作,虽然不阻塞客户端,但又无法保证时点性。

  • 时点性是必须保证的,否则快照就没有了意义,那就只能尝试将阻塞客户端的时间变短一点了;
  • 之前的阻塞客户端时间,是消耗在持久化,也就是内存拷贝到硬盘这个过程;
  • 优化一下,先从内存中拷贝一份到另一块内存空间,然后再对这块新的内存空间进行持久化(让持久化和处理客户端命令的这两个过程所用到的内存空间隔离开)。

这样,持久化的过程不耽误客户端命令,同时不受客户端命令影响,保证了时点性。而阻塞客户端的时间,仅仅是内存与内存之间拷贝一份数据的时间,相比于整个持久化过程,可以忽略不计。

实现

只需要新建一个进程去做持久化的过程即可,不同进程之间的内存是隔离的,也就是新建一个进程,会将原有进程的内存空间完全拷贝一份新的。

上图只是给用户的感觉是这样的,实际上,Linux 采用了“写时复制”技术,在 fork 出子进程时并没有立刻将内存进行拷贝,仅仅是拷贝了一份映射关系,让它们暂时指向同一个内存空间。而只有当父子进程对这块内存空间进行写操作时,才会真正复制内存,而且是以〈页〉为单位

也就是说,可以利用操作系统的进程的写时复制内存的原理(Copy-On-Write, COW),来代替我自己复制全部内存这个方案,因为持久化过程,对内存的写操作想来也不会特别多,大多数值都是不变的,所以这样就提高了效率。

/*
 * 要持久化时我就 fork 一个子进程去做这件事,由操作系统的进程内存隔离的特征
 * 替我保证时点性,写时复制原理替我保证效率,也就是减少客户端阻塞时间。
 */
void rdbSaveBackground() {
    // 子进程处理(利用了操作系统的写时复制技术)
    if ((childpid = fork()) == 0) {
        // 落盘主方法
        rdbSave();
    }
}
// 待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。

整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高的性能如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。

RDB 的缺点是最后一次持久化后的数据可能丢失!

源码

// bgsave 命令通过命令模式,找到这个方法
void bgsaveCommand(redisClient *c) {
    // 进行一些校验,有点像我们 low 逼的 Controller 层做的事
    ...
    // 进入主方法
    rdbSaveBackground(server.rdb_filename)
}

// 进入主方法
int rdbSaveBackground(char *filename) {
    // 子进程处理(利用写时复制技术,保证了时点性)
    if ((childpid = fork()) == 0) {
        // 关闭监听端口,不然他也收客户端命令,就乱了
        closeListeningSockets(0);
        // 落盘主方法
        retval = rdbSave(filename);
    }
    return REDIS_OK;
}

// 落盘主方法
int rdbSave(char *filename) {
    // 用 redis IO 一点一点按照 rdb 文件格式写出去
    rdbSaveRio(&rdb, &error);
    // 内核缓冲区刷新到磁盘
    fflush(fp);
}

// 一点一点按照 rdb 文件格式写出去
int rdbSaveRio(rio *rdb, int *error) {
    // 写魔数
    rdbWriteRaw(rdb, "REDIS0006" ,9);
    // 遍历所有库
    for (j = 0; j < server.dbnum; j++) {
        // 写 SELECTDB 常量
        rdbSaveType(rdb, REDIS_RDB_OPCODE_SELECTDB);
        // 写库号
        rdbSaveLen(rdb, j);
        // 遍历字典中的所有值
        while(dict) {
            // 写具体的一个 key 和 value
            rdbSaveKeyValuePair(rdb, &key, o, expire, now);
        }
        // 写结束标识 EOF
        rdbSaveType(rdb, REDIS_RDB_OPCODE_EOF);
        // 写校验和(结束啦!)
        rioWrite(rdb, &cksum,8);
    }
}

// 反正这俩方法,最终都调用了 rdbWriteRaw
int rdbSaveType(rio *rdb, unsigned char type) {
    ...
    return rdbWriteRaw(rdb,&type,1);
}

// 反正这俩方法,最终都调用了 rdbWriteRaw
int rdbSaveLen(rio *rdb, uint32_t len) {
    ...
    return rdbWriteRaw(...)
}

// 而 rdbWriteRaw 又调了 rioWrite,真烦啊
static int rdbWriteRaw(rio *rdb, void *p, size_t len) {
    ...
    rioWrite(rdb,p,len);
    return len;
}

// rioWrite 遍历并调用 write 方法
// 这个方法是个指针,实际指向 rioFileWrite
static inline size_t rioWrite(rio *r, const void *buf, size_t len) {
    while (len) {
        write(r, buf, bytes_to_write);
        buf = (char*)buf + bytes_to_write;
        len -= bytes_to_write;
    }
    return 1;
}

// 最后一站!
static size_t rioFileWrite(rio *r, const void *buf, size_t len) {
    ...
    // 行了到这就别跟了,这是标准库方法
    // 不会的同学重学一下 c 语言哈,反正我搞 Java 的我是不会
    return fwrite(buf, len, 1, r->io.file.fp);
}

1.2 相关配置

保存位置和名称:

保存策略:

如何触发持久化:

  • 配置文件中默认的快照配置(如上图);
  • 正常关闭 shudown
  • 手动 flushall (也会产生 dump.rdb 文件,但里面是空的,无意义);
  • 直接执行 save 或者 bgsave命令;
    • save 时只管保存,其他不管,全部阻塞;
    • bgsave在后台异步进行快照操作,快照操作同时还可以响应客户端请求。可以通过 lastsave 命令获取最后一次成功执行快照的时间。

其他配置:

  • stop-writes-on-bgsave-error yes 当 Redis 无法写入磁盘的话,直接关掉 Redis 的写操作;
  • rdbcompression yes 进行 RDB 保存时,将文件压缩;
  • rdbchecksum yes 在存储快照后,还可以让 Redis 使用 CRC64 算法来进行数据校验,但是这样做会增加大约 10% 的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。

1.3 备份和恢复

  • RDB 的备份
    • 先通过 config get dir 查询 RDB 文件的目录
    • 将 *.rdb 的文件拷贝到别的地方
  • RDB 的恢复
    • 关闭 Redis
    • 先把备份的文件拷贝到工作目录下
    • 启动 Redis,备份数据会直接加载

1.4 优缺点

RDB 的优点:

  • 节省磁盘空间
    • RDB 是一个非常紧凑的文件
    • RDB 保存数据,AOF 保存指令
  • 恢复速度快
    • RDB 在保存 RDB 文件时父进程唯一需要做的就是 fork 出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他 IO 操作,所以 RDB 持久化方式可以最大化 Redis 的性能;
    • AOF 要先将指令都执行一遍以生成要恢复数据,所以,在恢复大的数据集的时候,RDB 方式会更快一些。

RDB 的缺点:

  • 数据丢失风险大。在备份周期(一定间隔时间)做一次备份,所以如果 Redis 意外 down 掉的话,就会丢失最后一次快照后的所有修改;
  • RDB 需要经常 fork 子进程来保存数据集到硬盘上,fork 时使用了“写时拷贝技术”,大约 2 倍的膨胀性需要考虑,当数据集比较大的时候,fork 的过程是非常耗时的,可能会导致 Redis 在一些毫秒级不能响应客户端请求。

2. AOF(Append Only File)日志

2.1 日志保存

以日志的形式来记录每个写操作,将 Redis 执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,Redis 启动之初会读取该文件重新构建数据,换言之,Redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

AOF 默认不开启,需要手动在配置文件中开启。还可以在 redis.conf 中配置文件名称,默认如下;AOF 文件的保存路径,同 RDB 的路径一致。

AOF 和 RDB 同时开启,Redis 听 AOF 的。

2.2 文件故障

  • 文件故障备份
    • AOF 的备份机制和性能虽然和 RDB 不同,但是备份和恢复的操作同 RDB 一样,都是拷贝备份文件,需要恢复时再拷贝到 Redis 工作目录下,启动系统即加载;
    • AOF 和 RDB 同时开启,系统默认取 AOF 的数据;
  • 文件故障恢复
    • AOF 文件的保存路径,同 RDB 的路径一致;
    • 如遇到 AOF 文件损坏,可通过 redis-check-aof --fix appendonly.aof 进行恢复;

AOF 文件里边包含了重建 Redis 数据所需的所有写命令,所以 Redis 只要读入并重新执行一遍 AOF 文件里边保存的写命令,就可以还原 Redis 关闭之前的状态。

Redis 读取 AOF 文件并且还原数据库状态的详细步骤如下:

  1. 创建一个不带网络连接的的伪客户端,因为 Redis 的命令只能在客户端上下文中执行,而载入 AOF 文件时所使用的的命令直接来源于 AOF 文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行 AOF 文件保存的写命令,伪客户端执行命令的效果和带网络。连接的客户端执行命令的效果完全一样的。
  2. 从 AOF 文件中分析并取出一条写命令。
  3. 使用伪客户端执行被读出的写命令。
  4. 一直执行步骤 2 和步骤 3,直到 AOF 文件中的所有写命令都被处理完毕为止。

当完成以上步骤之后,AOF 文件所保存的数据库状态就会被完整还原出来。

2.3 同步频率设置

  • 始终同步,每次 Redis 的写入都会立刻记入日志,会严重减低服务器的性能;
  • 每秒同步,每秒记入日志一次,如果宕机,也只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;
  • 不主动进行同步,把同步时机交给操作系统,这并不能给服务器性能带来多大的提升,而且也会增加系统崩溃时数据丢失的数量。

2.4 文件重写

AOF 采用文件追加方式,文件会越来越大。为避免出现此种情况,新增了「重写机制」,当 AOF 文件的大小超过所设定的阈值时,Redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集

如何实现重写?

AOF 文件持续增长而过大时,会 fork 出一条新进程来将文件重写(也是先写临时文件最后再 rename,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作),遍历新进程的内存中数据,每条记录有一条的 set 语句。

整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。

AOF 文件重写并不需要对现有的 AOF 文件进行任何读取、分析或者写入操作,而是通过读取服务器当前的数据库状态来实现的。首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是AOF 重写功能的实现原理。

何时重写?

重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定 Redis 要满足一定条件才会进行重写。

(1)手动调用 bgrewriteaof 命令,如果当前有正在运行的 rewrite 子进程,则本次rewrite 会推迟执行,否则,直接触发一次 rewrite。

(2)自动触发,就是根据配置规则来触发。系统载入时或者上次重写完毕时,Redis 会记录此时 AOF 大小,设为 base_size,如果 Redis 的 AOF 当前大小 >= base_size + base_size*100% (默认)当前大小 >= 64mb(默认) 的情况下,Redis 会对 AOF 进行重写。

# AOF文件自上一次重写后文件大小增长了 100% 则再次触发重写
auto-aof-rewrite-percentage 100
# AOF文件至少要达到 64M 才会自动重写,文件太小恢复速度本来就很快,重写的意义不大。
auto-aof-rewrite-min-size 64mb

重写流程

AOF 重写函数会进行大量的写入操作,调用该函数的线程将被长时间阻塞,所以 Redis 在子进程中执行AOF 重写操作。

在整个 AOF 后台重写过程中,只有信号处理函数执行时会对 Redis 主进程造成阻塞,在其他时候,AOF后台重写都不会阻塞主进程。

2.5 优缺点

优点:

  • 备份机制更稳健,丢失数据概率更低(丢 1s);
  • Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写;
  • AOF 文件有序地保存了对数据执行的所有写入操作,这些写入操作以 Redis 协议的格式保存,因此 AOF 文件的内容非常容易被人读懂,对文件进行分析也很轻松。通过操作 AOF 文件,可以处理误操作。

缺点:

  • 比起 RDB 占用更多的磁盘空间;
  • 恢复备份速度要慢于 RDB;
  • 每次读写都同步的话,有一定的性能压力;
  • 存在个别 Bug,造成恢复不能。

小结:

如上图所示,AOF 持久化功能的实现可以分为命令追加(append)、文件写入(write)、文件同步(sync)、文件重写(rewrite)和重启加载(load)。其流程如下:

  1. 所有的写命令会追加到 AOF 缓冲中。
  2. AOF 缓冲区根据对应的策略向硬盘进行同步操作。
  3. 随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
  4. 当 Redis 重启时,可以加载 AOF 文件进行数据恢复。

RDB 与 AOF 对比:

3. 混合持久化

3.1 持久化方式怎么选

  • 生产环境可以都启用,Redis 启动时如果既有 RDB 文件又有 AOF 文件则优先选择 AOF 文件恢复数据,因为 AOF 一般来说数据更全一点;
  • 如果对数据不敏感,可以选单独用 RDB;
  • 不建议单独用 AOF,因为可能会出现 Bug;
  • 如果只是做纯内存缓存,可以都不用。

3.2 混合持久化

重启 Redis 时,我们很少使用 RDB 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 RDB来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。 Redis 4.0 为了解决这个问题,带来了一个新的持久化选项 —— 混合持久化。

通过如下配置可以开启混合持久化(必须先开启 AOF),即使用 RDB 作为全量备份,两次 RDB 之间使用 AOF 作为增量备份。

aof‐use‐rdb‐preamble yes

如果开启了混合持久化,AOF 在重写时,不再是单纯将内存数据转换为 RESP 命令写入 AOF 文件,而是将重写这一刻之前的内存做 RDB 快照处理,并且将 RDB 快照内容和增量的 AOF 修改内存数据的命令存在一起,都写入新的 AOF 文件,新的文件一开始不叫 appendonly.aof,等到重写完新的 AOF 文件才会进行改名,覆盖原有的 AOF 文件,完成新旧两个 AOF 文件的替换。

于是在 Redis 重启的时候,可以先加载 RDB 的内容,然后再重放增量 AOF 日志,就可以完全替代之前的 AOF 全量文件重放,因此重启效率大幅得到提升。

日志前半部分为二进制的 RDB 格式,后半部分是 AOF 命令日志。

3.3 数据备份策略

  1. 写 crontab 定时调度脚本,每小时都 copy 一份 RDB 或 AOF 的备份到一个目录中去,仅仅保留最近 48 小时的备份;
  2. 每天都保留一份当日的数据备份到一个目录中去,可以保留最近 1 个月的备份;
  3. 每次 copy 备份的时候,都把太旧的备份给删了;
  4. 每天晚上将当前机器上的备份复制一份到其他机器上,以防机器损坏。
posted @ 2020-09-04 18:19  tree6x7  阅读(181)  评论(0编辑  收藏  举报